본문 바로가기

INTERTLUDE/리버싱 스터디

Codeengn Basic RCE 20

Codeengn Basic RCE 20

 

Q. 이 프로그램은 Key파일을 필요로 하는 프로그램이다. 'Cracked by: CodeEngn!' 문구가 출력 되도록 하려면

crackme3.key 파일안의 데이터는 무엇이 되어야 하는가 Ex) 41424344454647

 

--> 해당 문제를 읽으며, 

이 CrackMe는 외부 key 파일(CRACKME3.KEY) 이 없으면 동작하지 않고,
그 key 파일의 내용이 프로그램이 원하는 대로 맞아야 Cracked by: CodeEngn! 이라는 성공 메세지가 출력된다는 사실을

알 수 있다. 즉, 우리가 해야 할 것은 “프로그램이 원하는 KEY 파일 내용을 찾아서 넣는 것” 이라는 사실을 알 수 있다. 

 

Codeengn Basic RCE 20번 문제를 풀기 위해서, 먼저 20.exe 문제 파일을 다운로드 받았다.

7-zip을 이용하여 압축을 해제한 다음, 

 

DIE를 통해서, 해당 파일이 UPX로 패킹이 되어 있지 않다는 사실을 알 수 있다.

만일 패킹이 되어 있다면 언패킹 과정을 거쳐야 하지만, 그렇지 않기에 Immunity Debugger로 가서 바로 파일을 열어주었다.

해당 코드 부분에서, CreateFileA , Readfile 함수가 있다는 것을 확인할 수 있다. 

- CreateFileA 함수는 윈도우 API 함수로, 파일 핸들을 얻는, 즉 파일을 열거나 새로 만드는 함수이고 

- ReadFile 함수는 윈도우 API 함수로, 열어 둔 파일에서 데이터를 읽는 함수라는 것을 알 수 있다. 

 

1) 파일 열기 : CreateFileA

 CreateFileA () 함수 코드를 통해, 

  • FileName = "CRACKME3.KEY"
  • 이 파일이 없으면 CreateFileA는 -1을 리턴 → 바로 종료 루트로 분기
  • 존재하면 정상 핸들이 반환 → 다음 단계로 진행

따라서 프로그램과 같은 폴더에 CRACKME3.KEY 파일을 만들어주면 일단, 첫 번째 분기를 통과한다는 사실을 알 수 있다. 


그리해서, 프로그램과 같은 폴더에 CRACKME3.KEY 파일을 생성해 주었다. 


2) 파일 길이 검사 – ReadFile

ReadFile 함수 : 윈도우 API 함수로, 열어 둔 파일에서 데이터를 읽는 함수

ReadFile() 이 호출되는 것은, KEY 파일 전체 내용을 특정 버퍼(0x402008)에 읽어오는 함수라는 것이다. 

 

읽은 뒤, 프로그램은 이러하게 검사를 진행한다. 

더보기
읽은 바이트 수 == 0x12 (18 bytes) ?  
아니면 종료
0040104D  BB 08204000      MOV EBX,00402008   ; KEY 버퍼 주소
00401052  6A 00           PUSH 0             ; lpOverlapped = NULL
00401054  68 A0214000     PUSH 004021A0      ; pBytesRead
00401059  50              PUSH EAX           ; BytesToRead = 0x12 (18)
0040105A  53              PUSH EBX           ; lpBuffer = 0x402008
0040105B  FF35 F5204000   PUSH [4020F5]      ; hFile
00401061  E8 30040000     CALL ReadFile
00401066  83BD A0214000,12 CMP DWORD PTR DS:[4021A0],12
0040106D  75 C8           JNZ SHORT 00401037 ; 18바이트 아니면 실패

 

  1. ReadFile() 로 CRACKME3.KEY 내용을 버퍼 0x402008에 읽는다.
  2. 실제로 읽힌 바이트 수를 pBytesRead(0x4021A0)에 저장한다.
  3. CMP [4021A0], 12h → 정확히 0x12(18) 바이트인지 검사
  4. 아니면 00401037(실패 루틴)으로 점프

따라서 KEY 파일 길이는 정확히 18바이트여야만 한다.

HxD로 CRACKME3.KEY를 열고,
우선 테스트용으로 "101112131415161718" (18글자) 를 넣어 길이 검사를 통과시켰다.


3) 길이 통과 후 – KEY 마지막 4바이트 비교                                                                                                              

길이 체크를 통과하면, 다음과 같은 코드가 실행된다.

00401086  68 08204000      PUSH 20.00402008      ; KEY 버퍼
0040108B  E8 AC020000      CALL 20.0040133C      ; 마지막 4바이트 로드
00401090  83C4 04          ADD ESP,4
00401093  3B05 F9204000    CMP EAX,DWORD PTR DS:[4020F9]
00401099  0F94C0           SETE AL
0040109C  50               PUSH EAX             ; (다음 루틴에서 사용)
0040109D  84C0             TEST AL,AL
0040109F  74 96            JE SHORT 00401037    ; AL==0 → 실패 경로

 

  • 이 부분의 동작은 다음과 같이 정리할 수 있다.
    1. PUSH 0x402008
      → KEY 데이터가 들어 있는 버퍼 주소(0x402008)를 인자로 스택에 올린다.
    2. CALL 0x40133C
      → 이 서브루틴이 KEY의 마지막 4바이트를 읽어 EAX에 저장한다.
    3. 호출이 끝나면 ADD ESP,4 로 인자 정리.
    4. CMP EAX,[4020F9]
      → 방금 읽어 온 KEY 마지막 4바이트(EAX)와,
      프로그램 내부에 저장된 기준 DWORD 값([4020F9])을 비교한다.
    5. SETE AL
      → 두 값이 같으면 AL=1, 다르면 AL=0 으로 설정한다.
      (여기서 AL은 더 이상 “EAX의 하위 바이트”가 아니라, 비교 결과를 담는 플래그 역할을 한다.)
    6. TEST AL,AL
      → AL이 0인지 확인. 0이면 ZF=1, 1이면 ZF=0.
    7. JE 00401037
      → ZF=1(즉 AL=0, 비교 실패)이면 실패 루틴으로 점프한다.
      반대로 AL=1(비교 성공)이면 점프하지 않고 다음 단계로 진행한다.
    따라서, KEY 파일의 마지막 4바이트를 [4020F9] 에 저장된 값과 같게 맞춰야, 이 조건을 통과할 수 있는 것이다. 
  • 3-1. 0x40133C 서브루틴 ( (KEY 끝 4바이트를 EAX로 읽어오는 함수)

 

0040133C  8B7424 04      MOV ESI,DWORD PTR SS:[ESP+4]  ; KEY 버퍼 주소
00401340  83C6 0E        ADD ESI,0E                    ; +0x0E → 마지막 4바이트 위치
00401343  8B06           MOV EAX,DWORD PTR DS:[ESI]    ; EAX = KEY[0x0E..0x11]
00401345  C3             RETN

 

  • CALL 40133C 직후의 스택에는
    • [ESP] : 리턴 주소
    • [ESP+4] : 인자(0x402008, KEY 버퍼 시작 주소)
  • MOV ESI,[ESP+4] → ESI = 0x402008
  • ADD ESI,0x0E → ESI = 0x402016
    → 18바이트 중 마지막 4바이트(Offset 0x0E~0x11)의 시작 주소.
  • MOV EAX,[ESI] → KEY 마지막 4바이트(DWORD)를 EAX에 로드.
  • RET → 호출 지점으로 복귀하며, EAX에는 여전히 KEY 마지막 4바이트 값이 남아 있다.

 

디버깅하면서 이 지점( MOV EAX,[ESI] 혹은 RET 직후 )에서
레지스터 창의 EAX를 보면 현재 KEY 파일의 마지막 4바이트가 무엇인지 바로 확인할 수 있는 것이다. 


 CALL 서브루틴에서 KEY 파일의 마지막 4바이트를 EAX에 가져온다.

 

EAX = 38313731 (HEX)

이 값은 아래 KEY 파일의 끝 부분과 일치한다:

 

 

4-2. EAX와 [4020F9] 비교

CMP  EAX, [4020F9]
SETE AL
TEST AL,AL
JE   00401037
  • CMP 결과가 같으면 → AL = 1
  • 다르면 → AL = 0
  • TEST AL,AL 로 AL이 0인지 확인하고,
  • 0이면(ZF=1) 실패 루틴으로 점프

즉, KEY 파일의 마지막 4바이트를 [4020F9] 에 들어 있는 값과 동일하게 맞춰야, 이 분기를 통과할 수 있다.

 

 

나는 디버거에서 CMP EAX,[4020F9] 줄에서 [4020F9] 를 더블 클릭해 메모리 창을 열어 보았고,
내 실행 파일에서는:

였다.

그래서 HxD에서 KEY 끝 4바이트를 07 50 34 12 로 수정했다.

 

이제 길이 검사 + 마지막 4바이트 검사까지는 통과하여
메시지 박스는 뜨지만, 내용이 "Cracked by: prruttw{x~zy]x!" 처럼
이상한 문자열로 출력되는 것을 확인했다.

 

 

이상한 문자열의 원인을 찾기 위해,
메시지 박스가 뜨기 전에 호출되는 또 다른 서브루틴을 찾아 보았다. 

 

그게 바로 다음 XOR 루프의 코드였다. 

00401313  XOR EAX,EAX
00401315  MOV ESI,[ESP+4]    ; KEY 시작 위치
00401318  MOV BL,41          ; BL = 0x41 (‘A’)
0040131B  MOV AL,[ESI]       ; KEY[i]
0040131D  XOR AL,BL          ; KEY[i] XOR BL
0040131F  MOV [ESI],AL       ; KEY[i] = XOR 결과
00401321  INC ESI
00401322  INC BL             ; BL = 41,42,43,... 증가
00401324  CMP AL,0
00401326  JE short break
00401328  CMP BL,4F          ; BL <= 4F 까지 반복
0040132C  JNZ 40131B

 

 

곧, 루프를 살펴보자면 

  • KEY[i]를 한 글자씩 가져와 BL과 XOR 연산을 함. 
  • XOR 결과를 다시 KEY[i] 위치에 덮어씀
  • BL은 0x41 (‘A’)부터 0x4F 까지 1씩 증가
  • AL(결과)이 0이면 루프 종료되는 것이다. 

즉, KEY 앞부분은 이 XOR 루프 후의 값이 메시지 문자열이 된다는 사실을 알 수 있다. 

 

루프는 다음과 같이 작동한다. 

for each byte in KEY:
    AL = KEY[i]
    AL = AL XOR BL   ; BL은 0x41부터 시작해 1씩 증가 (41, 42, 43 …)
    KEY[i] = AL       ; XOR 결과를 다시 KEY에 저장
    if AL == 0: break ; 0이 되면 루프 종료

 

즉,

  • KEY 첫 바이트는 BL=0x41과 XOR됨. 
  • 둘째 바이트는 BL=0x42와 XOR됨. 
  • 9번째는 BL=0x49와 XOR됨. 

 

따라서, 우리가 원하는 최종 문자열의 앞 부분은 "CodeEngn!" 이기에,

이 중 "Cracked by: " 는 실행 파일 내에 평문으로 존재하고,
우리가 맞춰야 하는 부분은 "CodeEngn" + 널 종료 \0 이다.

 

즉, XOR 루프가 끝났을 때 KEY의 앞 9바이트는 다음과 같아야 한다.

"CodeEngn\0"

 

루프 동작을 수식으로 쓰면:

(KEY[i] XOR BL) → "CodeEngn\0"

 

이므로 역으로:

 

 

 

 

을 사용해 계산할 수 있다. 

KEY[0] XOR 41 = 'C'
KEY[1] XOR 42 = 'o'
KEY[2] XOR 43 = 'd'
KEY[3] XOR 44 = 'e'
KEY[4] XOR 45 = 'E'
KEY[5] XOR 46 = 'n'
KEY[6] XOR 47 = 'g'
KEY[7] XOR 48 = 'n'
KEY[8] XOR 49 = 00 (널 종료)

 

역산하면:

 

문자                                      XOR BL                                                          KEY[i]

C 0x41 0x02
o 0x42 0x2D
d 0x43 0x27
e 0x44 0x21
E 0x45 0x00
n 0x46 0x28
g 0x47 0x20
n 0x48 0x26
\0 0x49 0x49

 

따라서 KEY 앞 9바이트는 다음과 같아야 한다. ( "CodeEngn\0"을 만들도록 XOR 역산한 값 ) 

02 2D 27 21 00 28 20 26 49

이 값을 HxD에서 CRACKME3.KEY 의 0~8번 바이트에 덮어써 준다.


  • 그렇다면 18바이트 key는 어떻게 이루어지는가? 

 

  • 앞 9바이트 : XOR 루프를 거쳐 "CodeEngn\0" 이 되도록 계산한 값
    → 02 2D 27 21 00 28 20 26 49
  • 중간 5바이트(0x09~0x0D) : 프로그램에서 따로 사용하지 않으므로 아무 값이나 가능
    (보통 00으로 채운다)
  • 마지막 4바이트(0x0E~0x11) : CMP EAX,[4020F9] 의 비교 대상이 되는 DWORD 값과 동일해야 함

 

** 중간 5바이트(0x09~0x0D)는 어떤 값을 넣어도 프로그램 동작에 영향을 주지 않는다. 일반적으로 00을 채운다.


 

 

 

디버깅한 [4020F9] 값이 7B553412임을 알 수 있다. 

그렇다면, 우리는 필요한  key 값을 모두 구하였다. 

 

최종 KEY 파일 구조 (18 bytes)

02 2D 27 21 00 28 20 26 49 00 00 00 00 00 7B 55 34 12

 

 

위에처럼 HxD를 통해 key 값을 입력하고, 변경 사항을 저장한 뒤

실행하면 이러하게 "CodeEngn!" 문자열이 나타나는 것을 확인할 수 있다.

 

문제 해결 성 -공 

 

 


 

(참고) [4020F9] 값은 사람마다 다를 수 있음. 

 

이번 풀이에서 핵심 포인트 중 하나는:

KEY 마지막 4바이트 = 실행 파일 내부의 어떤 DWORD 상수값

이라는 점이다. 그 상수값이 이 바이너리에서는 [4020F9] 에 저장되어 있었고,
내 환경에서는 그 값이  7B 55 34 12 였다.

 

하지만 다른 버전의 exe, 재배포본, 재컴파일본을 사용하면 각자 다른 값이 나올 수도 있다. 

 

그래서 이 문제의 일반적인 학습 포인트는:

  1. CALL 서브루틴을 따라가 EAX에 무엇을 싣는지(여기서는 KEY 마지막 4바이트) 확인하고,
  2. 바로 뒤에 나오는 CMP EAX, [XXXXXX] 에서
    비교 상수의 주소 XXXXXX를 찾은 뒤,
  3. 그 주소의 값을 덤프 창에서 보고
    그 값으로 KEY 마지막 4바이트를 맞춰 주는 것

이라는 것이다.

즉, 해당 문제를 풀 때는 자신이 가진 exe에 대해 직접 디버깅해서 [4020F9] 값을 확인해야 한다 -! 


더보기

** 이 문제를 풀면서 가져가야 할 핵심 아이디어들

 

1. 외부 파일 관련 API부터 추적하기

  • CreateFileA, ReadFile, fopen, fread 같은 함수가 보이면
    “어떤 파일을 열고 / 얼마나 읽는지” 먼저 보기 

2. 비교 지점(CMP)을 기준으로 성공/실패 분기 분석하기

  • CMP something, const → JZ/JNZ/JE/JNE 흐름을 따라가
    어느 쪽이 성공 루트인지 확인하기 

3. 데이터를 변형하는 루프가 있으면 “암호화/복호화”를 의심하기

  • 이 문제의 XOR 루프처럼:
    • 레지스터 하나(BL)가 점점 바뀌면서
    • 버퍼(ESI)를 한 바이트씩 읽고
    • XOR/ADD/SUB 같은 연산 후 다시 같은 위치에 써 넣는 구조는
      “간단한 자기복호화 루틴”일 가능성이 높기에. 
  • 이런 루프가 있다면 “루프 결과가 실제로 어디에 쓰이는지” 를 따라가고,
    그 결과를 원하는 문자열/값으로 맞추도록 역산하기.                                                                                          

 

 

'INTERTLUDE > 리버싱 스터디' 카테고리의 다른 글

Codeengn Basic RCE 17  (0) 2025.11.20
Codeengn Basic RCE 13  (0) 2025.11.18
Codeengn Basic RCE 16  (1) 2025.11.11
Codeengn Basic RCE 15  (0) 2025.11.11
Codeengn Basic RCE 14  (0) 2025.11.07