리버싱 write-up

HW·2024년 11월 14일

rev-basic-0
https://dreamhack.io/wargame/challenges/14

Reversing Basic Challenge #0
이 문제는 사용자에게 문자열 입력을 받아 정해진 방법으로 입력값을 검증하여 correct 또는 wrong을 출력하는 프로그램이 주어집니다.

해당 바이너리를 분석하여 correct를 출력하는 입력값을 찾으세요!

sol)
chall0.exe를 IDA로 열어보자.

main 함수 디컴파일하면 다음과 같다.

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4[256]; // [rsp+20h] [rbp-118h] BYREF

  memset(v4, 0, sizeof(v4));
  sub_140001190("Input : ", argv, envp);
  sub_1400011F0("%256s", v4);
  if ( (unsigned int)sub_140001000(v4) ) // v4 배열의 값을 sub_140001000 함수에 넘겨 결과가 참이면
    puts("Correct"); // "Correct" 출력
  else
    puts("Wrong");
  return 0;
}


입력값을 v4 배열에 저장하고 이를 sub_140001000 함수로 처리한 후 반환값에 따라 결과를 출력


그러면 이제 sub_140001000 함수를 살펴보자.
sub_140001000()는 다음과 같다.

_BOOL8 __fastcall sub_140001000(const char *a1)
{
  return strcmp(a1, "Compar3_the_str1ng") == 0;
}


주어진 문자열 a1이 "Compar3_the_str1ng"과 일치하면 (strcmp는 0 반환, 그 결과 == 0이 true가 되어) sub_140001000 함수는 1 반환

이 프로그램은 입력한 문자열이 "Compar3_the_str1ng"이라면 sub_140001000은 1을 반환하여 "Correct" 출력

flag 획득: DH{Compar3_the_str1ng}


rev-basic-8
https://dreamhack.io/wargame/challenges/22

Reversing Basic Challenge #8
이 문제는 사용자에게 문자열 입력을 받아 정해진 방법으로 입력값을 검증하여 correct 또는 wrong을 출력하는 프로그램이 주어집니다.

해당 바이너리를 분석하여 correct를 출력하는 입력값을 찾으세요!

sol)
main 함수는 다음과 같다.

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4[256]; // [rsp+20h] [rbp-118h] BYREF

  memset(v4, 0, sizeof(v4));
  sub_1400011B0("Input : ", argv, envp);
  sub_140001210("%256s", v4);
  if ( (unsigned int)sub_140001000(v4) )
    puts("Correct");
  else
    puts("Wrong");
  return 0;
}



이제 sub_140001000()를 살펴보자.
sub_140001000()는 다음과 같다.

__int64 __fastcall sub_140001000(__int64 a1)
{
  int i; // [rsp+0h] [rbp-18h]

  for ( i = 0; (unsigned __int64)i < 0x15; ++i )
  {
    if ( (unsigned __int8)(-5 * *(_BYTE *)(a1 + i)) != byte_140003000[i] )
      return 0i64;
  }
  return 1i64;
}

브루트 포스 방식으로 byte_140003000 배열의 각 값에 대해 적절한 ASCII 문자를 찾는 코드 작성. candidate -5가 암호화 방식이므로 이를 복호화하려면 -5로 곱한 값을 찾아야 하므로 그 값을 일일이 시도해가며 일치하는 값을 찾는다. byte_140003000[i]가 candidate -5와 일치하는 candidate 값이 무엇인지를 찾는 것이 포인트이다.

# ex.py
# 암호화된 값 배열 (각 값은 특정 문자의 -5배 곱한 결과)
byte_140003000 = [
    0xAC, 0xF3, 0x0C, 0x25, 0xA3, 0x10, 0xB7, 0x25,
    0x16, 0xC6, 0xB7, 0xBC, 0x07, 0x25, 0x02, 0xD5,
    0xC6, 0x11, 0x07, 0xC5
]

# 결과를 저장할 리스트
ai = []

# 0x20 (공백) ~ 0x7E (~) 사이의 ASCII printable 문자를 모두 시도
# 해당 범위 안의 문자를 'candidate'로 하여 반복
for i in range(20):  # byte_140003000 배열의 각 값에 대해 반복
    for candidate in range(0x20, 0x7E):  # ASCII printable 문자 범위
        # candidate 값을 -5로 곱하고 그 결과를 8비트로 마스킹 (0~255 사이 값으로 변환)
        # Why? -> 암호화된 값이 'candidate * -5'로 계산되었으므로 이를 복호화하려면 역으로 찾아야 함
        if (candidate * -5) & 0xFF == byte_140003000[i]:  
            # 'candidate' 값에 해당하는 문자 출력
            print(chr(candidate), end='')  # 해당 값에 대응되는 문자 출력
            ai.append(chr(candidate))  # 찾은 문자를 리스트에 추가
            break  # 해당 값을 찾았으므로 다른 'candidate' 값을 더 이상 시도할 필요 없이 반복문 종료

# 결과적으로 print(chr(candidate))는 암호화된 값에 대응하는 원래 문자를 출력하게 됨


patch
https://dreamhack.io/wargame/challenges/49

flag를 그리는 루틴을 분석하고 가려진 flag를 보이게 해주세요.

sol)
Patch.exe 실행해보면 다음과 같다.

WinMain()은 다음과 같다.

int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
  HCURSOR CursorW; // rax
  HWND Window; // rax
  HWND v8; // rbx
  HACCEL AcceleratorsW; // rbx
  WNDCLASSEXW v11; // [rsp+60h] [rbp-A8h] BYREF
  struct tagMSG Msg; // [rsp+B0h] [rbp-58h] BYREF

  LoadStringW(hInstance, 0x67u, &WindowName, 100);
  LoadStringW(hInstance, 0x6Du, &ClassName, 100);
  v11.cbSize = 80;
  v11.style = 3;
  v11.lpfnWndProc = (WNDPROC)sub_1400032F0;
  *(_QWORD *)&v11.cbClsExtra = 0i64;
  v11.hInstance = hInstance;
  v11.hIcon = LoadIconW(hInstance, (LPCWSTR)0x6B);
  CursorW = LoadCursorW(0i64, (LPCWSTR)0x7F00);
  *(__m128i *)&v11.hbrBackground = _mm_load_si128((const __m128i *)&xmmword_1400053F0);
  v11.hCursor = CursorW;
  v11.lpszClassName = &ClassName;
  v11.hIconSm = LoadIconW(hInstance, (LPCWSTR)0x6C);
  RegisterClassExW(&v11);
  qword_140007880 = (__int64)&unk_1400078A0;
  Window = CreateWindowExW(0, &ClassName, &WindowName, 0xC80000u, 0x80000000, 0, 600, 200, 0i64, 0i64, hInstance, 0i64);
  v8 = Window;
  if ( Window )
  {
    hWnd = Window;
    dword_140007920 = 600;
    dword_140007924 = 200;
    GdiplusStartup(&unk_1400078A0, &dword_1400078A8, 0i64);
    ShowWindow(v8, nShowCmd);
    UpdateWindow(v8);
    AcceleratorsW = LoadAcceleratorsW(hInstance, (LPCWSTR)0x6D);
    while ( GetMessageW(&Msg, 0i64, 0, 0) )
    {
      if ( !TranslateAcceleratorW(Msg.hwnd, AcceleratorsW, &Msg) )
      {
        TranslateMessage(&Msg);
        DispatchMessageW(&Msg);
      }
    }
    LODWORD(Window) = Msg.wParam;
  }
  return (int)Window;
}


lpfnWndProc가 보인다. 이제 sub_1400032F0을 분석해보자.

LRESULT __fastcall sub_1400032F0(HWND a1, UINT a2, WPARAM a3, LPARAM a4)
{
  _QWORD *v5; // rbx
  __int64 v6; // rbx
  __int64 v7; // [rsp+20h] [rbp-18h] BYREF

  switch ( a2 )
  {
    case 2u:
      PostQuitMessage(0);
      return 0i64;
    case 0xFu:
      qword_140007910 = (__int64)BeginPaint(hWnd, &Paint);
      v5 = (_QWORD *)GdipAlloc(16i64);
      if ( v5 )
      {
        *v5 = 0i64;
        v5[1] = 0i64;
        v7 = 0i64;
        *((_DWORD *)v5 + 2) = GdipCreateFromHDC(qword_140007910, &v7);
        *v5 = v7;
      }
      else
      {
        v5 = 0i64;
      }
      qword_140007918 = (__int64)v5;
      sub_140002C40();
      v6 = qword_140007918;
      if ( qword_140007918 )
      {
        GdipDeleteGraphics(*(_QWORD *)qword_140007918);
        GdipFree(v6);
      }
      EndPaint(hWnd, &Paint);
      return 0i64;
    case 0x202u:
      InvalidateRect(hWnd, 0i64, 1);
      UpdateWindow(hWnd);
      return 0i64;
    default:
      return DefWindowProcW(a1, a2, a3, a4);
  }
}

sub_1400032F0는 WndProc 함수로, 전달된 메시지 a2에 따라 다음과 같은 처리를 한다.
WM_QUIT (2): 프로그램 종료 (PostQuitMessage(0) 호출)
WM_PAINT (0xF): 화면을 그림. BeginPaint, EndPaint, GDI+의 GdipCreateFromHDC와 GdipDeleteGraphics가 호출됨
WM_USER (0x202): 윈도우를 무효화하고 업데이트 (InvalidateRect, UpdateWindow)

BeginPaint와 EndPaint 사이에 있는 sub_140002C40()는 GDI+ 객체로 그리는 작업을 처리할 것으로 추측된다. 즉 아마도 여기서 실제로 화면에 그림을 그리거나 텍스트를 출력하는 작업이 이루어질 것 같다.
이제 sub_140002C40 함수를 분석해보자.

char __fastcall sub_140002C40(__int64 a1, int a2)
{
  int v2; // ebx
  int v3; // edx
  int v4; // edx
  int v5; // edx
  int v6; // edx
  int v7; // edx
  int v8; // edx
  int v9; // edx
  int v10; // edx
  int v11; // edx
  int v12; // edx
  int v13; // edx
  int v14; // edx
  int v15; // edx
  int v16; // edx
  int v17; // edx
  int v18; // edx
  int v19; // edx
  int v20; // edx
  int v21; // edx
  int v22; // edx
  int v23; // edx
  int v24; // edx
  int v25; // edx
  int v26; // edx
  __int64 v27; // rbx
  __int64 v28; // r8
  __int64 v29; // r8
  __int64 v30; // rdx
  __int64 v31; // r8
  __int64 v32; // rdx
  __int64 v33; // r8
  __int64 v34; // rdx
  __int64 v35; // r8
  __int64 v36; // rdx
  __int64 v37; // r8
  __int64 v38; // rdx
  __int64 v39; // r8
  __int64 v40; // rdx
  __int64 v41; // r8
  __int64 v42; // r8
  __int64 v43; // rdx
  __int64 v44; // r8
  __int64 v45; // r8
  __int64 v46; // rdx
  __int64 v47; // r8

  v2 = qword_140007880;
  sub_140002B80(qword_140007880, a2, 30, 470, 80, -16777216);
  sub_140002B80(v2, v3, 35, 470, 75, -16777216);
  sub_140002B80(v2, v4, 40, 470, 70, -16777216);
  sub_140002B80(v2, v5, 45, 470, 65, -16777216);
  sub_140002B80(v2, v6, 50, 470, 60, -16777216);
  sub_140002B80(v2, v7, 55, 470, 55, -16777216);
  sub_140002B80(v2, v8, 60, 470, 50, -16777216);
  sub_140002B80(v2, v9, 65, 470, 45, -16777216);
  sub_140002B80(v2, v10, 70, 470, 40, -16777216);
  sub_140002B80(v2, v11, 75, 470, 75, -16777216);
  sub_140002B80(v2, v12, 80, 400, 60, -16777216);
  sub_140002B80(v2, v13, 30, 470, 90, -16777216);
  sub_140002B80(v2, v14, 35, 470, 30, -16777216);
  sub_140002B80(v2, v15, 40, 470, 35, -16777216);
  sub_140002B80(v2, v16, 45, 470, 50, -16777216);
  sub_140002B80(v2, v17, 50, 470, 40, -16777216);
  sub_140002B80(v2, v18, 55, 400, 90, -16777216);
  sub_140002B80(v2, v19, 60, 470, 60, -16777216);
  sub_140002B80(v2, v20, 65, 470, 30, -16777216);
  sub_140002B80(v2, v21, 70, 470, 80, -16777216);
  sub_140002B80(v2, v22, 75, 470, 70, -16777216);
  sub_140002B80(v2, v23, 80, 470, 60, -16777216);
  sub_140002B80(v2, v24, 80, 470, 80, -16777216);
  sub_140002B80(v2, v25, 80, 470, 70, -16777216);
  sub_140002B80(v2, v26, 90, 470, 90, -16777216);
  v27 = qword_140007880;
  sub_1400017A0(qword_140007880, 40i64, v28, 4278190080i64);
  sub_140001C80(v27, 80i64, v29, 4278190080i64);
  sub_140002640(v27, v30, v31, 4278190080i64);
  sub_1400020F0(v27, v32, v33, 4278190080i64);
  sub_140002390(v27, v34, v35, 4278190080i64);
  sub_140001240(v27, v36, v37, 4278190080i64);
  sub_140001F20(v27, v38, v39, 4278190080i64);
  sub_140001560(v27, v40, v41, 4278190080i64);
  sub_140001C80(v27, 360i64, v42, 4278190080i64);
  sub_1400019D0(v27, v43, v44, 4278190080i64);
  sub_1400017A0(v27, 440i64, v45, 4278190080i64);
  sub_140002870(v27, v46, v47, 4278190080i64);
  return 0;
}

sub_140002C40 함수는 sub_140002B80와 여러 다른 함수들을 호출하며 각각에 대해 파라미터를 다르게 전달한다. 이 함수들이 어떤 작업 수행하는지 알아야 할 것 같다.

반복적으로 호출되는 sub_140002B80는 전달된 파라미터들(x, y 좌표, 크기, 색상)을 바탕으로 화면에 무엇인가를 그리는 작업을 수행하는 것 같다.

sub_140002B80를 분석해보자.

__int64 __fastcall sub_140002B80(__int64 a1, __int64 a2, unsigned int a3, int a4, int a5, unsigned int a6)
{
  int v9; // eax
  __int64 v10; // rsi
  int v11; // eax
  __int64 v12; // rbx
  int v13; // eax
  __int64 v15; // [rsp+30h] [rbp-38h] BYREF
  __int64 v16; // [rsp+38h] [rbp-30h]

  v16 = 0i64;
  v15 = 0i64;
  v9 = GdipCreatePen1(a6, a2, 0i64, &v15);  // 펜 생성
  v10 = *(_QWORD *)(a1 + 120);               // GDI+ 핸들 또는 그래픽 객체 가져오기
  LODWORD(v16) = v9;
  v11 = GdipDrawLineI(*(_QWORD *)v10, v15, 150i64, a3, a4, a5);  // 선 그리기
  if ( v11 )
    *(_DWORD *)(v10 + 8) = v11;  // 선 그리기 오류 처리
  v12 = *(_QWORD *)(a1 + 120);
  v13 = GdipSetSmoothingMode(*(_QWORD *)v12, 4i64);  // 부드러운 선 처리
  if ( v13 )
    *(_DWORD *)(v12 + 8) = v13;  // 오류 처리
  return GdipDeletePen(v15);  // 펜 삭제
}

sub_140002B80 호출 시 전달된 파라미터(x, y 좌표, 크기, 색상)에 따라 선을 그림
디버거로 확인해보니 플래그를 가리는 부분을 그림

플래그가 화면에 나타나는 부분을 선으로 가리고 있기 때문에 이 부분이 그려지지 않으면 플래그를 얻을 수 있다.
즉, sub_140002B80 함수가 실행되는 순간 바로 리턴되도록 ret 명령어 삽입하면 그 함수 내에서 선을 그리는 동작이 실행되지 않게 되므로 선이 그려지지 않아서 플래그가 가려지지 않게 된다.

  1. IDA View-A에서 g를 누른 후 140002B80 입력해 sub_140002B80 함수 찾기
  2. Edit → Patch program → Assemble
  3. Instruction을 ret로 수정
  4. 수정한 어셈블리 코드 적용하기 위해
    Edit → Patch program → Apply patches to input file... 후 OK
  5. F9 눌러 실행

    flag 획득

0개의 댓글