INTERTLUDE/리버싱 스터디

Codeengn Basic RCE 19

희원킴 2025. 10. 8. 00:46

Q. 이 프로그램은 몇 밀리세컨드 후에 종료되는가

 

해당 문제를 풀기 위해서, 먼저 19.exe 파일을 다운로드하였다.

DIE로 파일을 열었더니, 패킹이 되어 있다는 것을 알 수 있었다. 

그래서 upx-4.2.4-win64 폴더 내에서, CMD를 열고 언패킹 명령어를 입력하였다.

** 언패킹 명령어 :  upx -d -o (저장할 파일명) (원본 파일명) 

 

 

언패킹이 완료된 후, Immunity Debugger로 파일을 열어서 실행시켜 보면
아래와 같은 화면이 나타나는 것을 확인할 수 있었다.

 

반면, 파일을 더블 클릭하여서 일반적으로 실행했을 때는 아래와 같은 화면이 나타났다. 

이처럼, 19번 문제도 4번 문제와 마찬가지로
디버거 실행 시와 일반 실행 시 출력되는 문구가 다른 이유를 파악하기 위해 코드를 자세히 살펴보자. 

 

우클릭 → Search For → All intermodular calls 메뉴를 이용해 외부 함수 호출 목록을 살펴본 결과, 

IsDebuggerPresent 함수가 호출되고 있음을 확인하였다. 

 

해당 함수는 디버거 환경을 탐지하는 역할을 하며, TEST EAX, EAX 명령의 결과가 0이 아닐 경우(ZF=0),

JNZ 분기문(0040E969)으로 점프하는 구조로 되어 있었다.

디버깅 환경에서도 정상적으로 실행되도록 하기 위해 JNZ → JE 로 명령어를 변경하여 분기 방향을 반대로 바꾸었다.

이후 copy to executable → Selection → Save file 과정을 거쳐 패치 파일을 따로 생성하였다.

 

패치한 파일을 실행해 보면, 디버거 실행 시에도 오류 없이 정상적으로 작동하는 것을 확인할 수 있었다.

그렇다면 이제, Q. 이 프로그램은 몇 밀리세컨드 후에 종료되는가 문제에 대한 답을 구해보고자 한다. 

 

이제 프로그램의 종료 시점을 구하기 위해 코드를 살펴보았다.

다시 All intermodular calls 메뉴를 이용해 탐색해 보면, timeGetTime 함수가 호출되고 있음을 확인할 수 있다.

 

** timeGetTime 함수란? : Windows API에서 사용되는 함수로, 시스템 부팅 이후 경과된 시간을 밀리세컨드(ms) 단위로 반환한다. (즉, 현재 시스템 시간을 측정할 때 사용되는 함수이다.) 

이후, timeGetTime 함수에 Set breakpoint on every call to TimeGetTime 기능을 사용하여서, 

함수가 호출되는 모든 지점에 BP(BreakPoint)를 설정하였다.

위의 과정을 진행한 이후에, BP(BreakPoint)의 목록을 확인해 보았다. 

 

BP 목록을 확인한 뒤 프로그램을 실행(F2)해보면,
여러 곳에 BP가 걸려 있음에도 불구하고 00444C44: CALL EDI 한 곳에서만 멈추는 것을 확인할 수 있었다. 

분명 BP는 여러 개에 걸려 있는데, 왜 해당 부분에서만 멈추는 것일까? 코드를 조금 더 찬찬히 살펴보자. 

코드의 동작 흐름에 대해 살펴보자면, 

 

1) CALL EDI (00444C44) : 첫 번째 timeGetTime 호출

→ 현재 시스템의 시간을 밀리세컨드 단위로 반환하며, 이 값은 EAX 레지스터에 저장된다.
→ 이후 MOV ESI, EAX 명령을 통해 이 반환값을 ESI로 복사한다.
(즉, 프로그램 시작 시점의 시간을 ESI에 저장하는 것)

2) CMP BYTE PTR DS:[48E8D3], 0
→ [48E8D3] 메모리에 저장된 값과 0을 비교한다.
→ 이 비교 결과에 따라 특정 분기로 넘어갈 수도 있지만, 현재 실습에서는 이 분기를 우회했기 때문에 그대로 진행된다.

3) EBX에 [ESP+14] 값 저장

→  해당 명령을 통해 EBX는 프로그램 실행 중 스택에 저장된 구조체의 시작 주소를 참조하게 된다.
→ 이후 이 EBX는 나중에 기준 시간값([EBX+4])을 읽어올 때 사용된다.

4) Sleep 함수 호출
→ KERNEL32.Sleep 함수가 호출되어 잠시 대기 상태로 들어간다.
→ 이 대기 시간 이후 다시 timeGetTime을 호출하여 경과 시간을 측정한다.

5) 두 번째 timeGetTime 호출 (다시 CALL EDI)
→ Sleep 후 다시 timeGetTime()을 호출하여 현재 시각을 EAX에 저장한다.
→ 즉, 이 시점에서의 EAX는 “현재 시각”, ESI는 “시작 시각”이 된다.

6) CMP EAX, ESI → JNB 분기 (00444D38로 분기) 
→ 두 번째로 얻은 현재 시각(EAX)이, 첫 번째 시각(ESI) 보다 크거나 같다면 분기한다.

 

CMP EAX, ESI → JNB 분기 (00444D38로 분기) 한 이후의 연산 과정 

1) SUB EAX, ESI
→ 현재 시각(EAX)에서 시작 시각(ESI)을 빼서 실행 경과 시간을 구한다.
→ 연산 결과는 다시 EAX에 저장된다. (즉, EAX = EAX - ESI)

2) CMP EAX, DWORD PTR DS:[EBX+4]
→ 계산된 실행 경과 시간(EAX)과 메모리 [EBX+4]에 저장된 기준 시간값을 비교한다.

** [EBX] = 008AF878, [EBX+4] = 008AF87C → EBX가 가리키는 주소를 보면 [EBX+4] = 008AF87C

3) Hex Dump 확인
→ 해당 메모리 주소(008AF87C)를 헥스 덤프 창에서 확인하면 -> 70 2B 00 00 값이 저장되어 있음. 

→ 리틀엔디안(Little Endian)은 하위 바이트부터 메모리에 저장하는 방식이므로,
70 2B 00 00은 실제 값이 00 00 2B 70 임을 의미한다. 

4) 10진수 변환
→ 00 00 2B 70(16진수) → 2*4096 + 11*256 + 7*16 + 0 = 11120(10진수).
→ 즉, 프로그램에 설정된 기준 시간은 11120 ms이다.

 

Q. 이 프로그램은 몇 밀리세컨드 후에 종료되는가에 대한 답은, 11120ms (밀리세컨드) 임을 확인할 수 있다. 


** 분기 조건 확인

  • JNB 00444C71
    → “EAX(실행 시간)가 [EBX+4] 값(기준 시간) 보다 크거나 같으면 점프”
    → 즉, 프로그램 실행 시간이 11120ms 이상이면 자동 종료 루틴으로 진입한다.

따라서, 실제 프로그램은 실행된 지 약 11.12초가 경과하면 종료되는 구조임을 확인할 수 있다.


** 분명 BP는 여러 개에 걸려 있는데, 왜 해당 부분에서만 멈추는 것일까? 

-> 이는 프로그램이 timeGetTime을 직접 호출하지 않고, EDI 레지스터에 저장된 주소를 통해 간접적으로 호출하기 때문이다.

 

실행 중에는 다음과 같은 형태로 동작한다:

MOV EDI, [IAT(kernel32!timeGetTime)]
CALL EDI

따라서, 실제로 실행되는 호출 지점은 CALL EDI 한 곳뿐이다.