Q. Key 값이 BEDA-2F56-BC4F4368-8A71-870B 일 때 Name 은 무엇인가.
힌트 : Name은 한자리인데.. 알파벳일수도 있고 숫 자일수도 있고..
정답인증은 Name의 MD5 해쉬값(대문자)
즉, 문제를 해결하려면
- 프로그램이 Name과 Key를 어떻게 검증하는지 분석하고
- 주어진 Key가 통과하도록 만드는 올바른 Name(단 한 글자) 을 찾은 뒤
- 그 Name의 MD5 해시(대문자) 를 제출하면 된다. 라고 정리할 수 있다.
Codeengn Basic RCE 17번 문제를 풀기 위해서, 먼저 17.exe 문제 파일을 다운로드하였다.
7-zip을 이용하여 압축을 해제한 다음,

DIE를 통해서, 해당 파일이 UPX로 패킹이 되어 있지 않다는 사실을 알 수 있다.
만일 패킹이 되어 있다면 언패킹 과정을 거쳐야 하지만, 그렇지 않기에 Immunity Debugger로 가서 바로 파일을 열어주었다.
1) 파일 열기 및 분석하기

Immunity Debugger에서 F9로 실행 시켰더니,
아래와 같은 창이 나타나는 것을 확인할 수 있었다.

- Name : 사용자 입력
- Key : 문제에서 주어진 BEDA-2F56-BC4F4368-8A71-870B 입력
문제에서 “Name은 한 자리” 라고 했으므로,
일단 테스트로 Name = ‘A’, Key = 문제에서 준 값을 입력하고 Check it! 을 눌러보았다.

그 결과, 성공 메시지가 뜨는 대신 Key 입력 칸이 Please Enter More Chars...로 바뀌는 것을 확인할 수 있었다.
즉, 힌트와 달리 실제 프로그램 내부에서는 길이 제한이 걸려 있어
너무 짧은 Name은 아예 검증 루틴에 들어가지 못한다는 사실을 알 수 있었다.
→ 따라서 첫 번째 목표는 길이 체크 우회이다.
2) 길이 체크 루틴 찾기
“Please Enter More Chars…” 문자열이 어디에서 사용되는지 찾기 위해,
우클릭 → Search for → All referenced strings 메뉴를 선택해, 문자열을 확인하였다.
여기서 "Please Enter More Chars..." 항목을 더블클릭하면, 해당 문자열을 사용하는 코드 위치로 바로 점프할 수 있다.

그 주변 코드는 다음과 같다.
0045BB1B TEST EAX,EAX
0045BB1D JE SHORT 0045BB24
0045BB1F SUB EAX,4
0045BB22 CMP EAX,3
0045BB27 JGE SHORT 0045BB3E
0045BB29 MOV EDX,17.0045BC18 ; "Please Enter More Chars..."
- EAX : Name 문자열 길이
- SUB EAX,4 → 내부적으로 길이에서 4를 뺀 뒤
- CMP EAX,3 / JGE를 통해 길이가 3 이상일 때만 정상 루틴으로 점프
- 그렇지 않으면 "Please Enter More Chars..." 메시지를 보여준다.
즉:: Name 최소 길이가 3으로 제한이 걸려 있는 상태이다.
-> 우리는 Name을 한 글자로 쓰고 싶으니, 조건을 “1 이상이면 통과” 로 바꿔주어야겠다는 생각을 하였다.
3) 길이 체크 패치 (CMP EAX,3 → CMP EAX,1)
여기서의 EAX는 SUB EAX,4 이후의 값이므로, 원래는 길이가 3 이상일 때만 정상 루틴으로 점프하도록 설계된 것이다.

그러나, 우리는 “길이가 1 이상이면 통과” 로 변경하고 싶기에,

따라서 CMP EAX,3 부분에 커서를 두고
Space(Assemble) 를 눌러 다음과 같이 수정하였다.
- 원래 : CMP EAX,3
- 수정 : CMP EAX,1
이제 Name 길이가 1 이상이면 정상 루틴을 통과하게 되는 것이다.

수정한 코드 범위를 드래그해서 Copy to executable → Selection → Save file로
새 파일을 저장하고, 이후부터는 이 패치된 파일을 열어서 디버깅을 진행했다.
4) 성공 메시지에서 Key 비교 루틴 추적
다음 목표는 “어디에서 Key를 만들고 비교하는지” 찾는 것이다.
이번에는 문자열 리스트에서 "Good Boy!!!"를 찾아 더블클릭하였다. (성공 시 출력되는 메시지 박스 문자열)
그 주변 코드에는 대략 다음과 같은 흐름이 보인다.

0045BB9B CALL 17_chang.0045B850 ; Name으로부터 내부 Key 생성
...
0045BBA4 CALL 17_chang.00404C3C ; 내부 Key와 사용자가 입력한 Key 비교
0045BBA9 JNZ SHORT 17_chang.0045BBC5 ; 다르면 실패 경로로 점프
...
0045BBB0 CALL 17_chang.00458C78 ; "Good Boy!!!" 메시지 박스
- 의미를 정리하면:
- CALL 0045B850
→ Name 문자열을 이용해 내부 Key 값을 생성한다. - CALL 00404C3C
→ 내부 Key와, 우리가 입력한 Key(문제에서 준 값)를 비교한다. - JNZ
→ 두 값이 다르면 다른 루틴으로 빠지고,
이 쪽에서는 Sleep()을 반복 호출하는 무한 루프가 이어진다. - 그 분기를 통과했을 때만
→ "Good Boy!!!" 메시지가 뜨며 성공.
- CALL 0045B850
- Name → 내부 Key 생성 → 비교 → 성공 의 구조임을 알 수 있었다.
5) 내부 Key 계산 확인


이제 0045BB9B 의 CALL 지점에 브레이크포인트를 걸고,
패치된 실행 파일(17_chang.exe)에서
- Name : 한 글자 (예: ‘A’)
- Key : BEDA-2F56-BC4F4368-8A71-870B
를 입력한 뒤 Check it! 을 눌러 실행하였다.

브레이크포인트에서 멈춘 뒤, F8(Step Over)로 한 단계씩 진행하면서
비교 직전 레지스터 값을 확인해 보면 다음을 알 수 있었다.
F8(Step over)로 한 단계씩 실행하며, 비교 직전 레지스터 상태를 보면:
- EAX : 사용자가 입력한 Key 문자열
→ "BEDA-2F56-BC4F4368-8A71-870B" 가 들어 있는 버퍼 주소 - EDX : Name을 이용해 프로그램이 계산한 내부 Key
→ "66EE-3EEC-2A139188-8B4F-79E2" 같은 형태의 문자열 버퍼 주소
즉,
EAX = 입력 Key
EDX = Name 기반 내부 Key
--> 이렇게 두 주소를 가리키고 있고, 이후 비교 함수에서 이 둘을 비교한다는 사실을 확인할 수 있다.
6) 내부 Key 생성 루틴 분석
이제 CALL 0045B850 내부로 들어가서 Name을 이용해 Key를 계산하는 부분을 살펴보았다.

조금 내려가면 다음과 같은 루프가 보인다.

0045B89D MOV EBX,DWORD PTR SS:[EBP-4] ; Name 문자열 주소
0045B8A0 MOVZX ESI,BYTE PTR DS:[EBX+ECX-1] ; Name[ECX-1] 문자 1바이트 로드
0045B8A5 ADD ESI,EDX ; 누적값과 더함
0045B8A7 IMUL ESI,ESI,772 ; * 0x772
0045B8AD MOV EDX,ESI
0045B8AF IMUL EDX,ESI ; 제곱(ESI * ESI)
0045B8B2 ADD ESI,EDX ; ESI = ESI + ESI^2
0045B8B4 OR ESI,ESI ; (값은 그대로, 플래그만)
0045B8B6 IMUL ESI,ESI,474 ; * 0x474
0045B8BC ADD ESI,ESI ; * 2
0045B8BE MOV EDX,ESI ; 결과 누적
0045B8C0 INC ECX ; 다음 문자
0045B8C1 DEC EAX
0045B8C2 JNZ SHORT 0045B89D
의미를 정리하면:
- EBX : Name 문자열 포인터
- ECX : 현재 문자 인덱스 (1부터 시작)
- EAX : 남은 문자 개수 (루프 카운터)
- EDX/ESI : 누적 계산에 쓰이는 변수
한 글자 Name의 경우를 생각해 보면, 처음에 EDX = 0이고 루프는 딱 한 번만 돌고 끝난다고 볼 수 있다.
이를 수식으로 정리하면 (i = Name 한 글자의 ASCII 값):
ESI = i + 0 = i
ESI = ESI * 0x772
EDX = ESI
EDX = ESI * ESI → ESI^2
ESI = ESI + EDX → ESI = ESI + ESI^2
ESI = ESI * 0x474
ESI = ESI * 2
결국 최종적으로
ESI = (((i × 0x772)² + (i × 0x772)) × 0x474) × 2 라는 32비트 값이 만들어진다.
이 값이 이후 Key 문자열로 변환되는 과정에 사용된다.
7) C 코드로 변환
어셈블리 루프를 그대로 C로 옮긴 코드가 아래와 같다.

#include <stdio.h>
#include <math.h>
int main(void)
{
unsigned int esi = 0;
// ASCII '0'(0x30) ~ 'z'(0x7A) 범위 브루트포스
for (int i = 0x30; i <= 0x7A; i++)
{
// 어셈블리 루프와 동일한 계산
esi = i * 0x772;
esi = ((pow(esi, 2) + esi) * 0x474) * 2;
// 상위 2바이트가 0xBEDA인지 확인
unsigned short* pesi = (unsigned short*)&esi;
if (*(++pesi) == 0xBEDA)
{
printf("%c -> %X correct!!\n", i, esi);
continue;
}
printf("%c -> %X\n", i, esi);
}
return 0;
}
여기서 중요한 부분은:
- i : Name 후보 문자 (ASCII)
- esi : 위에서 정리한 수식대로 계산된 32비트 값
- unsigned short* pesi = (unsigned short*)&esi;
→ 32비트 esi를 16비트 2개(하위/상위 워드)로 나눠보기 위함 - *(++pesi) : 상위 16비트(두 번째 워드)
→ 이 값이 0xBEDA와 같은지 검사 부분이다.
0xBEDA와 비교하는 이유는,
최종 Key가 BEDA-2F56-...로 시작하기 때문에, 내부 32비트 값의 상위 2바이트가 0xBEDA이어야 맞기 때문이다.
Q. 왜 상위 2바이트(0xBEDA)를 비교할까?
최종 Key는 BEDA-2F56-... 로 시작한다.
내부에서 사용되는 32비트 값 esi 를
**16비트 2개(하위/상위 워드)**로 나누어 보면,
그중 상위 16비트가 0xBEDA 이면
→ 바로 Key 앞부분과 연결될 수 있기 때문이다.
코드를 실행해 보면 콘솔에 각 문자와 계산 결과가 쭉 출력되는데,
그중 아래와 같은 줄이 눈에 띈다.

F -> BEDACA60 correct!!
즉,
- Name 후보 문자를 ‘F’(0x46)로 넣었을 때
- 최종 ESI 값이 0xBEDACA60 이 되고,
- 이 값의 상위 2바이트가 0xBEDA로,
우리가 원하는 Key의 앞부분과 정확히 일치한다는 것.
따라서 이 문제에서 요구하는 Name은 Name = 'F'라는 결론을 얻을 수 있다.
8) MD5 계산 및 검증
문제에서 “정답 인증은 Name의 MD5 해쉬값(대문자)”라고 했으므로, 이제 ‘F’에 대한 MD5를 구하면 된다.
- Name : F
- MD5("F") (대문자) : 800618943025315F869E4E1F09471012
실제로 프로그램에:
- Name : F
- Key : BEDA-2F56-BC4F4368-8A71-870B

를 입력하고 Check it! 을 눌러보면,
Good Boy!!! 메시지 박스가 뜨면서 성공적으로 통과하는 것을 확인할 수 있다.

문제 해결 성-공!
'INTERTLUDE > 리버싱 스터디' 카테고리의 다른 글
| Codeengn Basic RCE 9 (0) | 2025.11.26 |
|---|---|
| Codeengn Basic RCE 11 (0) | 2025.11.26 |
| Codeengn Basic RCE 13 (0) | 2025.11.18 |
| Codeengn Basic RCE 20 (0) | 2025.11.17 |
| Codeengn Basic RCE 16 (1) | 2025.11.11 |