보안/Reversing

[Dreamhack] Recover

melonbbang-ruffy 2024. 12. 27. 18:45

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

 

Recover

Description (en) This challenge provides chall binary, along with encrypted file. The chall binary encrypts the flag.png file containing the flag, then stores it as an encrypted file. Recover flag.png file to get the flag! The flag format for this challeng

dreamhack.io

chall 바이너리의 암호화 방식을 리버싱을 통해 분석하여 복호화 방법을 알아낸 후, encrypted 파일을 복호화해서 flag.png를 복구하는 문제인가...?

일단 파일을 다운받고 까보자.

64비트의 elf 파일이다.

gdb와 ida로 분석해봐야 할듯

아무래도 심볼이 없는 모양

컴파일 시 -g 옵션을 주지 않은 모양이다.

gdb에서 No symbol~ 이 뜰 때 우리가 디버깅 할 수 있는 main 함수는 .text 영역에 있기 때문에, .text 영역의 메모리 주소를 찾으면 메인 함수 엔트리포인트를 찾아서 동적 디버깅이 가능하다. 하지만 이렇게까지 돌아서 문제를 풀 필요는 없을 것 같다.

code

ida에서 디컴파일한 코드를 분석해 봤다.

더보기
#include <defs.h>


//-------------------------------------------------------------------------
// Function declarations

__int64 (**init_proc())(void);
void sub_1020();
void sub_1030();
void sub_1040();
void sub_1050();
void sub_1060();
void sub_1070();
void sub_1080();
void sub_1090();
// int __fastcall _cxa_finalize(void *);
// int puts(const char *s);
// size_t fread(void *ptr, size_t size, size_t n, FILE *stream);
// int fclose(FILE *stream);
// FILE *fopen(const char *filename, const char *modes);
// void __noreturn exit(int status);
// size_t fwrite(const void *ptr, size_t size, size_t n, FILE *s);
void __fastcall __noreturn start(__int64, __int64, void (*)(void)); // idb
char *deregister_tm_clones();
__int64 register_tm_clones(void); // idb
char *_do_global_dtors_aux();
__int64 j_register_tm_clones(void); // idb
__int64 __fastcall main(int a1, char **a2, char **a3);
void term_proc();
// int __fastcall _libc_start_main(int (__fastcall *main)(int, char **, char **), int argc, char **ubp_av, void (*init)(void), void (*fini)(void), void (*rtld_fini)(void), void *stack_end);
// int __fastcall __cxa_finalize(void *);
// __int64 _gmon_start__(void); weak

//-------------------------------------------------------------------------
// Data declarations

_UNKNOWN unk_2004; // weak
void *off_4008 = &off_4008; // idb
char byte_4010; // weak


//----- (0000000000001000) ----------------------------------------------------
__int64 (**init_proc())(void)
{
  __int64 (**result)(void); // rax

  result = &_gmon_start__;
  if ( &_gmon_start__ )
    return (__int64 (**)(void))_gmon_start__();
  return result;
}
// 4068: using guessed type __int64 _gmon_start__(void);

//----- (0000000000001020) ----------------------------------------------------
void sub_1020()
{
  JUMPOUT(0LL);
}
// 1026: control flows out of bounds to 0

//----- (0000000000001030) ----------------------------------------------------
void sub_1030()
{
  sub_1020();
}

//----- (0000000000001040) ----------------------------------------------------
void sub_1040()
{
  sub_1020();
}

//----- (0000000000001050) ----------------------------------------------------
void sub_1050()
{
  sub_1020();
}

//----- (0000000000001060) ----------------------------------------------------
void sub_1060()
{
  sub_1020();
}

//----- (0000000000001070) ----------------------------------------------------
void sub_1070()
{
  sub_1020();
}

//----- (0000000000001080) ----------------------------------------------------
void sub_1080()
{
  sub_1020();
}

//----- (0000000000001090) ----------------------------------------------------
void sub_1090()
{
  sub_1020();
}

//----- (0000000000001120) ----------------------------------------------------
// positive sp value has been detected, the output may be wrong!
void __fastcall __noreturn start(__int64 a1, __int64 a2, void (*a3)(void))
{
  __int64 v3; // rax
  int v4; // esi
  __int64 v5; // [rsp-8h] [rbp-8h] BYREF
  char *retaddr; // [rsp+0h] [rbp+0h] BYREF

  v4 = v5;
  v5 = v3;
  _libc_start_main((int (__fastcall *)(int, char **, char **))main, v4, &retaddr, 0LL, 0LL, a3, &v5);
  __halt();
}
// 112A: positive sp value 8 has been found
// 1131: variable 'v3' is possibly undefined

//----- (0000000000001150) ----------------------------------------------------
char *deregister_tm_clones()
{
  return &byte_4010;
}
// 4010: using guessed type char byte_4010;

//----- (0000000000001180) ----------------------------------------------------
__int64 register_tm_clones(void)
{
  return 0LL;
}

//----- (00000000000011C0) ----------------------------------------------------
char *_do_global_dtors_aux()
{
  char *result; // rax

  if ( !byte_4010 )
  {
    if ( &__cxa_finalize )
      _cxa_finalize(off_4008);
    result = deregister_tm_clones();
    byte_4010 = 1;
  }
  return result;
}
// 4010: using guessed type char byte_4010;

//----- (0000000000001200) ----------------------------------------------------
// attributes: thunk
__int64 j_register_tm_clones(void)
{
  return register_tm_clones();
}

//----- (0000000000001209) ----------------------------------------------------
__int64 __fastcall main(int a1, char **a2, char **a3)
{
  char ptr; // [rsp+Bh] [rbp-25h] BYREF
  int v5; // [rsp+Ch] [rbp-24h]
  _BYTE *v6; // [rsp+10h] [rbp-20h]
  FILE *stream; // [rsp+18h] [rbp-18h]
  FILE *s; // [rsp+20h] [rbp-10h]
  unsigned __int64 v9; // [rsp+28h] [rbp-8h]

  v9 = __readfsqword(0x28u);
  v6 = &unk_2004;
  stream = fopen("flag.png", "rb");
  if ( !stream )
  {
    puts("fopen() error");
    exit(1);
  }
  s = fopen("encrypted", "wb");
  if ( !s )
  {
    puts("fopen() error");
    fclose(stream);
    exit(1);
  }
  v5 = 0;
  while ( fread(&ptr, 1uLL, 1uLL, stream) == 1 )
  {
    ptr ^= v6[v5 % 4];
    ptr += 19;
    fwrite(&ptr, 1uLL, 1uLL, s);
    ++v5;
  }
  fclose(stream);
  fclose(s);
  return 0LL;
}

//----- (0000000000001364) ----------------------------------------------------
void term_proc()
{
  ;
}

flag.png 파일을 받아서 encrypted 파일로 만드는 과정만 살펴보자.

-> main 함수 살펴보기!

__int64 __fastcall main(int a1, char **a2, char **a3) {
  char ptr; // 바이트 단위로 읽기 위한 변수
  int v5;   // 바이트 처리 시 키 순환을 위한 인덱스
  _BYTE *v6; // 4바이트 키의 시작 주소
  FILE *stream; // 입력 파일 (flag.png)
  FILE *s;      // 출력 파일 (encrypted)

  v6 = &unk_2004; // 키값 가져옴
  stream = fopen("flag.png", "rb");
  if ( !stream ) {
    puts("fopen() error");
    exit(1);
  }

  s = fopen("encrypted", "wb");
  if ( !s ) {
    puts("fopen() error");
    fclose(stream);
    exit(1);
  }

  v5 = 0; // 인덱스 초기화
  while ( fread(&ptr, 1uLL, 1uLL, stream) == 1 ) { // 한 바이트씩 읽어오기
    ptr ^= v6[v5 % 4]; // 키 값에서 1바이트씩 가져와서 XOR연산 후 저장, 키 바이트는 4바이트이므로 0, 1, 2, 3...이렇게 순환함
    ptr += 19; // 19 더하기
    fwrite(&ptr, 1uLL, 1uLL, s); // 결과 바이트를 출력
    ++v5; // 인덱스 증가
  }
  fclose(stream);
  fclose(s);
  return 0LL;
}

unk_2004 변수의 주소를 가져와서 v6 변수에 저장한 후, flag.png 파일을 1바이트 씩 가져와 xor 연산 후 연산한 값에 19를 더한 뒤 encrypted 파일에 해당 결과값을 쓰고 있다.

그대로 역연산을 해 보자면, 먼저 encrypted 파일의 바이트에 19를 뺀 후(16진수 기준) 키 값을 구해서 그대로 첫번째 인덱스 값부터 xor 연산을 수행하면 원본 파일인 flag.png를 복구할 수 있을 듯 하다.

Exploit

HxD를 켜서 파일의 시그니처 값을 비교해서 풀이 가능할 듯 싶다.

encrypted 파일의 헤더 시그니처 :

6A 10 03 BB E6 BA B7 F8

 

png 파일의 헤더 시그니처 : 

89 50 4E 47 0D 0A 1A 0A

encrypted 파일의 헤더 시그니처에 19를 빼고, png 파일의 헤더 시그니처의 값을 하나씩 xor 연산하면 키 바이트의 값을 구할 수 있을 것 같다.

def calculate_key(encrypted_signature, png_signature):
    key = []
    for enc, png in zip(encrypted_signature, png_signature):
        key_byte = ((enc - 19) & 0xFF) ^ png  # 연산 결과값이 음수가 나오는 걸 방지함
        key.append(key_byte)
    return key

if __name__ == "__main__":
    encrypted_signature = [0x6A, 0x10, 0x03, 0xBB, 0xE6, 0xBA, 0xB7, 0xF8]
    png_signature = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]

    key = calculate_key(encrypted_signature, png_signature)
    print("Calculated Key:", ["0x{:02X}".format(k) for k in key])
    # 02: 숫자를 최소 2자리로 표현, 한 자리 숫자는 앞에 0을 추가
	# X: 숫자를 대문자 16진수로 표현하겠다는 의미

파이썬에서 zip 함수는 두개의 리스트를 병렬로 묶어주는 함수로서, 같은 인덱스에 존재하는 값을 하나의 튜플로 묶어준다.

예를 들어, 0번 인덱스의 두 값이 0x6a, 0x89라고 할 때, (enc, png) = (0x6a, 0x89) 이렇게 저장해서 계산하도록 해 주는 것

키는 총 4바이트이므로, [0xDE, 0xAD, 0xBE, 0xEF]

키를 구했으므로, encrypted 파일의 바이트들을 역연산해서 flag.png를 복원하는 코드를 짜 보자.

def decrypt_file(encrypted_file, output_file, key):
    key_length = len(key)

    with open(encrypted_file, 'rb') as enc_file, open(output_file, 'wb') as out_file:
        index = 0  

        while byte := enc_file.read(1):  # encrypted 파일에서 1 바이트씩 읽어옴
            byte = bytearray(byte)[0]  # 바이트 -> 정수값 변환
            byte = (byte - 19) & 0xFF  # 19를 뺀 후, 음수값을 방지하기 위해 0xff를 and 연산함
            byte ^= key[index % key_length]  # 키 값과 xor 연산
            out_file.write(byte.to_bytes(1, 'little'))  # 바이트로 변환 후 1바이트씩 리틀 엔디언으로 파일 작성
            index += 1  # 인덱스 1씩 증가

if __name__ == "__main__": # 코드가 실행되면 바로 실행되는 부분
    key = [0xDE, 0xAD, 0xBE, 0xEF]
    decrypt_file("encrypted", "flag.png", key)
    print("Decryption complete! Check the output file: flag.png")

코드를 실행시키면 플래그가 적힌 png 파일이 생성된 것을 확인할 수 있다.

 

Exploit 2

data 영역과 rodata 영역을 조사하여 값을 알아낼 수도 있다.

data 영역에는 딱히 단서될 만한 게 없었다. rodata 영역을 한번 살펴보자.

사실 야매긴 하지만 저 deadbeef가 너무 수상하기 때문에 바로 키 값인 걸 알아챌듯

사실 flag.png 바로 앞에 위치한 고정된 문자열이기 때문에 이 친구가 키 값이라고 추론이 가능하긴 하다.

bss 세그먼트 : 선언 후 초기화되지 않은 전역변수가 위치함, 프로그램 시작 시 모두 0으로 초기화가 됨
data 세그먼트 : 초기화된 전역변수 or 전역상수가 위치함
rodata 세그먼트 : data 세그먼트 안에 위치한 수정 불가능한 영역으로, 값이 변하면 안되는 데이터가 위치한다. (문자열, 전역 상수 )
전역변수를 선언하고 초기화하지 않을 경우 프로그램 실행 도중 변수에 값을 할당하는 과정이 있을텐데, 이때 쓰기 권한이 사용되기 때문에 bss 세그먼트에는 읽기권한 뿐만 아니라 쓰기권한도 존재한다.
프로그램을 시작하는 시점에 이미 값이 쓰여있는 경우, 프로그램 실행 도중 값이 변경될 수도 있고 변경되지 않을 수도 있다. 예를 들어 printf("hello")의 경우, 프로그램 실행 과정에서 변경될 필요가 없다.
즉, 데이터 영역에 쓰기, 읽기권한이 모두 있을 경우 -> data 세그먼트 위치
데이터 영역에 읽기권한만 있을 경우 -> data 세그먼트 안 rodata 세그먼트에 위치

초기화된 전역변수, 전역상수 모두 프로그램 시작 지점에는 값이 유지가 되어야 하기 때문에 모두 data 세그먼트 영역에 속하고, 거기서 변경되냐 / 변경되지 않느냐에 따라 data 세그먼트에 위치 / rodata 세그먼트에 위치하게 된다.

 

Referneces

https://juntheworld.tistory.com/118

 

BSS vs 데이터 vs rodata (전역영역 이름 구분하기)

BSS / 데이터 / rodata 세그먼트 모두 전역변수/상수가 저장되는 위치이다. 일단 이거는 머리에 넣어두고 다만 어떤 전역변수/상수가 저장되느냐 인데, 이 글을 통해 살펴보자. 각 세그먼트마다 머

juntheworld.tistory.com