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 형식에 맞춰 입력해주면

정답!
'4-1. 2025-1 심화 스터디 > 워게임 도장 깨기' 카테고리의 다른 글
[3주차] 250401 워게임 도장 깨기 (0) | 2025.04.02 |
---|---|
[1주차] 250318 워게임 도장 깨기 (1) | 2025.03.21 |