Reversing CTF

k4bunny·2025년 7월 21일

Layer7

목록 보기
12/13

Layer7, Thief the monariza

문제 파일을 다운로드 한 후, 바이너리를 IDA로 열어서 디컴파일된 main 함수부터 살펴보았다.

입력받은 문자열 v4를 인자로 갖는 함수 checkPassword 에서 참이 반환되면 realllllll.jpg 라는 파일을 서버에서 내려받는 시스템이다.

함수 checkPassword를 확인해보면 다음과 같은 입력값을 검증하는 코드를 확인할 수 있다.
검증은 비교적 간단하다.
v4[v3[i]]와 0x55 XOR 연산한다.
이 때 이 값이 입력받은 문자열의 인덱스값과 다르면 즉시 0을 반환한다.
모든 키 값이 동일할 경우, 마지막 문자열이 0 이면 '참' 아니면 '거짓'을 반환한다.

#include <stdio.h>

int main() {
    unsigned long long v4[3] = {0xB21E9F215807A934, 0x934F18E7D50CC430, 0xAC55FEDA3B81672A};
    unsigned char *bytes = (unsigned char *)v4;

    char key[7];

    int v3[6] = {0, 2, 4, 6, 8, 10};

    for (int i = 0; i < 6; i++) {
        key[i] = bytes[v3[i]] ^ 0x55;
    }
    key[7] = 0;

    printf("%s", key);

    return 0;
}

다음의 코드로 Key 값을 찾은 결과
정답 키값은 aRtKeY였다.
해당 코드를 gdb로 실행시킨 후, aRtKeY를 입력하여 realllllll.jpg 파일을 다운로드 받았다.

하지만 어째서인지 jpg 파일이 깨져있었고 HxD를 이용하여 Hex 데이터를 분석하여 jpg 파일 헤더에 이상이 있는지 확인해보았다.

그 결과 FF D8이 되어야하는 부분이 FF DB로 되어있는 것을 확인하고 고쳤다.
그랬더니 정상적으로 jpg 파일이 열리며 파일에 담긴 플래그를 찾을 수 있었다.

사진은 조금 흉측한 관계로 첨부하지 않겠다..

YEKROX

마찬가지로 문제 파일을 다운 받은 후, IDA로 디컴파일하여 main함수를 보았다.

var0라는 문자열에 암호화된 확인 문자열이 들어가는 것을 확인할 수 있었다.
암호화는 간단하게 인덱스값과 66을 더한 후, v3와 XOR 연산을 실시한다.
v3의 값은 byte_402040에 저장된 값이다.
이 문제는 입력받은 값을 암호화하여 키 값과 비교하는 것이 아니므로 역연산이 필요없다.
따라서 byte_402040에 저장된 값을 확인한 후, 그대로 코드를 짜주었다.

#include <stdio.h>

int main() {
    unsigned char byte_402040[36] = {
    0x0E, 0x22, 0x3D, 0x20, 0x34, 0x70, 0x33, 0x20, 0x2D, 0x24, 0x13,
    0x20, 0x21, 0x3B, 0x20, 0x3E, 0x3D, 0x3F, 0x39, 0x2C, 0x33, 0x38,
    0x36, 0x06, 0x3D, 0x3A, 0x25, 0x28, 0x30, 0x00, 0x15, 0x0F, 0x06,
    0x06, 0x16, 0x18
    };


    unsigned char vars0[37] = {0};
    
    int v3 = 14;

    for (int i = 0; i < 36; i++) {
        v3 = byte_402040[i];
        vars0[i] = v3 ^ (i + 66);   
    }

    vars0[36] = 0;

    printf("%s", vars0);

    return 0;
}

플래그는 정상적으로 출력되었다.

XORING

문제파일을 IDA로 실행시킨 후, main함수를 보았다.

s2에 xmmword_2060에 값을 넣고 입력받은 문자열과 v6의 주소값을 인자로 사용하여 sub_1350을 실행한다.

이 함수를 통해 입력받은 값 31바이트의 문자열을 XOR 암호화한 후, 아까 main 함수에서 memcmp를 통해 키값과 비교하여 일치하는지를 검사한다.
하지만 여기서 이상한 점을 발견할 수 있는데 단순히 xmmword_2060에 저장된 값은 16바이트 밖에 안된다. 따라서 15바이트를 또 찾아내야한다.

또한 xmmword_2060의 저장된 값 역시 그대로 참조하는 것이 아닌, 리틀엔디안 방식
즉, 뒤에서부터 1바이트씩 가져와야한다.
이제 나머지 15바이트를 찾아보자.
본인은 도저히 찾기가 어려워서 디컴파일을 해제하고 다시 어셈블리를 확인하여 분석을 이어갔다.
그러다가 main함수에 위쪽에서 특이한 것을 발견했다.

살짝 해석해보면
var_78은 8바이트, var_70은 4바이트, var_6C는 2바이트, var_6A는 1바이트를 갖는 스택 프레임의 변수 레이아웃이였다.
s2는 우리가 아까봤던 xmmword 인것을 보니 이것을 모두 더한 31바이트가 키값이 되는 것을 확신하고 각각의 변수에 저장된 값을 확인하여 아까봤던 XOR 연산의 역산 코드를 작성하였다.


int main() {
    uint8_t enc[31] = {
        0x06, 0x22, 0x2F, 0xD0, 0x20, 0x44, 0x39, 0x30,
        0x2B, 0x67, 0x63, 0x64, 0x12, 0x65, 0x73, 0x1A,
        0x72, 0x72, 0x58, 0xA0, 0x87, 0xAB, 0x5F, 0x85,
        0x81, 0x97, 0x8D, 0xEC,
        0xEC, 0xEC,
        0xEF
    };

    char flag[32];
    int v2 = 3;

    for (int i = 0; i < 31; ++i) {
        int temp = (enc[i] ^ 0x5A) - 13;
        int ch = temp ^ v2;
        v2 = (v2 + 7) & 0xFF;
        flag[i] = (char)ch;
    }

    flag[31] = '\0';

    printf("%s", flag);
    return 0;
}

다음 코드를 실행 시키면 플래그가 출력된다.

How can i live without u

IDA를 이용해 디컴파일 한 후, main함수를 확인해보았다.

__int64 __fastcall main(int a1, char **a2, char **a3)
{
 int v3; // eax
 int v4; // edx
 int i; // eax
 char v6; // cl
 __int64 v7; // rdx
 size_t v8; // rax
 char *v9; // rdx
 char v10; // al
 __int64 j; // rax
 char *v12; // r8
 char *v13; // rdx
 int v14; // eax
 char v15; // cl
 char *v16; // rax
 int v17; // ecx
 int v18; // edx
 int v19; // esi
 __int64 k; // rsi
 int v21; // edx
 char v22; // al
 char v23; // al
 unsigned int v24; // r12d
 int v26; // [rsp+Ch] [rbp-1FCh]
 __int128 v27; // [rsp+10h] [rbp-1F8h] BYREF
 _BYTE v28[64]; // [rsp+20h] [rbp-1E8h] BYREF
 char s1[144]; // [rsp+60h] [rbp-1A8h] BYREF
 char s[280]; // [rsp+F0h] [rbp-118h] BYREF

 v3 = 0;
 v26 = 305419896;
 do
 {
   v4 = v3 ^ v26;
   v3 -= 1640531527;
   v26 = v4;
 }
 while ( v3 != -844395452 );
 for ( i = 0; i != 64; ++i )
 {
   v6 = i;
   v7 = i;
   v28[v7] = v6 ^ 0x5A;
 }
 __printf_chk(1, "Enter the flag: ");
 fflush(stdout);
 if ( fgets(s, 256, stdin) )
 {
   v8 = strcspn(s, "\n");
   v9 = (char *)&v27;
   s[v8] = 0;
   v10 = 0;
   v27 = 0;
   while ( 1 )
   {
     *v9++ = __ROL1__(~v10, 1) + 51;
     if ( v28 == v9 )
       break;
     v10 = *v9;
   }
   if ( strlen(s) != 144 )
     goto LABEL_23;
   for ( j = 0; j != 144; ++j )
     s1[j] = s[j];
   v12 = s1;
   v13 = s1;
   v14 = 0;
   do
   {
     v15 = v14;
     v14 += 23;
     *v13++ ^= v15 ^ 0x42;
   }
   while ( (_BYTE)v14 != 0xF0 );
   v16 = s1;
   v17 = 55;
   v18 = 19;
   do
   {
     *v16 += v18;
     v19 = v17;
     ++v16;
     v17 += v18;
     v18 = v19;
   }
   while ( s != v16 );
   for ( k = 0; k != 144; ++k )
     s1[k] = __ROL1__(s1[k], k % 7 + 1);
   v21 = 0;
   do
   {
     v22 = *v12++;
     v23 = v21 ^ v22;
     v21 += 3;
     *(v12 - 1) = v23 ^ 0xAA;
   }
   while ( s != v12 );
   v24 = memcmp(s1, &unk_402040, 0x90u);
   if ( v24 )
   {
LABEL_23:
     v24 = 1;
     __printf_chk(1, "Wrong!\n");
   }
   else
   {
     __printf_chk(1, "Yes\n");
   }
 }
 else
 {
   v24 = 1;
   __printf_chk(1, "Error reading input!\n");
 }
 return v24;
}

뭔가 복잡해보이지만 하나씩 풀어보면 과제를 하면서 한 번쯤 해봤던 암호화들이 여러 개가 엮여서 이루어진 것이였다.

하나씩 확인해보면
XOR 연산을 통해 입력받은 문자열을 바이트마다 다른 키로 암호화를 시도한다.
그 후, 1차 암호화 된 값을 복잡한 형태로 덧셈 연산하는데 이를 피보나치 수열을 변형하여 암호화 한 것이라고 한다.
다음으로 이제는 너무나도 익숙한 비트 회전 ROL을 실행한다.
마지막으로 한 번 더 XOR 연산을 실시 한 후 최종적으로 비교를 하여 일치하면 참을 출력한다.
XOR 연산은 결과에 다시 XOR 연산을 가하면 원래 값이되는 것을 이용한다.
비트 회전은 다시 반대로 회전시키면 쉽게 원래 값을 찾을 수 있다.
복잡한 덧셈 연산 암호화는 덧셈이기 때문에 이항해서 하나씩 풀면 풀린다.

아무리봐도 과제식 시간 끌기 문제였던 것 같다.
패치나 gdb 실행을 시키지 않고 순수히 역연산 코드를 일일이 짰다..
unk_402040 안에 키값이 들어있지만, 너무 길어서 적기를 포기했다..
144바이트 ....

#include <stdio.h>

unsigned char enc[144] = {
    0xE8,0x04,0x56,0x9D,0x40,0x31,0xDD,0x99,0xD6,0xAC,0x77,0x4B,0xAD,0xE5,0xFB,0xEA,
    0xDC,0x9C,0xF7,0xF4,0x55,0xC4,0xD8,0x44,0x23,0x04,0xAB,0x74,0xA6,0x9C,0xCE,0x32,
    0x60,0xF0,0x03,0x6F,0x65,0xD6,0xC9,0x91,0xDE,0x42,0xEC,0x71,0xA3,0xC5,0xA8,0x86,
    0x66,0x69,0x56,0xCE,0x77,0x5F,0xB0,0x25,0x05,0x71,0xD9,0x35,0x97,0xEF,0x90,0x71,
    0x88,0x12,0xCA,0x8A,0x92,0x64,0x40,0x88,0x5E,0xD3,0x79,0x82,0xC2,0x02,0x18,0xEB,
    0x10,0x75,0xDC,0x27,0x66,0xDC,0x7A,0x39,0x42,0x4B,0x32,0x78,0x9E,0x2A,0x46,0xDD,
    0x94,0x0D,0xE6,0x8D,0x21,0xC6,0x9E,0x67,0x67,0x80,0xB5,0x22,0xEE,0xB4,0xE6,0x76,
    0xC1,0x95,0x07,0x69,0x92,0x59,0x1B,0x33,0x83,0xD0,0xDD,0x1C,0xDE,0x4E,0x50,0x43,
    0x52,0xA5,0x84,0x8B,0x8E,0x41,0x18,0x25,0x63,0x9A,0x78,0x10,0x8C,0xA8,0x60,0xAB
};

unsigned char ror(unsigned char val, unsigned char r_bits) {
    return (val >> r_bits) | (val << (8 - r_bits));
}

void decrypt(unsigned char *flag) {
    unsigned char v21 = 0;

    for (int i = 0; i < 144; i++) {
        unsigned char tmp = flag[i];
        tmp ^= 0xAA;
        flag[i] = tmp ^ v21;
        v21 = (v21 + 3) & 0xFF;
    }

    for (int i = 0; i < 144; i++) {
        flag[i] = ror(flag[i], (i % 7) + 1);
    }

    unsigned char v17 = 55;
    unsigned char v18 = 19;

    for (int i = 0; i < 144; i++) {
        flag[i] = (flag[i] - v18) & 0xFF;
        unsigned char tmp = v17;
        v17 = (v17 + v18) & 0xFF;
        v18 = tmp;
    }

    unsigned char v14 = 0;
    for (int i = 0; i < 144; i++) {
        flag[i] ^= (v14 ^ 0x42);
        v14 = (v14 + 23) & 0xFF;
    }
}

int main() {
    unsigned char flag[144];

    for (int i = 0; i < 144; i++) {
        flag[i] = enc[i];
    }

    decrypt(flag);

    for (int i = 0; i < 144; i++) {
            printf("%c", flag[i]);
    }

    return 0;
}

ROL 비트회전을 복호화를 위한 ROR을 함수로 선언해준 후, main 함수에는 최종 결과값만 출력하도록 구현하였다.
나머지 복호화는 전부 decrypt 함수에서 실시한다.

해당 코드를 실행 시키면 플래그가 출력된다.

profile
배고파요 ..

0개의 댓글