본문 바로가기

4-1. 2025-1 심화 스터디/워게임 도장 깨기

[2주차] 250326 워게임 도장 깨기

2주차

- 리버싱 문제 풀이 후 공유했다.

 

문제 1. rev-basic-4

문제 설명

 

문제 풀이

main 디컴파일 한 모습
sub140001000 함수에 따라 correct 와 wrong이 달라지므로 이곳으로 가본다.

 

이동 후 디컴파일 한 모습

byte_140003000으로 들어가 보았다.

 

basic-rev-3와 비슷하게 16진수로 표현된 것들이!!!!

sub~~에 있던 if문을 변형해 코드를 짜면 될 것 같았다.

 

if ( ((unsigned __int8)(16 * *(_BYTE *)(a1 + i)) | ((int)*(unsigned __int8 *)(a1 + i) >> 4)) != byte_140003000[i] )

 

이 부분 해석이 어려웠다.
a1+i에 있는 바이트 값을 16을 곱함 (4비트 왼쪽 시프트)
a1+i값을 4비트 오른쪽 시프트

이 두 값을 비트 OR 연산

이를 통해 상위 4비트와 하위 4비트를 서로 교환한 값을 생성하는 과정이다.

<<<지피티의 예제...>>>
16 * 0xAB = 0xAB0 (101010110000)
0xAB >> 4 = 0x0A (00001010)
0xAB0 | 0x0A = 0xABA (101010111010)

 

비트 시프트란 숫자의 이진수 표현을 왼쪽/오른쪽으로 이동시키는 연산
--> 비트 단위로 연산해 속도가 빠르고 특정 연산 최적화에 용이하다.

5 << 2 (5를 2비트 왼쪽시프트)
0000 0101 --> 0001 0100

20 >> 2 (20을 2비트 오른쪽 시프트)
0001 0100 --> 00000101

x >> n == x / 2^n

 

풀이 코드

#include <stdio.h>

int main(){
    int string[27] = {0x24, 0x27, 0x13, 0xC6, 0xC6, 0x13, 0x16, 0xE6, 0x47, 0xF5, 0x26, 0x96, 0x47, 0xF5, 0x46, 0x27, 0x13, 0x26, 0x26, 0xC6, 0x56, 0xF5, 0xC3,0xC3, 0xF5, 0xE3, 0xE3};

    
    for (int i= 0; i < 27; i++){                 //string에 27개

        printf("%c", (16* string[i]) & 0xF0 | (string[i]>>4));
    }

    return 0;

}

 

 

문제2. Simple Crack Me

문제 설명

이 문제는 사용자에게 숫자를 받아 정해진 방법으로 입력값을 검증하여 correct 또는 wrong를 출력하는 프로그램이 주어진다. 해당 바이너리를 분석해 correct를 출력하는 10진 양수 값을 찾는 문제이다.

 

문제 풀이

 

 

문제 설명에서 correct와 wrong을 출력한다고 설명했으므로 서치 기능을 통해서 correct와 wrong을 먼저 검색 해줬다.

 

위에서 correct 또는 wrong으로 출력되기 때문에 상위 함수를 봐야한다.
그래프를 올려보면 입력값을 받고 출력을 구현하는 함수의 이름은 sub_401AD5인 것을 알 수 있음. (이 함수를 찾는 것이 문제풀이에서 중요하다고 생각했다..!)

 

sub_401AD5 함수에서 F5키를 눌러주면 어느정도 분석 가능한 코드를 보여준다.
v11이 322376503값을 가질 때 correct를 출력하고 그렇지 않을 때는 %x is wrong을 출력한다는 것을 알 수 있다.

flag값은 DH{}형식을 갖는 정답이 되는 10진수 양수 값이기 때문에 저 수를 형식에 맞게 입력!

 

 

문제 풀이 핵심

: 문제 설명을 참고하여 correct와 wrong이 나뉘는 부분을 중심으로 분석, main함수가 명시되어 있지 않아 서치 기능 없이 함수를 찾기는 어려웠음.
+ 문제를 풀 때 IDA를 능숙히 사용하기 위해 IDA사용법을 익혀야겠다..!

 

 

<+ Simple Crack Me>

IDA로 열었을 때 모습이다. 어셈블리어로 Correct가 나오는 부분을 찾아줬다.

cmp          eax, 13371337h

 

코드를 보다보면 cmp를 찾아볼 수 있다. eax와 13371337h를 비교하고 jnz를 통해 점프 진행, correct를 출력하는 형태를 보아 여기서 비교하고 있는 13371337h가 우리가 찾고 있는 비교 flag이지 않을까 추측할 수 있다.

 

 13371337h에 우클릭해주면 이렇게 10진수 변환값이 나온다. 10진수 변환값인 322376503을 플래그 형식에 맞춰 입력해주면 문제가 풀린다.

 

 

문제3. Simple Crack Me2

문제 설명

1과 동일한 correct, wrong을 출력하는 프로그램에서 correct를 출력하는 값을 찾는 문제이다.

코드 파악

    lea     rax, [rbp+s1]
.text:000000000040140C                 lea     rdx, unk_402068
.text:0000000000401413                 mov     rsi, rdx
.text:0000000000401416                 mov     rdi, rax
.text:0000000000401419                 call    sub_4011EF
.text:000000000040141E                 lea     rax, [rbp+s1]
.text:0000000000401425                 mov     esi, 1Fh
.text:000000000040142A                 mov     rdi, rax
.text:000000000040142D                 call    sub_401263
.text:0000000000401432                 lea     rax, [rbp+s1]
.text:0000000000401439                 mov     esi, 5Ah ; 'Z'
.text:000000000040143E                 mov     rdi, rax
.text:0000000000401441                 call    sub_4012B0
.text:0000000000401446                 lea     rax, [rbp+s1]
.text:000000000040144D                 lea     rdx, unk_40206D
.text:0000000000401454                 mov     rsi, rdx
.text:0000000000401457                 mov     rdi, rax
.text:000000000040145A                 call    sub_4011EF
.text:000000000040145F                 lea     rax, [rbp+s1]
.text:0000000000401466                 mov     esi, 4Dh ; 'M'
.text:000000000040146B                 mov     rdi, rax
.text:000000000040146E                 call    sub_4012B0
.text:0000000000401473                 lea     rax, [rbp+s1]
.text:000000000040147A                 mov     esi, 0F3h
.text:000000000040147F                 mov     rdi, rax
.text:0000000000401482                 call    sub_401263
.text:0000000000401487                 lea     rax, [rbp+s1]
.text:000000000040148E                 lea     rdx, unk_402072
.text:0000000000401495                 mov     rsi, rdx
.text:0000000000401498                 mov     rdi, rax
.text:000000000040149B                 call    sub_4011EF
.text:00000000004014A0                 mov     rcx, cs:s2
.text:00000000004014A7                 lea     rax, [rbp+s1]
.text:00000000004014AE                 mov     edx, 20h ; ' '  ; n
.text:00000000004014B3                 mov     rsi, rcx        ; s2
.text:00000000004014B6                 mov     rdi, rax        ; s1
.text:00000000004014B9                 call    _memcmp
.text:00000000004014BE                 test    eax, eax
.text:00000000004014C0                 jnz     short loc_4014D8
.text:00000000004014C2                 lea     rax, aCorrect   ; "Correct!"
.text:00000000004014C9                 mov     rdi, rax        ; s
.text:00000000004014CC                 call    _puts
.text:00000000004014D1                 mov     eax, 0
.text:00000000004014D6                 jmp     short loc_4014EC

 

correct가 출력되는 부분의 코드이다. 보면 s1을 s2랑 비교하는 것을 알 수 있다. 우리가 찾는 문자열은 s2, 입력 받은 문자열은 s1이 되는 것 같다. 다만 입력 받은 값을 여러 변환 과정을 거친 후 s2와 비교한다.

correct 부분의 코드를 디컴파일 해봤다. s1과 s2를 memcmp로 비교하고 있다.

sub_4011B6을 통해 s1의 길이가 32인지 확인하는 작업을 우선 진행한다.

sub_4011EF를 통해 문자열 변형을 진행한다.

 

이 함수는 key 문자열을 반복하면서 dst 문자열 앞 32바이트에 XOR 연산을 적용하는 함수였다.

sub_401263을 통해 문자열 변형을 진행한다.

 

401263을 디컴파일 한 결과

결국 중요한 건 result += a2 하는 함수이다.

 

4012B0을 디컴파일한 결과이다.

이것도 결국 중요한 건 위와 반대되는 함수라는 것. 얘는 -a2를 진행하는 함수이다.

XOR, +, -, XOR, +, -, XOR 진행으로 문자열을 암호화 했던 것이다. 이를 역으로 진행해주면 처음 문자열을 알 수 있다.

 

문제 풀이

unk_402008      db 0F8h                 ; DATA XREF: .data:s2↓o
.rodata:0000000000402009                 db 0E0h
.rodata:000000000040200A                 db 0E6h
.rodata:000000000040200B                 db  9Eh
.rodata:000000000040200C                 db  7Fh ; 
.rodata:000000000040200D                 db  32h ; 2
.rodata:000000000040200E                 db  68h ; h
.rodata:000000000040200F                 db  31h ; 1
.rodata:0000000000402010                 db    5
.rodata:0000000000402011                 db 0DCh
.rodata:0000000000402012                 db 0A1h
.rodata:0000000000402013                 db 0AAh
.rodata:0000000000402014                 db 0AAh
.rodata:0000000000402015                 db    9
.rodata:0000000000402016                 db 0B3h
.rodata:0000000000402017                 db 0D8h
.rodata:0000000000402018                 db  41h ; A
.rodata:0000000000402019                 db 0F0h
.rodata:000000000040201A                 db  36h ; 6
.rodata:000000000040201B                 db  8Ch
.rodata:000000000040201C                 db 0CEh
.rodata:000000000040201D                 db 0C7h
.rodata:000000000040201E                 db 0ACh
.rodata:000000000040201F                 db  66h ; f
.rodata:0000000000402020                 db  91h
.rodata:0000000000402021                 db  4Ch ; L
.rodata:0000000000402022                 db  32h ; 2
.rodata:0000000000402023                 db 0FFh
.rodata:0000000000402024                 db    5
.rodata:0000000000402025                 db 0E0h
.rodata:0000000000402026                 db 0D9h
.rodata:0000000000402027                 db  91h
.rodata:0000000000402028                 db    0

 

s2가 있는 곳으로 이동하면 위와 같은 내용을 확인할 수 있다. 이를 정리하면

s2 = [ 0xF8, 0xE0, 0xE6, 0x9E, 0x7F, 0x32, 0x68, 0x31,
0x05, 0xDC, 0xA1, 0xAA, 0xAA, 0x09, 0xB3, 0xD8, 0x41,
0xF0, 0x36, 0x8C, 0xCE, 0xC7, 0xAC, 0x66, 0x91, 0x4C,
0x32, 0xFF, 0x05, 0xE0, 0xD9, 0x91 ]

 

위에서 디컴파일 한 코드를 보면 XOR 연산을 진행하는데 key가 세개 들어간 것을 확인할 수 있다.

각각 첫번째 unk_402068, 두번째 unk_40206D, 세번째 unk_402072

그 부분을 찾아가보면 이렇게 s2와 같이 찾아볼 수 있다.

 

역연산 전 다시 확인, 정리해주면

원래 변형: XOR1 > +31 > -90 > XOR2 > -77 > +243 > XOR3

역연산: XOR3 > -243 > +77 > XOR2 > +90 > -31 > XOR1

 

XOR 연산

#include <stdio.h>
#include <string.h>
#include <stdint.h>

void xor_buffer(uint8_t* buf, int len, const uint8_t* key, int key_len) {
    for (int i = 0; i < len; i++) {
        buf[i] ^= key[i % key_len];
    }
}

int main() {
    uint8_t s2[32] = {
        0xF8, 0xE0, 0xE6, 0x9E, 0x7F, 0x32, 0x68, 0x31,
        0x05, 0xDC, 0xA1, 0xAA, 0xAA, 0x09, 0xB3, 0xD8,
        0x41, 0xF0, 0x36, 0x8C, 0xCE, 0xC7, 0xAC, 0x66,
        0x91, 0x4C, 0x32, 0xFF, 0x05, 0xE0, 0xD9, 0x91
    };

    uint8_t key1[] = { 0xDE, 0xAD, 0xBE, 0xEF };
    uint8_t key2[] = { 0xEF, 0xBE, 0xAD, 0xDE };
    uint8_t key3[] = { 0x11, 0x33, 0x55, 0x77, 0x99, 0xBB, 0xDD };

    // 복사해서 s1 버퍼로 사용
    uint8_t s1[32];
    memcpy(s1, s2, 32);

    // 역변환 순서 적용 (역방향)
    xor_buffer(s1, 32, key3, sizeof(key3));
    for (int i = 0; i < 32; i++) s1[i] = (s1[i] - 243 + 256) % 256;
    for (int i = 0; i < 32; i++) s1[i] = (s1[i] + 77) % 256;
    xor_buffer(s1, 32, key2, sizeof(key2));
    for (int i = 0; i < 32; i++) s1[i] = (s1[i] + 90) % 256;
    for (int i = 0; i < 32; i++) s1[i] = (s1[i] - 31 + 256) % 256;
    xor_buffer(s1, 32, key1, sizeof(key1));

    // 결과 출력 (ASCII 가능한 문자만 출력)
    printf("Recovered input: ");
    for (int i = 0; i < 32; i++) {
        if (s1[i] >= 32 && s1[i] <= 126)
            printf("%c", s1[i]);
        else
            printf(".");
    }
    printf("\n");

    return 0;
}

 

위 코드를 실행한 결과다. 문자열 변환을 거치기 전 올바른 입력 값은 9ce745c0d5faaf29b7aecd1a4a72bc86이었다. 이를 flag 형식에 맞춰 입력해주면

정답!