패커 분석 – UPX, PE 파일 구조

패커 분석  – UPX, PE 파일 구조

많은 패커들은 암호화된 코드를 복호화하고 초기 스택 주소로 되돌아 오는 특성을 가지고 있다(패커에서 만들었다기보다는 함수 호출 전 ESP 레지스터를 저장하는 운영체제 특성이다). 이러한 특성을 이용하여 초기의 ESP 레지스터에 대해 메모리 브레이크 포인트를 설정해, 자동적으로 프로그램을 실행하면 암호화된 코드를 해제를 완료한 후 OEP(실제 엔트리 포인트라 불린다. Original Entry Point)에 접근을 시도하기 때문에 일시 정지되는 경우가 많다. 이를 이용해 쉽게 패킹을 해제할 수 있는데, 이와 같은 방법 이외에도 공개된 패커에 대한 패킹을 해제할 수 있는 방법들도 많은 분석가들이 공유하고 있는 상황으로 이 분야 역시 뜨거운 감자라 할 수 있다.

프로그램 패킹의 흐름을 그림으로 보면 다음과 같다.

패커 분석 – UPX, PE 파일 구조

 

대부분의 패커는 위와 같은 구조를 가진다. 기존 프로그램의 전체를 암호화하여 기존 PE 파일 구조도 함께 변경하게 된다(PE 파일 구조는 실행에 필요한 요구 사항과 같아서 조건만 맞으면 실행하는 데는 지장이 없다). 즉 기존 파일을 암호화하고 암호화를 해제하기 위한 프로그램 코드를 삽입하게 된다. 패커의 기능에 따라 여기에 안티 디버깅 코드들도 함께 삽입되고, 코드를 암호화하면서 분석을 방해하는 코드도 함께 삽입하는 경우 파일 용량은 커지기도 하고, 그와 반대로 불필요한 공간을 제거하는 경우 파일의 용량을 줄일 수 있다.

그럼 여기에서 공개 패커 중 유명한 UPX를 이용해 진행해 보도록 하자. UPX를 이용할 경우 PE 파일 구조 중 불필요한 부분들을 삭제해 파일 용량을 대폭 줄일 수도 있다. 실제로도 UPX는 프로그램 보호보다 용량 축소에 더 초점이 맞추어진 도구로 안티 디버깅 기능은 존재하지 않아 분석용도로 유용하다. 그럼 UPX를 이용하여 패킹에 대해 이해해 보자.

UPX는 다음 사이트에서 다운로드 받을 수 있다.

http://upx.sourceforge.net/

UPX는 패킹과 언패킹 기능을 제공하는데, 이용 가능 기능 명령은 다음과 같다(옵션을 주지 않으면 기본적으로 패킹으로 진행된다).

-d: UPX를 이용하여 패킹한 프로그램 파일을 언패킹 한다.

-t: UPX를 이용하여 패킹이 가능한지 테스트를 진행한다.

-l: 패킹시 사이즈를 미리 확인해 볼 수 있다. 패킹 전 데이터와 비교 후 정보를 출력한다(실제 패킹을 하지는 않는다).

추가로 사용 가능한 옵션으로 –q 옵션을 추가하면, 정보를 출력하지 않을 수 있다. –qqq를 이용할 경우 아무런 메시지를 표시하지 않는다.

그럼 C:\Windows 디렉토리에 있는 노트패드(Notepad.exe)를 복사하여 UPX를 이용해 압축해 보자. 별다른 옵션 없이 UPX 실행파일 이름으로 기본 압축이 된다.

[그림] UPX를 이용한 패킹

그럼 PE 구조는 어떻게 변경되었을까? 앞서 2부에 사용하였던 PEiD 도구를 이용해 변경된 PE 파일 구조를 확인해 보자.

↓UPX 패킹 후 아래와 같이 PE 파일구조가 변경된다.

[그림] 패킹 코드가 추가되면서 PE 파일구조도 함께 변경한다

위 PEiD 정보로는 EP(Entry Point)는 0x00015330이며, EP가 존재하는 세션은 UPX1로 나와 있다. 이 정보는 정확하지 않지만 참고 용도로 알아두고 PEiD는 이 프로그램 파일이 UPX로 압축되어 있다고 탐지하였다. 이러한 정보는 분석가들에게 패킹 여부와 패킹을 해제하는 데 많은 도움을 주니, 유용하다. UPX는 EP는 PUSHAD로 시작하게 된다. 그 후 압축을 해제하고자 하는 UPX1 주소를 ESI 레지스터에 담고, UPX0 시작 주소를 EDI 레지스터에 담고 UPX0 주소는 스택에 저장하게 된다(실행 초기 UPX1 세션은 압축을 해제하는 내용을 가진 세션으로 이 값을 이용해 UPX0 세션의 내용을 변경하게 된다. 그리고 EBX 레지스터는 압축 해제 조건 분기에 이용될 4바이트의 값을 불러오는 용도로 이용된다).

그럼 UPX를 Ollydbg를 이용하여 분석을 진행해보자.

[그림] PUSHAD명령을 이용해 현재 모든 레지스터정보를 스택에 담는다

PEiD에서 알려준 EP에서 메모리 주소 0x01015330의 PUSHAD 명령은, 현재 CPU 레지스터 정보를 스택에 저장하고, 본인이 압축 해제에 이용할 영역들을 ESI, EDI 레지스터에 저장하게 된다. 그리고 0x01015352에서부터 압축 해제가 진행된다. 그럼 UPX의 언패킹 초기 로직을 확인해 보도록 하자.

// 내용을 1바이트 UPX0 세션에 입력하고, EBX레지스터를 통해 바이트값을 추가 입력할지를 확인한다.
01015348 > /8A06 MOV AL,BYTE PTR DS:[ESI] ß ESI 주소로부터 1바이트 정보를 가져온다.0101534A . |46 INC ESI ß 1바이트 만큼 주소위치를 증가시킨다.

0101534B . |8807 MOV BYTE PTR DS:[EDI],AL ß 압축을 해제된 데이터를 UPX0 세션에 1바이트 기록한다.

0101534D . |47 INC EDI ß 1바이트 만큼 주소위치를 증가시킨다.

0101534E > |01DB ADD EBX,EBX ß ADD 연산을 통해 값이 0이되면, 01015359 즉 다음 세션으로 이동한다.

// EBX레지스터에 압축 해제 조건용 데이터를 입력하는 로직으로, 4바이트 단위로 이용된다.

01015350 . |75 07 JNZ SHORT 01015359

01015352 > |8B1E MOV EBX,DWORD PTR DS:[ESI] ß ESI에서 가리키는 압축 해제 조건 변경정보인 4바이트를 EBX에 저장한다.

01015354 . |83EE FC SUB ESI,-4 ß 그리고 이후 사용을 위해 -4를 이용하여 다음 위치인 1011004h로 위치를 변경해 놓는다.

01015357 . |11DB ADC EBX,EBX ß UPX는 압축은 더하기 연산을 이용한다. ADC는 실제 ADD 명령과 비슷하다. 추가로 CF 플래그 값도 함께 더한다. EBX레지스터는 UPX에서 데이터 이용에 대한 조건으로 이용된다.

[그림] 4바이트값의 ADC 연산을 조건으로 이용한다

01015359 >^\72 ED JB SHORT 01015348 ß AL의 값이 이용할 영역이 더 있다면 조건 분기를 하지 않게 된다.

// …이후 ECX 레지스터에 데이터를 복사할 반복횟수를 입력하는 작업들이 계속 진행된다. 계속 Stepinto(F7키)로 따라가다 보면 다음과 같이 다시 실제 내용을 기입하는데, 해당 바이트가 추가입추가 이용될 때, 이 같이 사용된다.

[그림] 해당 바이트가 추가로 이용될 때 ECX레지스터를 이용한다

[실습] UPX 언패킹 분석

ECX 레지스터의 수만큼 반복하게 되므로, 끝나는 지점인 JMP 0101543eh에 브레이크 포인트(F2키)를 설정하고 실행(F9키)로 진행하자. 0100136ch까지 00만 채워진다. 그리고 다시 초기 위치로 오게 되고, ESI의 주소는 01011005 주소로부터 AL에 값을 저장하고 해당 값을 이용해 조금 전까지 입력한 0100136ch에 데이터를 채우게 된다. ECX 레지스터 값을 계산해 내기 위한 연산은 다소 복잡하지만, EBX에 4바이트는 암호화 데이터를 채우기 위한 조건으로 사용되고, 이용이 완료되면 이후 4바이트씩 이동하는 구조로 진행된다(계속 이와 같은 방식으로 UPX0세션을 복구한다).

이 책에서 UPX의 모든 코드를 복구하기까지 따라가기에는 내용이 너무 많아서, 초기 데이터에 대해서만 진행하였으니 이해해 주기 바라며, 처음 패킹을 해제해 본다면 다소 지루하더라도 UPX0세션을 비교해가며 패킹이 해제되는 것을 따라가보기 바란다. 조금 코드가 어렵다면, 계속 확인하다 보면 코드의 흐름이 눈에 들어올 것이다(역분석은 반복 학습이 중요하다. 숙달만이 살길이다).

조금 더 코드 흐름을 확인해 보면, 아래 2개의 JMP를 이용해, 데이터를 반복적으로 UPX0에 작성하고 있음을 확인할 수 있다.

[그림] UPX는 JMP 명령으로 실행 영역이 나누어진다.

UPX0과 UPX1 세션을 계속 비교해가면 값이 써지는 것을 확인하고, 연산 결과에 따른 조건 분기를 확인하길 바란다(계속 확인해 보면, EDI 레지스터는 기존 UPX0에서 UPX1 세션의 영역까지 넘어가게 되어, UPX1 세션도 수정하게 된다). 언패킹은 반복되는 내용이 많아 지루하므로, 0x01015402에 브레이크 포인트(F2키)를 설정하고, 위 그림의 JMP 명령의 브레이크 포인트는 삭제하고 실행하자. 그럼 이제 UPX 언패킹의 거의 끝인 IAT 코드 설정부분에 들어간다. 그리고 UPX0 세션에 작성되는 것을 확인할 수 있다. 추가로 0x0101547에 브레이크 포인트(F2키)를 설정하면, IAT가 설정이 완료되는 것을 확인할 수 있다.

[그림] EBX레지스터를 이용하여 IAT 정보를 설정한다

이제 언패킹이 완료되는 코드가 보일 것이다. UPX 실행 초기 PUSHAD를 통해 레지스터 정보를 저장하였다. UPX는 언패킹이 완료되었을 때 다시 POPAD를 이용하여 초기 레지스터로 복귀하게 된다. 따라서 IAT 설정 코드 이후에 확인되는 POPAD 명령이 있는 코드는 언패킹이 완료된 코드로, 해당 명령에서 브레이크 포인트(F2키)를 설정하면 쉽게 언패킹이 가능하다.

[그림] POPAD를 통해 UPX 언패킹이 완료되었음을 알 수 있다

위 위치에서 프로그램이 일시 정지하였다면, 아래 JMP 100739dh 이후 우리가 찾던 OEP(Orignal Entry Point)로 이동하게 된다(언패킹 후 찾아낸 OEP를 통해 PE 파일를 재구성하는 것은 잠시 후에 다룬다). 해당 OEP에 하드웨어 브레이크 포인트를 설정하면, 프로그램을 분석을 재시작해도 바로 OEP에서 분석을 시작할 수 있다.

[그림] 찾아낸 OEP에 하드웨어 브레이크 포인트를 설정하자.

스택 복귀 구조를 이용한 언패킹

우리는 2부 스택 구조를 배우면서 스택 공간의 낭비를 막기 위해 프로그램 내 함수를 호출하게 되면 EBP와 ESP를 통해 해당 함수에서 스택 주소를 지정하여 사용하고 함수 복귀시 스택을 정리한다는 것을 알고 있다. 이는 패킹에서 이용할 수 있는데, 패킹 역시 함수를 호출하여 언패킹을 하는 하나의 처리 함수로 볼 수 있다.

[그림] 함수 호출 완료시 함수 내 이용한 스택을 정리하게 된다

함수 호출 규약에 따라 스택 정리 시점과 방법은 약간 달라지지만, 함수 호출이 끝나면, 함수 내에서 이용한 스택을 정리한다는 점은 같다. 따라서 패킹에 사용된 함수가 완료되면, 함수에서 사용한 스택을 정리하고 기존 스택으로 되돌아 온다는 점을 이용하여, 이를 언패킹하는 데 이용할 수 있다. 물론 고급 패커들은 이러한 기능이 언패킹에 이용된다는 것을 알고 언패킹시 초기 스택주소를 이용하지 않도록 변경하지만, 이 초기 스택 주소을 이용한 언패킹은 많이 유용하다. UPX에서 역시 초기 스택에 이용된 ESP 레지스터에 하드웨어 브레이크 포인트를 접근시(Access)로 설정하고 프로그램을 시작하면 바로 위 POPAD에서 프로그램이 일시 정지 한다. 그 외 여러 패커에서도 이 스택 복귀 주소를 이용한 언패킹은 유용하므로 알아두기 바란다.

UPX 패킹된 프로그램을 Ollydbg로 열고, Step into(F7키)로 명령을 한번 실행하자. 그럼 ESP의 위위치가 변경되는 것을 알 수 있다(UPX 패커외 다른 패커라면 ESP 레지스터가 변경되는 시점). 그럼 레지스터 정보 창에서 ESP 레지스터에 마우스 우클릭한 후 Follow in Dump를 통해 메모리 창에 현재 ESP 레지스터정보를 표시하도록 하자.

[그림 24-21] 레지스터가 사용하는 메모리 영역으로 이동

위 내용을 진행하면, 덤프 창에 ESP 레지스터에 사용되는 메모리 영역이 나타나는데, 여기에 아래 그림과 같이 하드웨어 브레이크 포인트를 설정한다.

[그림] 해당 메모리 영역에 접근시 일시 정지 하도록 하드웨어 브레이크 포인트 설정

이후 프로그램을 실행(F9키)하면, 조금 전 확인한 POPAD에서 프로그램이 일시 정지하게 된다.

이후 OEP를 쉽게 확인할 수 있다. 이와 같은 방법은 언패킹뿐만 아니라 함수 처리를 완료하는 시점에 브레이크 포인트를 걸고자 할 때 이용해도 유용하다.

Facebook Comments

Leave A Reply

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.