2개의 exe 파일들이 주어진다.

    하나는 Original.exe, 다른 하나는 Packed.exe.

    Original.exe

    패킹되어있지 않은 바이너리다.

    Decompiled-src

    WinMain

    int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
    {
      int v4; // eax
      char v5; // cl
      char v7[16]; // [esp+0h] [ebp-14Ch]
      char v8[16]; // [esp+10h] [ebp-13Ch]
      CHAR Text[32]; // [esp+20h] [ebp-12Ch] BYREF
    
      strcpy(Text, "Congratulation!\r\n\r\nPassword is ");
      v7[0] = 0x2F;
      v7[2] = 0x2F;
      v4 = 0;
      v8[0] = 0x10;
      v8[1] = 0x20;
      v8[2] = 0x30;
      v8[3] = 0x40;
      v8[4] = 0x50;
      v8[5] = 0x60;
      v8[6] = 0x70;
      v8[7] = 0x80;
      v8[8] = 0x90;
      v8[9] = 0xA0;
      v8[10] = 0xB0;
      v8[11] = 0xC0;
      v8[12] = 0xD0;
      v7[1] = 0x1F;
      v7[3] = 0x7F;
      v7[4] = 0x6F;
      v7[5] = 0x5F;
      v7[6] = 0x4F;
      v7[7] = 0xBF;
      v7[8] = 0xAF;
      v7[9] = 0x9F;
      v7[10] = 0x8F;
      v7[11] = 0xFF;
      v7[12] = 0xD0;
      do
      {
        v5 = v8[v4] ^ v7[v4];
        v7[v4] = v5;
        Text[v4++ + 31] = v5;
      }
      while ( v4 < 13 );
      MessageBoxA(0, Text, Caption, 0x40u);
      return 0;
    }

    XOR 복호화된 Text를 MessageBoxA 함수를 통해 호출하고 있다.

    0x4010D1에 브레이크포인트를 걸어 확인해보면 실제로 복호화된 문자열을 확인할 수 있다.

    그게 끝이다.

    Packed.exe

    패킹된 바이너리이다.

    그러면 비밀번호를 입력하라는 창이 나오는데, ABCD 문자열을 넣고 분석해보자.

    Analysis

    sub_4091D8 함수에 의해 반환된 eax 값이 0x0E98F842A인지 확인한다.

    sub_4091D8 함수의 작동 방식을 살펴보면,

    import sys
    
    def ROR(data, shift, size=32):
        shift %= size
        body = data >> shift
        remains = (data << (size - shift)) - (body << size)
        return (body + remains)
    
    def ROL(data, shift, size=32):
        shift %= size
        remains = data >> (size - shift)
        body = (data << shift) - (remains << size )
        return (body + remains)
    
    def ah_to_eax(eax, ah):
        eax = hex(eax).replace('0x', '')
        eax = eax.rjust(8, "0")
        eax = "0x" + eax
        eax = list(eax)
        if(ah > 0xf):
            eax[6] = hex(ah)[-2]
        else:
            eax[6] = "0"
        eax[7] = hex(ah)[-1] 
        eax = ''.join(eax)
    
        return int(eax, 16)
    
    def al_to_eax(eax, al):
        eax = hex(eax).replace('0x', '')
        eax = eax.rjust(8, "0")
        eax = "0x" + eax
        eax = list(eax)
        if(al > 0xf):
            eax[8] = hex(al)[-2]
        else:
            eax[8] = "0"
        eax[9] = hex(al)[-1] 
        eax = ''.join(eax)
    
        return int(eax, 16)
    
    def sub_4091D8(esi):
        eax = 0 #004091D8 xor eax, eax
        ah = 0
        for i in range(len(esi)):
            ah = ah ^ esi[i]    #004091E1: xor ah, [esi]; ah: 41(0x00 ^ 0x41), B9(0xFB ^ 0x42), 87(0xC4 ^ 0x43), B2(0xF6 ^ 0x44), 41(0x41 ^ 0x00)
            eax = ah_to_eax(eax, ah)
            for edx in range(0x10000, 0, -1): #control dx register
                dl = (edx & 0xffff) & 0xff
                al = eax & 0xff
                al = al ^ dl    #004091E3: xor al, dl
                eax = al_to_eax(eax, al)
                eax = (eax + 0x434F4445) & 0xffffffff   #004091E5: add eax, 434F4445h
                al = eax & 0xff
                cl = al #004091EA: mov cl, al
                eax = ROR(eax, cl)  #004091EC: ror eax, cl
                eax = eax ^ 0x55AA5A5A  #004091EE: xor eax, 55AA5A5Ah
                ah = (eax & 0xffff) >> 8
        return eax
            
    input_esi = b"ABCD" + b"\x00"
    val = sub_4091D8(input_esi)
    print(f"sub_4091D8 ret: {hex(val)}")    #sub_4091D8 ret: 0xe4c86270

    위와 같이 패스워드를 “ABCD”로 입력했을때
    sub_4091D8 함수에 대한 연산을 파이썬3 코드로 구현시킬 수 있다.

    반환된 eax 값이 0x0E98F842A되게끔 역산을 구현하기에,
    즉 password 길이 제한도 없고 값을 알아내기는 어렵기 때문에
    우선은 0x0040919C 주소 지점에 브레이크포인트를 걸고

    eax 값을 0x0E98F842A로 수정하여 cmp 검사를 통과하자.

    sub_4091D8 함수에서 검사를 통과하고나면, 0x4090A8 주소로 복귀한다.

    그리고 sub_409200 함수를 호출하는 것을 볼 수 있는데,

    sub_4091DA 함수는 이전에 봤다시피 xor eax, eax를 제외하고는
    sub_4091D8 함수의 작동방식이 같아 역산이 불가능하기에 넘어가도록 하고,
    loc_40921F 부터 자세히 살펴보자.

    0x401000 주소에서 4바이트씩 0x1000번 반복하여 opcode를 복호화하고 있는 것을 확인할 수 있다.

    특히 0040921F xor [edi], eax에서
    처음에 edi는 0x401000 값을 가지고 있고, 4바이트씩 복호화하는 것을 알 수 있는데,

    Original.exe 파일의 0x401000 주소부터 4바이트씩 참고해서 xor 연산을 통해 eax 값을 구할 수 있다.

    Original.exe 파일의
    0x401000 주소에 있는 4바이트값은 0x014CEC81,
    0x401004 주소에 있는 4바이트값은 0x57560000,
    0x401008 주소에 있는 4바이트값은 0x000008B9이다.

    Packed.exe 파일의
    0x401000 주소에 있는 4바이트값은 0xB6E62E17,
    0x401004 주소에 있는 4바이트값은 0x0D0C7E05,
    0x401008 주소에 있는 4바이트값은 0x99C5159E이다.

    따라서 위와 같이 xor 연산을 다시하면, 복호화하는데 필요한 올바른 eax값을 구할 수 있다.
    edi가 0x401000일때는 eax가 0xb7aac296이여야 한다.
    edi가 0x401004일때는 eax가 0x5a5a7e05이여야 한다.
    edi가 0x401008일때는 eax가 0x99c51d27이여야 한다.

    ebx값으로 rol, xor, ror, add 연산과 함께 새로운 eax값을 매번 만드므로,
    위 3가지의 eax값을 참고해서 ebx값을 구할 것이다.
    ebx 값은 4바이트 크기이므로, 0x0~0xffffffff까지 브루트포싱을 통해 추측해서 때려맞추면 된다.

    파이썬3 코드로 작성하면 굉장히 오래걸리기 때문에
    C언어 작성이 요구되었다.

    #include <stdio.h>
    #include <stdint.h>
    
    int32_t rotl32 (int32_t x /*value*/, unsigned int y /*rotate*/)
    {
        __asm__ ("roll %1, %0" : "+g" (x) : "cI" ((unsigned char)y));
        return x;
    }
    
    int32_t rotr32 (int32_t x /*value*/, unsigned int y /*rotate*/)
    {
        __asm__ ("rorl %1, %0" : "+g" (x) : "cI" ((unsigned char)y));
        return x;
    }
    
    int32_t eax_to_al(int32_t eax) {
        return (eax & 0xff);
    }
    
    int32_t ebx_to_bh(int32_t ebx) {
        return ((ebx & 0xffff) >> 8);
    }
    
    int main(void) {
        for(int ebx = 0; ebx < 0xffffffff; ebx++) {
            int eax = 0xb7aac296;
            int al = eax_to_al(eax);
            int orig_ebx = ebx;
            ebx = rotl32(ebx, al);
            eax = eax ^ ebx;
            int bh = ebx_to_bh(ebx);
            eax = rotr32(eax, bh);
    
            if(eax == 0x5a5a7e05) {
                printf("orig_ebx candidate? 0x%x\n", orig_ebx);
                ebx = (ebx + eax) & 0xffffffff;
                al = eax_to_al(eax);
                ebx = rotl32(ebx, al);
                eax = eax ^ ebx;
                bh = ebx_to_bh(ebx);
                eax = rotr32(eax, bh);
                if(eax == 0x99c51d27)
                    printf("orig_ebx confirmed! 0x%x\n", orig_ebx);
            }
    
            ebx = orig_ebx;
        }
    
        return 0;
    }
    seo@seo:~/Documents$ gcc -o solve_pepassword solve_pepassword.c  && ./solve_pepassword
    orig_ebx candidate? 0xa1beee22
    orig_ebx candidate? 0xc263a2cb
    orig_ebx confirmed! 0xc263a2cb
    seo@seo:~/Documents$

    약 3분~5분 정도 걸렸던 것 같다.

    이제 loc_40921F 초기에
    ebx값은 0xc263a2cb, eax값은 0xb7aac296 값으로 변경해주면 복호화가 제대로 진행될 것이다.

    0040921F 지점에 브레이크포인트를 걸고, 걸렸을때 ebx, eax 값을 변경해주자.

    복호화 진행이 끝나고 스텝을 하나씩 밟다보면,

    OEP는 0x409151으로, 0x004010F0 주소로 점프한다.

    sub_4010F0

    int __usercall sub_4010F0@<eax>(int a1@<ebx>, int a2@<edi>, int a3@<esi>)
    {
      unsigned int v3; // eax
      int v4; // eax
      int v5; // eax
      int v7; // [esp-18h] [ebp-80h]
      int v8; // [esp-14h] [ebp-7Ch]
      int v9; // [esp-Ch] [ebp-74h] BYREF
      int v10; // [esp+4h] [ebp-64h]
      int v11; // [esp+8h] [ebp-60h]
      char v12[44]; // [esp+Ch] [ebp-5Ch] BYREF
      int v13; // [esp+38h] [ebp-30h]
      unsigned __int16 v14; // [esp+3Ch] [ebp-2Ch]
      int *v15; // [esp+50h] [ebp-18h]
      _DWORD **v16; // [esp+54h] [ebp-14h]
      struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList; // [esp+58h] [ebp-10h]
      int v18; // [esp+5Ch] [ebp-Ch]
      void *v19; // [esp+60h] [ebp-8h]
      int v20; // [esp+64h] [ebp-4h]
    
      v20 = -1;
      v19 = &unk_4050A8;
      v18 = 4201916;
      ExceptionList = NtCurrentTeb()->NtTib.ExceptionList;
      v15 = &v9;
      v3 = off_405014(a2, a3, a1);
      dword_408508 = BYTE1(v3);
      dword_408504 = (unsigned __int8)v3;
      dword_408500 = BYTE1(v3) + ((unsigned __int8)v3 << 8);
      dword_4084FC = HIWORD(v3);
      if ( !sub_401C65(0) )
        sub_40120B(28);
      v20 = 0;
      sub_401945();
      dword_4089F8 = off_405010();
      dword_4084E4 = sub_401813();
      sub_4015C6();
      sub_40150D();
      sub_40122F();
      v13 = 0;
      off_40500C(v12);
      v10 = sub_4014B5();
      if ( (v13 & 1) != 0 )
        v4 = v14;
      else
        v4 = 10;
      v8 = v4;
      v7 = v10;
      v5 = off_405008(0);
      v11 = sub_401000(v5, 0, v7, v8);
      sub_40125C(v11);
      return sub_401331(**v16, v16);
    }

    main 함수로 추측되는 sub_0x401000를 호출하는 것을 확인할 수 있다.

    sub_401000

    int __stdcall sub_401000(int a1, int a2, int a3, int a4)
    {
      int v4; // eax
      char v5; // cl
      char v7[16]; // [esp+0h] [ebp-14Ch] BYREF
      char v8[16]; // [esp+10h] [ebp-13Ch]
      char v9[32]; // [esp+20h] [ebp-12Ch] BYREF
    
      strcpy(v9, "Congratulation!\r\n\r\nPassword is ");
      qmemcpy(v7, "VR_-", 4);
      v4 = 0;
      v8[0] = 16;
      v8[1] = 32;
      v8[2] = 48;
      v8[3] = 64;
      v8[4] = 80;
      v8[5] = 96;
      v8[6] = 112;
      v8[7] = 0x80;
      v8[8] = -112;
      v8[9] = -96;
      v8[10] = -80;
      v8[11] = -64;
      v8[12] = -48;
      v7[4] = 15;
      v7[5] = 39;
      v7[6] = 56;
      v7[7] = -52;
      v7[8] = -94;
      v7[9] = -1;
      v7[10] = -111;
      v7[11] = -31;
      v7[12] = -48;
      do
      {
        v5 = v8[v4] ^ v7[v4];
        v7[v4] = v5;
        v9[v4++ + 31] = v5;
      }
      while ( v4 < 13 );
      off_40509C(0, v9, &unk_4084E0, 64);
      return 0;
    }

    제대로 복호화된 것으로 보인다!
    저 off_40590C는 user32_MessageBoxA 함수로,
    Congratulation! 문구와 함께 패스워드를 메시지창 띄울 것이다.

    FLAG

    From_GHL2_!!

    답글 남기기

    이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다