Notice
Recent Posts
Recent Comments
Link
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

Study

리버싱 핵심원리_15_UPX 실행 압축된 notepad 디버깅 본문

Reversing/리버싱 핵심원리

리버싱 핵심원리_15_UPX 실행 압축된 notepad 디버깅

마늘부추 2019. 2. 5. 03:08

15. UPX 실행 압축된 notepad 디버깅

 

15.1 압축코드 소개

 

 

파일을 키면 UPX EP 코드(1015330)가 나타난다.
이는 UPX1 영역에 속하는 주소이며 notepad 프로그램을 실행시키는 코드의 진입점이 아니라 원본 코드를 압축 해제하는 코드의 EP 코드이다.

 

※ 참고

① UPX는 압축된 원본 코드와 압축 해제 코드를 UPX1 섹션에 저장하고, UPX0 섹션에 압축을 풀어 원본 코드를 올린다는 것을 14장에서 배웠다.

② .rsrc 섹션은 압축이 되지 않고 원본 그대로 남아있는데, 그 이유는 파일의 아이콘, 이미지, IAT 관련 정보를 담고 있기 때문이다.

실행 이전에 필요한 리소스를 담고 있으므로 압축해서는 안 된다.

 

 

원본 코드를 복원시키는 코드는 UPX1의 마지막 부분에 있으며 1015330 PUSHAD 코드부터

 

 

원본 OEP 코드로 점프하는 10154BB JMP 0100739D 코드까지이다.

 

 

HxD로 보면 이정도 범위이다.

 

 

 

15.2.1 IAT영역 정리(로 추정)

 

 

PUSHAD 명령어는 EAX~EDI 레지스터의 값을 모두 스택에 저장해놓는 역할을 한다.

 

MOV ESI,1011000
10110000은 UPX1(Section 2)의 영역의 시작점이다. ESI의 값을 위와 같이 설정한다.

 

LEA EDI,DOWRD PTR DS:[ESI+FFFF0000]
EDI의 값을 1001000으로 설정한다. 이곳은 UPX0(Section 1)의 시작 지점이다.

위 두 명령어를 통해 ESI영역의 데이터(압축 데이터)가 EDI 영역에 압축 해제될 것을 예상할 수 있다.

 

PUSH EDI
OR EBP,FFFFFFFF
JMP SHORT 01015352
그 후 EBP를 의미없는 값으로 초기화 시킨 후 1015352 위치로 점프한다.

 

MOV EBX,DWORD PTR DS:[ESI]
1011000에 있는 922881DB 값을 EBX에 저장

 

SUB ESI,-4
ESI의 주소를 다음 주소로 옮긴다.
CF와 AF가 1로 변한다. 아직은 잘 모르겠지만 이게 다음 명령어에 영향을 미치는듯하다.

 

ADC EBX,EBX
ADC는 기본적으로 ADD와 같지만 차이는 Carry bit 값도 더한다는 뜻이다.

 

JB SHORT 01015348

1015348 주소로 점프한다.

 

※ 여기서 잠깐!

JA LABEL
Jump if Above의 약자로 CMP로 두 값을 비교했을 때 앞의 값이 크면 원하는 레이블로 점프하는 명령어
JB LABEL
Jump if Below의 약자로 CMP로 두 값을 비교했을 때 뒤의 값이 크면 원하는 레이블로 점프하는 명령어
JE LABEL
Jump if Equal의 약자로 CMP로 두 값을 비교했을 떼 두 값이 같으면 원하는 레이블로 점프하는 명령어

JA와 JB는 JE와 같이 쓸 수 있다(크거나 같으면, 작거나 같으면).

 

 

레지스터와 Dump Window를 천천히 구경하며 내리다 보면 처음 나타나는 루프가 있는데, 계속 00값을 EDI 위치에 복사한다.

후에 관찰해보니 이곳은 IAT 영역이었다. 지금 이 동작은 덮어씌울 값을 위해 공간을 정리하는 동작으로 추정된다.

 

1001001~100136B 주소까지 이 동작을 36B번 반복하여 덮어쓰므로 그냥 10153E6 주소에 BP 설치 후 넘긴다.

 

 

 

15.2.2 압축 해제 코드

 

 

너무 길고 점프문 때문에 보기가 복잡해서 캡처와 설명은 생략하지만 1015348~10153FD까지가 압축을 해제한 내용을 UPX0의 100136E부터 쓰는 과정이다.
10153FD에 브레이크 포인트를 걸고 충분히 반복하면서 Dump Window를 지켜보면

 

 

NULL로 가득차있던 UPX0 공간에

 

 

위와 같이 압축이 해제되어 원본 내용이 기록되는 것을 볼 수 있다.

이 또한 기다리려면 너무 오래 걸리므로 1015302에 브레이크 포인트를 건 후 넘긴다.

 

 

 

15.2.3 호출 코드 재지정

 

 

다음 반복문이다.


1015402~1015434는 눈으로 관찰해서는 무슨 역할을 하는 루프인지 잘 모르겠다.
다만 관찰한 바로는 EDI 레지스터가 가리키는 주소의 값이 E8 또는 E9일 때 어떠한 조건에 의해 그 뒤의 값이 새로운 바이트로 대체된다는 것이다. (그런데 E8, E9라고 해서 반드시 뒤의 데이터가 변하는 것은 아니었다.)

 

한참을 관찰 후 책을 다시 읽어보니 원본 코드의 CALL/JMP 명령어(op code : E8/E9)의 destination의 주소를 복원시켜주는 코드라고 설명되어 있다. 아마도 CALL과 같은 명령어는 API 호출의 경우가 많으므로 새로 로드된 메모리 주소에 맞게 재구성하는 것이 아닌가 생각된다.
JMP도 마찬가지.

 

 

 

15.2.4 IAT 재구성

 

 

다음 새로운 반복 루프이다.

 

LEA EDI,DWORD PTR DS:[ESI+13000]
현재 ESI 값 1001000에 13000을 더한 1014000주소를 EDI에 복사한다.
1014000은 UPX1의 뒷부분 영역이다.
이 위치를 Dump Window로 가보면 다음 사진과 같이 API 이름으로 추정되는 문자열들이 존재한다.

 

 

이를 통해 IAT 세팅 과정을 위한 루프임을 짐작할 수 있다.

 

MOV EAX,DWORD PTR DS:[EDI]
124 값을 EAX레지스터에 복사한다.

 

OR EAX,EAX
JE SHORT 0101547E
MOV EBX,DWORD PTR DS:[EDI+4]
LEA EAX,DWORD PTR DS:[EAX+ESI+1BE04]
EAX(124)+ESI(1001000)+1BE04=101CF28
이곳 역시 UPX1 영역이며 위치로 가보면 위와 같이 문자열들이 존재하는데

 

 

다만 DLL 이름이 존재한다.(뒤에는 바로 API도 나타난다)
DLL 이름들의 끝에는 친숙한 LoadLibraryA와 GetProcAddress 함수가 가장 먼저 나타난다.
아무튼 101CF28에는 KERNEL32.DLL이 존재하는데 이 위치를 EAX 레지스터에 복사한다.
리버싱 핵심원리 책의 171페이지에 IAT 입력 순서를 참고하니, Name 멤버를 읽는 동작인 것을 알 수 있었다.
아래 1015452 주소의 CALL DWORD PTR DS:[ESI+1BECC]는 아마도 LoadLibrary 함수일 것이다.

 

ADD EBX,ESI
EBX(8C)+ESI(1001000)=100108C
글의 앞부분에서 언급했듯 역시 1001000 초반대 주소는 IAT를 위한 공간이었다.

 

PUSH EAX
KERNEL32.DLL 문자열을 스택에 PUSH한다. 아래에 있는 CALL의 인자로 넘길 Name 멤버이다.
LoadLibrary("KERNEL32.DLL")

 

ADD EDI,8

 

 

EDI(1014000)에 8을 더한다. API 이름 문자열의 시작부분.

 

CALL DWORD PTR DS:[ESI+1BECC]

 

 

LoadLibrary 함수가 맞다.

 

 

라이브러리 함수의 주소를 반환하여 EAX에 저장한 모습.

 

XCHG EAX,EBP
EAX와 EBP의 값을 바꾼다.
KERNEL32.DLL의 시작주소를 EBP에 담아둔다.

 

MOV AL,BYTE PTR DS:[EDI]
INC EDI
EDI를 증가시켜 API명 시작주소를 담는다.

 

OR AL,AL
JE SHORT 0101543C
MOV ECX,EDI
PUSH EDI
API명을 스택에 저장한다.

 

DEC EAX
이것을 수행함으로써 EAX는 FFFFFF00이 된다. 이는 다음 명령어를 위한 준비인데

 

REPNE SCAS BYTE PTR ES:[EDI]

※ 이 명령어의 의미는
SCAS : AL/AX/EAX에 저장돼 있는 값과 ES:(E)DI가 가리키는 곳에 저장되어 있는 값을 비교한다.
여기서 검사할 범위는 BYTE이므로 AL과 EDI 위치 1BYTE를 비교한다.

REPNE : REP 계열의 명령어는 문자열 컨트롤에 자주 쓰이는 명령어 중 하나이다.
REP은 repeat의 약자로, ecx 레지스터와 함께 자주 사용되는 편이고 ecx>0인 동안 어떠한 행위를 처리한다.

위 DEC EAX로 인해 AL은 00이 되었으므로 REPNE로 인해 00을 만날 때까지 비교/진행하며 문자열을 인식하는 명령어인 것 같다.
실제로 명령어를 수행하면

 

 

이 위치에 있던 EDI값이

 

 

으로 변한다.

 

문자열의 개수가 따로 카운트되지 않는 것으로 보아 문자열을 세는 것은 아니고
다만 그 끝을 찾아내는 명령어인 것 같다.(확실하진 않으니 좀 더 공부 후 수정할 계획)

 

PUSH EBP
GetProcAddress 함수의 인자로 넘길 값을 저장한다.
KERNEL32.77160000

 

CALL DWORD PTR DS:[ESI+1BED0]

 

 

KERNEL32.77160000를 인자로 전달받은 해당 함수는 반환값으로 GetCurrentThreadId 함수의 시작 주소(771718E0)를 얻는다.

 

OR EAX,EAX
JE SHORT 01015478
조건이 맞으면 1015478 CALL 명령어로 분기한다. 이는 OEP 주소로 가는 명령어이다.
IAT가 전부 재구성된 후에 분기한다.


MOV DWORD PTR DS:[EBX],EAX
EBX(100108C)위치에 얻은 라이브러리 함수 시작주소를 기록한다.

 

 

아무것도 없다가

주소가 입력되었다.

 

ADD EBX,4
다음 함수 주소 기록을 위해 4byte 증가시킨다.

 

JMP SHORT 01015459
위 과정을 처음부터 반복한다.

 

CALL DWORD PTR DS:[ESI+1BEE0]

 

 

80크기만큼 사용했던 스택을 모두 0으로 초기화 후 OEP 코드로 가는 JMP 명령어를 실행한다.

 

 

 

 

15.3 UPX의 OEP 찾기

 

 

15.3.1 PUSHAD/POPAD

 

 

프로그램을 처음 OLLYDBG에 올렸을 때 나타나는 EP 코드와

 


OEP로 가기 직전의 코드이다.

 

PUSHAD와 POPAD는 레지스터들의 값을 스택에 모두 저장/스택에 저장된 값을 레지스터에 모두 저장
하는 서로 반대의 명령어다.
따라서 이는 압축 해제 코드의 시작과 끝을 나타내므로, OEP를 바로 찾아가고 싶다면


 

Code Window에서 우클릭 -> Search for -> all commands를 이용하여 POPAD를 검색하고
PUSHAD 바로 밑에 있는 POPAD를 찾아가면 된다.

 

 

 

15.3.2 하드웨어 BP


앞서 처음 PUSHAD 명령어를 실행하면 스택에 레지스터들의 값이 모두 저장된다고 했었다.
반대로 POPAD 명령어를 수행할 때 이 스택 메모리에 접근하여 값을 레지스터에 빼올 것을 예상할 수 있다.

 

그렇다면 결국 같은 메모리에 다시 한 번 접근하게 되는 셈인데 이점을 이용하여 메모리 위치에 BP를 설치하는 방법이다.

 

하드웨어 BP는 CPU에서 지원하는 브레이크 포인트이며 4개까지 설치 가능하다.

 

메모리에 BP가 걸렸다고 해서 그곳에 access 하는 순간 멈추는 것이 아니고
일단 그 BP에 접근하는 명령어가 끝까지 수행된 후 멈춘다.(걸렸다고 도중에 명령어가 중지되는 것은 아니다)

 

 

 

15.4 Comment

 

처음 봤을 때는 명령어가 어색하고 무슨 루프라고 설명을 해줘도 전혀 이해하지 못했는데

대략 20장까지 공부 후 다시 한번 보니 반복문과 그에 따른 목적이 조금은 눈에 보이게 되었다.

 

PE 포맷에 익숙해지고 PEview를 잘 볼 줄 알게 되면 한결 보기가 편할 것 같다.

 

필수로 분석해볼 것까지야 없다고 생각하지만 압축 해제 매커니즘과 코드 이해도 향상에 도움이 될 것이라고 생각한다.