정현진Jeong Hyeonjin

정현진 — 보안 포트폴리오

Research / 리버싱 / 코드 케이브로 Nag 창 없애기, 일련번호 생성 로직 따라가기

코드 케이브로 Nag 창 없애기, 일련번호 생성 로직 따라가기

유형
연구 기록
분류
리버싱
발행
2025. 12. 29.
분량
2분
종류
연구 기록

세 번 뜨는 창 중 두 번째만 남기도록 빈 코드 영역에 카운터를 넣었고, 일련번호 반복문은 명령 하나씩 레지스터 값을 따라가며 계산했습니다.

리버싱 입문서 4장을 공부하면서 Lena 강의 15강·17강 예제를 풀었습니다. 15강은 원치 않는 안내 창(Nag)을 없애는 문제, 17강은 아이디로 일련번호를 만드는 로직을 이해하는 문제입니다.

Nag 창 없애기 (Lena 15강)

실행하면 "I am the starting nag. Remove me" 창이 먼저 뜨고, 메인 프로그램을 거쳐 Exit를 누르면 "I'm the closing nag" 창이 또 뜹니다. 시작·종료 Nag 두 개를 없애야 합니다.

콜 스택으로 호출 위치 찾기

콜 스택은 함수들이 어떤 순서로 호출됐는지 추적하는 정보입니다. Nag 창이 떠 있을 때 프로그램을 멈추고 콜 스택을 보면, 창을 띄운 함수까지 거슬러 갈 수 있습니다.

  • 콜 스택 창의 [Called from]은 호출한 코드, [Procedure]는 호출된 함수입니다.
  • 콜 스택 창에 안 보이면 스택 영역에서 빨간색으로 표시된 복귀 주소를 따라갑니다.
  • 스택에서 ReverseMe.NAGs.exe 모듈의 복귀 주소를 찾아 Enter로 코드로 넘어가면, 바로 위에서 창을 띄우는 함수를 호출합니다.

분기 구조

  • 00420377 TEST EAX, EAX: EAX가 0이면 ZF=1
  • 00420379: ZF=1이면 004203BA로 건너뛰고, 아니면 0042039A에서 창을 여는 함수 호출

이 코드가 세 번 실행되고 창도 세 번 열립니다. 첫 번째와 세 번째가 Nag, 두 번째가 메인 창이었습니다. 그래서 몇 번째 호출인지 세서 두 번째만 통과시키기로 했습니다.

코드 케이브

원래 자리에는 카운터 로직을 넣을 공간이 없습니다. 코드 케이브는 쓰이지 않는 빈 코드 영역에 새 코드를 넣고, 원래 자리에서는 그쪽으로 점프만 하는 방식입니다.

  1. 수정할 명령을 빈 영역으로 가는 JMP로 바꿈
  2. 빈 영역에 새 코드를 넣음
  3. 새 코드 끝에서 원래 이어질 명령으로 돌아오는 JMP

코드와 데이터를 어디에 넣을지는 메모리 맵에서 정했습니다.

영역 접근 권한 넣을 것
.text R E (읽기·실행) 새 코드
.data R W (읽기·쓰기) 카운터 값

코드 영역은 0040100000438000, 데이터 영역은 0044200000447000이었습니다. 0이 이어지는 빈 곳 중 코드는 00437D6A, 데이터는 00445E90을 골랐습니다.

넣은 코드

00420377의 TEST EAX, EAX를 JMP 00437D6A로 바꿨습니다. ("Keep size"는 원래 명령 크기를 넘지 않게, "Fill rest with NOPs"는 남는 자리를 NOP으로 채우는 옵션입니다.)

  1. ADD BYTE PTR DS:[445E90], AL — 창을 띄울 때만 EAX가 1이라서, 호출 횟수가 카운트됩니다.
  2. 카운터가 2인지 비교
  3. 2가 아니면(첫 번째·세 번째) 창을 띄우지 않는 004203BA로 점프
  4. JMP로 덮어쓰면서 사라진 원래 명령을 다시 실행
  5. 두 번째면 원래의 창 띄우는 로직으로 점프

파일로 저장

디버거에서 고친 내용은 다시 실행하면 사라집니다. [Edit] → [Copy all modifications to executable] → 새 창에서 [Save file]로 저장했고, 실행하자 Nag 없이 메인 창만 떴습니다.

일련번호 생성 로직 (Lena 17강)

아이디와 일련번호를 넣고 Check를 누르면 맞는지 확인합니다. 일련번호는 아이디로 계산됩니다.

구조

  • 004012FB~`00401302: lstrlen()`으로 아이디 길이를 구해 ECX(반복 횟수)에 넣음
  • 00401309~`00401336`: 반복문으로 일련번호 계산, 결과는 ESI
  • 00401336~`0040133C`: ESI와 입력한 일련번호 비교

반복문 한 바퀴 따라가기

아이디 "Lena15", 일련번호 "1234"로 실행했습니다. 아이디는 00403038에 저장되고 길이 6이 ECX에 들어갑니다. 첫 바퀴에서 첫 글자의 값은 0x6C였습니다.

명령 결과
MOV DL, [EAX+403037] 한 글자를 DL로
AND EDX, 0FF 하위 1바이트만 남김 → 6C
MOV EBX, EDX 6C
IMUL EBX, EDX 6C × 6C = 2D90
ADD ESI, EBX ESI = 2D90
MOV EBX, EDX 6C
SAR EBX, 1 오른쪽으로 1비트 → 36
ADD EBX, 3 39
IMUL EBX, EDX 39 × 6C = 180C
SUB EBX, EDX 17A0
ADD ESI, EBX 4530
ADD ESI, ESI 8A60
INC EAX / DEC ECX 다음 글자로, 남은 횟수 1 감소
JNE 00401309 ECX가 0이 아니면 반복

IMUL은 부호 있는 곱셈, MUL은 부호 없는 곱셈입니다. SAR은 부호를 유지하며 오른쪽으로 미는 시프트입니다.

6바퀴를 돌면 ESI에 0016BC4E가 남았습니다.

계산된 값 꺼내 보기

비교 직후 JNE를 코드 케이브로 가는 점프로 바꾸고, 케이브에서 MOV DWORD PTR DS:[403148], ESI로 계산된 일련번호를 메모리에 저장한 뒤 원래 분기로 돌아가게 했습니다. 실행 후 00403138에는 내가 넣은 값, 00403148에는 프로그램이 만든 값이 나란히 들어가 비교할 수 있었습니다.

정리

  • 창을 띄우는 함수는 콜 스택과 복귀 주소로 거슬러 찾았습니다.
  • 공간이 부족한 패치는 코드 케이브로 넣고, 덮어쓴 원래 명령을 꼭 다시 실행해야 합니다.
  • 반복 계산은 한 바퀴만 레지스터 값을 표로 따라가 보면 구조가 보입니다.

0 백링크0 링크3 태그