• [ CODEGATE 2020 Preliminary ] RS(702pt) wripte-up

    2020. 2. 9.

    by. ugonfor

    write-up을 쓰기가 정말 귀찮지만, 한번 써보죠..

    아.... 거의 다 썼는 데, 한번 날아가서 정말 쓰기 귀찮네요..

    바이너리

    먼저 제가 분석해둔 ida database 파일이랑, rs 문제 바이너리입니다.

    (처음에는 rs와 result 파일 두개 주어졌습니다.)

    rs
    0.28MB
    rs.i64
    4.13MB
    result
    0.00MB

    이렇게 나오네요. 

     

    rs의 문제 설명에 Rust 파일의 리버싱이라고 나와있었습니다. 그래서 rust가 무엇인지 찾아보았는 데, 프로그래밍 언어라고 합니다. IDA로 열어보았더니, C++ 형식으로 나와서 그렇게 중요하지는 않았습니다. 다만 터미널에 오류메세지 등이 다른 것들과는 다르고, backtrace를 보여주더군요!!

    그리고 IDA로 열었을 때, 함수이름이 좀 이상하고, 실질 바이너리 뿐이 아니라 여러가지 많이 붙어서 오는 게 달랐습니다. 

     

    그런데 오류 문구를 읽어보면, Cannot open flag 라는 메세지를 읽을 수 있습니다.

    그래서 flag 파일을 만들어서 한번 실행해봤습니다. 

     

    여러번 시도해본 끝에 알아낸 것은 16개의 글자마다 flag파일에서 받아와서, 32개의 헥사 값을 출력한다는 것이었습니다. 위 사진에도 보면 16개마다 글자를 똑같이 해줬는 데, 터미널에서 출력값도 보면 1a부터 두번째줄 5번째 9c까지가 4번 반복되는 것을 볼 수 있습니다. 

     

    그래서 아마 블럭암호같은 형식으로 16개씩 암호화를 진행하고 출력한다는 것을 알 수 있었습니다. 

     

    함수 분석

    이 바이너리는 가장 힘들었던 것이 함수가 너무나도 많다는 것이었습니다. 

    IDA로 열어보아도 함수안에 함수가 또 있고, 또 있어서 분석하기가 힘들었습니다. 

    그래서 저는 GDB로 함수단위로 실행시켜보면서, 우리가 원하는 수상한 hex값이 생성될때마다 그 함수로 들어가서 실행시켜보았습니다. (가장 돌아가는 방법을 사용한 거 같네요 ㅎㅎ...)

     

    그리고 IDA에 함수 네이밍을 많이해서 처음 열어보면 제가 설명하는 거랑 이름이 다른 부분이 꽤 있습니다. 위에 올린 rs.i64 파일 참고해주세용

     

    먼저, 메인함수부터 분석을 해보았습니다. 

    메인함수에서 호출하는 함수 두개를 따라가면, 위 사진 같은 부분을 볼 수 있는 데, 적당한 거리마다 브포를 걸어서 확인해보았는 데, _rust_maybe_catch_panic 부분에서 우리가 원하는 hex값이 생성되더군요.

     

    실행하면 출력되는 값을 알아서, find 1a 명령어를 통해서 문자열 검색을 해서 나타나는 지 확인했습니다.

     

    저 함수를 지나는 순간 문자열이 생기더군요ㅎㅎ. 

     

    함수를 ida로 조금 더 보다보면 알 수 있는 것이, &some이라고 표시한 주소. 그 주소는 또, 저 전체 함수의 매개변수로 받는 주소인데, 그 주소에서 저 hex값이 생성되는 것을 알 수 있습니다. 

     

    그게 실제 메인함수의 리턴함수의 첫번째 매개인자더군요.

    offset이 12190h인 곳을 보면

    위 사진 처럼 어셈블리를 볼 수 있는 데, 함수를 하나하나 ni 명령어로 한줄한줄 실행해봤습니다.

    그때, loc_121ec의 함수에서 제가 입력값으로 준 CODEGATE2020{aaa를 볼 수 있었습니다.

    그래서 저 함수에서 특정값이 생성되는 것을 알았고, 더 분석을 해봤죠.

     

    그 함수의 수도코드 인데요 ㅎㅎ...

    sub_9610에서 매개변수로 준 주소에 어떤 이상한 값이 생성되는 것을 보고, 그 값이 분명히 나중에 키 처럼 쓰일 것 같아서 제가 입력으로 준 값과 별개로 저 값도 계속 확인했습니다. 

    *** 마지막에 함수에 보면 key라고 명명해놨습니다.

     

    다른 함수들은 저는 많이 확인 해봤지만, 결국 확인해야하는 건 sub_12CE0이었습니다. 저 함수를 지나고 나니,

    RAX, RCX 레지스터에 마지막에 출력되는 요상한 값을 볼 수 있었습니다. 

    쓰다보니 생각난 건데, 저는 정말 근성만 가지고서 요령없이 분석을 해서 엄청 긴시간이 걸렸습니다...
    요령있고 리버싱 오래 하신 분은 저보다 잘 분석하셨을 거예요..
    그래서 롸업을 보면, 삽질을 엄청한 걸 볼 수 있어요..

    하여튼 그래서 그 함수를 또 IDA로 계속 까봤는 데, memcpy만 주구장창하고 함수를 계속 호출하길래 또 따라가봤습니다. 

    함수 호출을 두번정도 따라가면 제가 last라고 명명해둔 함수가 있는 데, 그것도 열어보면 다음 사진 처럼 나옵니다. 

    제가 지금 표시해둔 llast함수를 지나는 순간 또 그 요상한 값이 레지스터에 박혀나오더라구요.. 그래서 진짜 마지막함수다 싶어서 llast로 명명하고 또 분석해보는 데... (참고로 llllllllast까지 있어요..)

     

    ㅎㅎ... 계속 따라갑시다...

     

    lllast에서는 뭔가 함수가 있길래 무언 가 했더니, pcoga라는 곳에 값이 있는 지 확인을 하더라구요. 그게 아마 바이너리가 16개 블록단위로 암호화를 해서, 반복문이 있고, 한블럭이 끝날 때마다 검사를 하는 것 같습니다.

    그리고 llllast... 함수에서 codegate 텍스트 포인터가 매개로 들어가고 리턴값은 또 그 요상한 값이길래 또 따라갔습니다. 

     

    계쏙 따라가죠..

     

    또 따라 갑시다... 

    참고로, 저는 함수 열때마다 gdb로도 실행해보면서 매개변수값이 무엇이 들어가고, 반환값이 무엇인지 확인을 했습니다. last로 명명한 것들은 다 함수를 지나기전에는 ./rs 를 실행했을 때 반환하는 이상한 헥사 값이 없다가 함수를 지난 후에는 생기는 함수들입니다. 

     

    ㄱㄱ

    ㄱㄱㄱ  마지막임...!!

     

    last real을 열어서 봤더니!!!

    __int64 __fastcall lllllllast_97f0_real(__int64 pcoga, __int64 pkey, __int64 coga, __int64 a4)
    {
      __int64 v4; // rdx
      __int64 v5; // rdx
      __int64 pcoga_1; // rax
      __int64 v7; // rdx
      __int64 v8; // rdx
      __int64 v9; // rdx
      __int64 pcoga_2; // rax
      __int64 v11; // rdx
      int v12; // edx
      int v13; // ecx
      int v14; // er8
      int v15; // er9
      char r2; // [rsp+26h] [rbp-1C2h]
      char r1; // [rsp+27h] [rbp-1C1h]
      unsigned __int8 *coga_until16_1; // [rsp+28h] [rbp-1C0h]
      unsigned __int8 _key; // [rsp+37h] [rbp-1B1h]
      char ogat; // [rsp+47h] [rbp-1A1h]
      __int64 v22; // [rsp+50h] [rbp-198h]
      _BYTE *v23; // [rsp+90h] [rbp-158h]
      __int64 _0_1; // [rsp+98h] [rbp-150h]
      unsigned __int8 n_10h; // [rsp+E7h] [rbp-101h]
      struct _Unwind_Exception coga_until16; // [rsp+120h] [rbp-C8h]
      __int64 v27; // [rsp+140h] [rbp-A8h]
      __int64 v28; // [rsp+148h] [rbp-A0h]
      int v29[2]; // [rsp+150h] [rbp-98h]
      __int64 v30; // [rsp+158h] [rbp-90h]
      int v31[2]; // [rsp+160h] [rbp-88h]
      int _0[2]; // [rsp+168h] [rbp-80h]
      __int64 v33; // [rsp+170h] [rbp-78h]
      __int64 v34; // [rsp+178h] [rbp-70h]
      int v35[2]; // [rsp+180h] [rbp-68h]
      __int64 v36; // [rsp+188h] [rbp-60h]
      int v37[2]; // [rsp+190h] [rbp-58h]
      __int64 count; // [rsp+198h] [rbp-50h]
      __int64 _3[3]; // [rsp+1A0h] [rbp-48h]
      __int64 ppcoga[4]; // [rsp+1B8h] [rbp-30h]
    
      ret_a1(coga, a4);
      ret_a1_5();
      sub_C170(&coga_until16);                      // ret값이 16개로 자른 앞부분
      n_10h = ret_0(0);
      sub_5570(&coga_until16, 0x30uLL, n_10h);
      v27 = 0LL;
      v28 = 16LL;
      *v29 = 0_0(0LL);
      v30 = v4;
      while ( 1 )
      {
        *v31 = sub_ACA0(v29);
        *_0 = v5;
        if ( !*v31 )
          break;
        if ( *v31 != 1LL )
          BUG();
        _0_1 = *_0;
        v23 = sum_pa1_a2_f90(&coga_until16, *_0);
        if ( ~sub_A980(v23) )
        {
          v33 = 0LL;
          v34 = 32LL;
          *v35 = 0_0(0LL);
          v36 = v7;
          while ( 1 )                               // 16번 돌림
          {
            *v37 = sub_ACA0(v35);                   // 0또는 1반
            count = v8;                             // rdx 들어
            if ( !*v37 )
              break;
            if ( *v37 != 1LL )
              BUG();
            v22 = count;
            ogat = *sum_pa1_a2_f90(&coga_until16, _0_1 + count + 1);
            _key = *sum_pa1_a2_250(pkey, v22 + 1);
            coga_until16_1 = sum_pa1_a2_f90(&coga_until16, _0_1);
            r1 = xor1_ab0(_key, *coga_until16_1);
            r2 = xor_0_a90(ogat, r1);
            *sub_7370(&coga_until16, _0_1 + v22 + 1) = r2;
          }
        }
      }
      pcoga_1 = ret_pa1(&coga_until16);
      pcoga_2 = sub_A7E0(pcoga_1, v9);
      sub_CAC0(ppcoga, pcoga_2, v11, 16LL);
      sub_C130(_3, ppcoga);
      llllllllast_C190___(pcoga, _3);               // (p_1,ppcoga)
                                                    // 이미 값이 만들어져 있더라...
      sub_11AD0(&coga_until16, _3, v12, v13, v14, v15);
      return pcoga;
    }

    위처럼 수도코드를 볼 수 있었습니다. 

    이 함수는 3개의 갈래로 나눌 수 있는 데, 

    1. While문 전

    2. While문

    3. While문 후

    입니다. 

     

    1. While문 전

    반복문 전에는 입력값들을 16개 단위로 자르고, 암호화를 하기위해 준비를 하는 과정이었습니다

    그래서 별로 대수롭지 않게 여기고 넘어갔습니다.

     

    3. While문 후

    반복문 후에도 별로 중요한 것은 없었습니다. 반복문 동안 우리가 계속 수상하게 여기던 출력값이 생성되는 것을 확인할 수 있었고, 반복문 후에는 아마 여러가지 포인터를 정리하고 출력을 하기위한 준비과정 이었습니다. 

     

    그래서 

    2. While문

    이 부분이 제일 중요한 부분이었는 데, 그중에서도 

          while ( 1 )                               // 16번 돌림
          {
            *v37 = sub_ACA0(v35);                   // 0또는 1반
            count = v8;                             // rdx 들어
            if ( !*v37 )
              break;
            if ( *v37 != 1LL )
              BUG();
            v22 = count;
            ogat = *sum_pa1_a2_f90(&coga_until16, _0_1 + count + 1);
            _key = *sum_pa1_a2_250(pkey, v22 + 1);
            coga_until16_1 = sum_pa1_a2_f90(&coga_until16, _0_1);
            r1 = xor1_ab0(_key, *coga_until16_1);
            r2 = xor_0_a90(ogat, r1);
            *sub_7370(&coga_until16, _0_1 + v22 + 1) = r2;
          }

    이 부분이 제일 중요했습니다.

     

    코드펜스는 가독성이 떨어지는 거 같아서 다시 스샷으로 하겠슴.

     함수들이 다 다 열어보면 별거 없고 단순 연산하는 함수들이었습니다. 

     sum_pa1_a2_f90 함수는 CODEGATE2020{aaa 텍스트의 주소를 받아서, 1 더해져서 ODEGATE2020{aaa 의 포인터를 반환했습니다. 그래서 명명은 (c)o(de)gat(e)으로 해놓았습니다.

     

     sum_pa1_a2_250 함수의 경우에는 매개변수로 아까 언급했던 키처럼 사용될거라는 텍스트가 있는 포인터를 받아서 반환값으로는 char(키값 1바이트)를 반환합니다.

     

    또, sum_pa1_a2_f90의 경우에는 CODEGATE2020{aaa의 포인터를 받아서 맨 앞의 "C" 값(char)을 반환했습니다. 

     

    그리고 아래에 xor이라고 명명한 함수 2개의 경우에, xor1함수의 경우에는 키 값을 이용해서 특정 알고리즘이 있었고, xor0 함수의 경우에는 정말 단순 xor이었습니다.

     

    그래서 이 부분에 대한 역연산을 파이썬 코드로 짜서 우리가 result로 받은 코드에 대해서 실행시키면 되는 것이죠.

     

    파이썬 코드

    def xor1_inv(r1,keyi):
        if(r1 == 0):
            return 0
        v4 = ord(keyi)
        x = []
        for i in range(8):
            x.append(v4)
            v4 = v4 * 2
            if (v4 >= 0x100):
                v4 ^= 0x11d
        for i in range(1,256):
            k = i
            temp = 0
            j = 0
            length = i.bit_length()
            while(k):
                
                if(k&1) ==1:
                    temp ^=x[j]
                j+=1
                k>>=1
            if(r1 == temp):
                return (i)
    
    def xor1(keyi,flagj):
        r1 = 0
        v4 = ord(keyi)
        
        i = (flagj)
        while i:
            if (i&1) == 1:                
                r1 ^= v4
            v4 *=2
            
            if v4 >= 0x100:
                v4 ^= 0x11d         
            
            i >>= 1
        return r1
    
    result1 = [0xef ,0x43 ,0x4b ,0x3f ,0x5e ,0xb9 ,0xf0 ,0xd0 ,0x8c ,0xb5 ,0x7e ,0x6f ,0x7b ,0xc8 ,0xa6 ,0x7b ,0x09 ,0xe2 ,0x61 ,0x9d ,0x98 ,0x03 ,0x5f ,0x56 ,0x5d ,0x66 ,0x82 ,0x0b ,0x9e ,0x2b ,0x76 ,0x92]
    result2 = [0x5b ,0xc3 ,0xdc ,0xf2 ,0x3c ,0xd0 ,0xb6 ,0x81 ,0x60 ,0x34 ,0xa5 ,0x66 ,0xca ,0xbd ,0x7d ,0x6a ,0x00 ,0xfe ,0xe4 ,0x0b ,0x44 ,0xe1 ,0xba ,0x81 ,0xcb ,0xae ,0x8b ,0x24 ,0x0b ,0xa5 ,0x1f ,0x6d]
    result3 = [0xba ,0x0e ,0x61 ,0x1a ,0x30 ,0xa7 ,0x77 ,0x51 ,0x23 ,0x41 ,0xa6 ,0x1a ,0xc0 ,0x7f ,0x71 ,0x71 ,0x9f ,0xd5 ,0x93 ,0xe5 ,0x38 ,0xce ,0x52 ,0x8b ,0x25 ,0x86 ,0xb3 ,0x12 ,0xb7 ,0xa7 ,0x1c ,0x43]
    result4 = [0xb4 ,0x08 ,0x81 ,0x47 ,0xae ,0xd6 ,0x18 ,0x46 ,0xc5 ,0x6b ,0x69 ,0x63 ,0x0b ,0xcc ,0x95 ,0xab ,0x49 ,0x53 ,0x6f ,0xde ,0xbe ,0x2f ,0x2e ,0xd9 ,0x9b ,0xdc ,0xdd ,0x76 ,0x69 ,0xa4 ,0xf0 ,0x58]
    key = "\x74\x40\x34\xae\x36\x7e\x10\xc2\xa2\x21\x21\x9d\xb0\xc5\xe1\x0c"
    key += "\x3b\x37\xfd\xe4\x94\x2f\xb3\xb9\x18\x8a\xfd\x14\x8e\x37\xac\x58"
    
    #main
    def main(input):
        flag = input
        key = "\x74\x40\x34\xae\x36\x7e\x10\xc2\xa2\x21\x21\x9d\xb0\xc5\xe1\x0c\x3b\x37\xfd\xe4\x94\x2f\xb3\xb9\x18\x8a\xfd\x14\x8e\x37\xac\x58"
        temp = [0]*48
        tmp = [0] * 32
        for i in range(len(flag)):
            temp[i] = ord(flag[i])
        flag = temp
    
        for j in range(16):
            for i in range(32):
                flag[i+j+1] = flag[(i+j+1)] ^ xor1(key[i],flag[j])
                tmp[i] = flag[i+j+1]
    
        print(tmp)
    
    main("CODEGATE2020{aaa")
    
    def inv(result):
        #inverse
        result = [0]*16 + result
        flag = ''
    
        for i in range(15,-1,-1):
            result[i] = xor1_inv(result[i+32],key[31])
            for j in range(len(key)-1):
                result[i+j+1] ^= xor1(key[j],result[i])
    
        for i in range(16):
            flag += chr(result[i])
        print(flag)
    
    inv(result1)
    inv(result2)
    inv(result3)
    inv(result4)

    파이썬 코드는 다음 위와 같습니다.

    먼저, xor함수를 그대로 파이썬으로 가져왔고, xor 함수에 대한 역연산도 만들었습니다. 

    main함수의 경우에는 바이너리에서 출력값을 만드는 과정을 코드로 짜본 것이고, inv함수는 그것의 역연산과정을 한 것입니다.

     

    main함수를 지금까지 우리가 봐왔던 수상한 헥사값을 잘 출력하는 지 한번 실행시켜보았습니다. 

    그리고 result값에 대해서 inv함수를 실행시켰더니

    우리가 원하던 flag가 나오더군요 ㅎㅎ

     

    inv함수를 4개로  나눠서 한 이유는 16바이트마다 flag파일을 잘라서 블록형식으로 암호화를 했고, 또 32 바이트로 출력해서, result값을 32바이트마다 나눠서 inv함수를 돌렸습니다.

     

    이건 파이썬 파일

    RS_sol.py
    0.00MB

     

     

    롸업 정말 기네요..

    멀고도 멀은듯.. 아직..

    'Writeup > CTF_Writeup' 카테고리의 다른 글

    [ Defenit CTF 2020 ] Lord fool song remix  (0) 2020.06.08
    [ Defenit CTF 2020 ] momsTouch  (0) 2020.06.08
    [ RCTF 2020 ] rust-flag  (0) 2020.06.02
    [Insomni'hack teaser] Kaboom writeup  (0) 2020.01.21
    [Insomni'hack teaser] Welcome  (0) 2020.01.21

    댓글