CTF & Wargame(REVERSING)

[dreamhack] level1 legacyopt 문제

KWAKBUMJUN 2025. 3. 9. 03:06

legacyopt 바이너리를 실행하면 입력할 수 있는 칸이 나온다. 

입력을 하면 7912394e5c 와 같은 특정한 문자열을 출력해주는 것을 확인할 수 있다.

그리고 같은 디렉터리에 저장되어 있는 output.txt를 확인해보면

220c6a33204455fb390074013c4156d704316528205156d70b217c14255b6ce10837651234464e

라는 문자열이 저장되어 있다.

 

IDA 분석

main 함수 디컴파일 코드

사용자에게 입력받은 값을 s에 저장

v3에 입력값의 길이를 저장

 ptr, s, v3를 인자로 하는 sub_1209함수 실행

s의 길이만큼 반복

ptr[i]를 출력

 

sub_1209 함수 분석

unsigned __int64 __fastcall sub_1209(_BYTE *ptr, char *input, int input_len)
{
** 생략 **
  v3 = input_len + 7;
  v4 = input_len + 14;
  if ( v3 < 0 )
    v3 = v4;
  v32 = v3 >> 3;
  result = (unsigned int)(input_len % 8);
  switch ( (int)result )
  {
    case 0:
      goto LABEL_4;
    case 1:
      goto LABEL_11;
    case 2:
      goto LABEL_10;
    case 3:
      goto LABEL_9;
    case 4:
      goto LABEL_8;
    case 5:
      goto LABEL_7;
    case 6:
      goto LABEL_6;
    case 7:
      while ( 1 )
      {
        v9 = input++;
        v10 = *v9;
        v11 = ptr++;
        *v11 = v10 ^ 0x66;
LABEL_6:
        v12 = input++;
        v13 = *v12;
        v14 = ptr++;
        *v14 = v13 ^ 0x44;
LABEL_7:
        v15 = input++;
        v16 = *v15;
        v17 = ptr++;
        *v17 = v16 ^ 0x11;
LABEL_8:
        v18 = input++;
        v19 = *v18;
        v20 = ptr++;
        *v20 = v19 ^ 0x77;
LABEL_9:
        v21 = input++;
        v22 = *v21;
        v23 = ptr++;
        *v23 = v22 ^ 0x55;
LABEL_10:
        v24 = input++;
        v25 = *v24;
        v26 = ptr++;
        *v26 = v25 ^ 0x22;
LABEL_11:
        v27 = input++;
        v28 = *v27;
        result = (unsigned __int64)ptr++;
        *(_BYTE *)result = v28 ^ 0x33;
        if ( --v32 <= 0 )
          break;
LABEL_4:
        v6 = input++;
        v7 = *v6;
        v8 = ptr++;
        *v8 = v7 ^ 0x88;
      }
      break;
    default:
      return result;
  }
  return result;
}

input_len % 8 한 값을 result에 저장

만약 result의 값이 0이면 LABEL_4, 1이면 LABEL_11, 2면 LABEL_10 등등으로 이동한다.

 

핵심 : 해당 함수가 입력받은 문자열을 암호화 시킨다.

해결 아이디어

output.txt에 암호화 되어 있는 문자열이 저장되어 있다.

--> 220c6a33204455fb390074013c4156d704316528205156d70b217c14255b6ce10837651234464e

따라서 해당 문자열을 복호화 시키면 flag를 얻을 수 있을 것 같다.

sub_1209 함수가 입력받은 문자열을 특정 연산을 통해 암호화 시키기 때문에 어떤 값을 입력해야 220c6a33204455fb390074013c4156d704316528205156d70b217c14255b6ce10837651234464e로 암호화 되는지 알아내야 할 것 같다.

 

결론적으론 output.txt 파일을 복호화 시켜야 한다.

 

 

해당 바이너리를 확인해보면 문자 하나만 입력한 경우에는 52와 같이 두개의 문자를 출력, 문자 2개를 입력하면 4개의 문자를 출력한다.

 

따라서 output.txt에 저장된 문자의 길이는 78이기 때문에 flag는 78 / 2 = 39개의 문자로 구성되어 있다는 것을 알 수 있다.

예시로 DH{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}를 입력하면 

220c6a16344352e907257016344352e907257016344352e907257016344352e90725701634434e 라는 결과를 얻을 수 있는데, DH{를 암호화하면 220c6a, 마지막 문자인 }를 암호화 하면 4e이기 때문에 output.txt와 같은 문자열로 암호화 되어 있는 것을 확인할 수 있다.

 

따라서 브루트 포스를 진행하여 나머지 값들을 알아낼 것이다.

 

<과정>

가정 : flag - DH{aaaa}

encrypted_code = "220c6a33204455fb390074013c4156d704316528205156d70b217c14255b6ce10837651234464e"

flag = ""

아스키 코드의 값을 legacyopt 바이너리에 하나씩 대입한다. 

ex) DH{a}

a를 대입하고 복호화 했을때 len(DH{a})의 수 * 2만큼의 복호화 키가 encrypted_code와 일치하면 flag변수에 해당 값을 저장

만약 일치하지 않으면 다음 아스키 코드 대입

 

import subprocess

length = 39
prefix = "DH{"
subfix = "}"
valid_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}"

flag = list(prefix) + ["a"] * (length - len(prefix) - len(subfix)) + list(subfix)

with open("./output.txt", "r") as f:
    data = f.read()

for i in range(len(prefix), length - len(subfix)):
    for char in valid_chars:
        # 맨 처음엔 ['D', 'H', '{', 'a', 'a', 'a', ..., 'a',
        # 두번째엔 ['D', 'H', '{', 'b', 'a', 'a', ..., 'a', '}']
        # 이런식으로 작동

        flag[i] = char

        # 매 시도마다 새로운 프로세스를 생성
        process = subprocess.Popen(
            ["./legacyopt"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
        )

        try:
            process.stdin.write("".join(flag) + "\n")
            process.stdin.flush()  # 데이터 플러쉬(버퍼에 들어있는 데이터 작성)
            encrypted_output = process.stdout.read(length * 2)


            if encrypted_output[:(i + 1) * 2] == data[:(i + 1) * 2]:
                print(f"{i}, {char}")
                break
        except Exception as e:
            print(f"[!] Error during process execution: {e}")
        finally:
            process.terminate()  # 한번 반복이 마무리 될 때마다 무조건 terminate로 프로세스 초기화

print(''.join(flag))

 

exploit 코드

 

코드 작성하는게 어려워서 다른 사람 코드를 참고했다..

 

아무튼 flag : DH{Duffs_Device_but_use_memcpy_instead}