많은 현대 프로그램들은 악성 행위로부터 자신을 보호하기 위해 실행 파일을 패킹하여 배포하곤 한다. 프로그램을 패킹, 또는 언패킹한다는 것은 무슨 뜻일까? 단순히 실행파일의 크기를 줄이기 위해 압축하는 컴프레싱(Compressing)과는 달리 패킹은 프로그램 분석을 어렵게 만들기 위한 목적을 가지고 있다. 이를 프로텍팅(Protecting)이라 하는데 프로그램이 분석되어 악의적으로 동작한다면 보안 사고를 초래할 수 있기 때문에 이 분석 자체를 어렵게하는 행위를 뜻한다. 이는 프로그램의 원본 실행 코드를 암호화하여 저장하고 이를 복호화하는 코드를 프로그램에 포함하는 방식으로 구현할 수 있다.
패커로 인해 패킹된 프로그램의 실행 코드는 실행 시점에 복호화되어 동작을 수행한다. 그렇기 때문에 패킹되지 않은 프로그램과 비교하면 실행 속도가 좀 느릴 수 있지만 보호가 필요한 경우 감수할 만한 사항일 것이다. 이렇게 암호화된 코드를 실행 시점에 다시 복호화하려면 프로그램에 포함된 복호화 코드가 프로그램의 진입점(Entry Point)이 되야 할 것이다. 그래서 프로그램이 메모리에 로드된 후 복호화 코드로 원본 실행 코드를 복호화, 메모리의 어느 공간에 저장하여 실행할 수 있기 때문이다. 이때 복호화 된 원본 실행 코드의 진입점이 OEP(Original Entry Point)가 되며 이를 찾는 것이 언패킹의 첫 번째 단계가 될 것이다.
OEP를 찾아서 압축 해제된 프로그램 코드를 덤프해야 하는 이유는 단순히 프로그램을 실행시키고 메모리에 올라간 코드를 덤프(메모리 덤프)한다고 정상적인 실행 파일로 동작하지는 않는다. 패커에 의해 프로그램이 패킹되면 PE 헤더에 명시된 진입점이 바뀌는데 이 진입점을 OEP로 바꿔줘야 프로그램이 올바른 곳(원본 실행 코드)에서 실행될 수 있기 때문이다. 이를 확인하기 위해 패킹 전, 후를 비교해볼 수 있는데 프로그램의 PE 구조를 파악하는 DIE(Detect It Easy)와 오픈소스 패커인 UPX를 사용해보았다.
먼저 UPX를 이용해 Reversing.kr의 첫 번째 문제인 Easy_CrackMe.exe를 패킹해보았다. 파일 크기가 약 3분의 1로 확연히 줄어듬으로써 컴프레싱이 잘 된 것을 확인할 수 있다. 그렇다면 패킹 전, 후 파일을 DIE로 비교해보면 어떨까?
패킹 전의 Easy_CrackMe.exe 파일은 스캔 결과 Microsoft Visual C/C++로 컴파일된 프로그램임을 확인할 수 있었다. 메모리 맵이나 기타 정보를 확인해도 멀쩡한 PE 파일임을 확인할 수 있는데 패킹된 파일은 어떨까?
가장 눈에 띄는 차이점은 진입점이 달라졌다는 것(0x00401188 -> 0x0040b9f0)과 섹션의 수가 줄어들었다는 것(4개 -> 3개), 그리고 컴파일러 정보 대신 패커 정보가 스캔되었다는 것이다. 진입점은 위에서 얘기한 것처럼 복호화 코드의 시작 부분으로 옮겨갔다고 하면 섹션에는 어떤 변화가 일어났을까? 이를 비교해보도록 하자.
먼저 패킹 전 파일에는 .text, .rdata, .data, .rsrc 등 일반적으로 PE 파일에서 볼 수 있는 섹션들이 존재하고 있다.
그러나 패킹 후 파일에는 그런 섹션들이 사라지고 UPX0, UPX1, .rsrc 라는 섹션들이 존재하고 있다. 특이하게 UPX0 섹션에는 아무런 데이터가 존재하지 않는데 이는 UPX 패커에 의해 패킹된 실행 코드가 복호화된 후 저장되는 공간이다. 재미있는 것은 IMAGE_NT_HEADERS 같은 PE 헤더를 살펴보면 패킹 전, 후 프로그램은 값은 다르지만 동일하게 PE 구조를 유지하고 있는 것을 확인할 수 있다.
이는 패커에 의해 패킹된 파일도 어쨌든 윈도우즈 운영체제에서 실행되어야 하는 PE 파일이기 때문에 패킹된 프로그램이 실행되려면 PE 구조를 망가뜨리지 않아야 하기 때문이다.
아무튼 이렇게 패커에 의해 변형된 파일을 복원하려면 여러가지 툴의 도움이 필요한데 시중에 나와있는 책이나 블로그에 기록된 자료들은 대부분 올리 디버거와 덤프 플러그인을 사용하고 있다. 그러나 이 글을 참고하면 x96dbg에 포함되어있는 Scylla 플러그인으로 쉽게 덤프를 뜰 수 있다. 그런데 언패킹된 프로그램을 덤프해서 실행시켜보면 다음과 같은 오류가 뜨는 것을 볼 수 있다.
DLL을 찾을 수 없어 프로그램을 실행할 수 없다는 것인데 이는 패킹된 프로그램을 덤프할 때 IAT, Import Address Table이 손상되었기 때문에 프로그램 실행에 필요한 DLL(kernel, user32 등)을 불러올 수 없어 발생하는 문제다. 대부분 DLL 방식을 사용하는 프로그램 특성 상 사용하는 함수 주소가 IAT에 적혀있지 않다면 이를 불러올 수 없기 때문에 단순히 덤프를 뜨는 것만으로는 단독 실행파일을 만들 수 없는 것이다. 그래서 원래 실행 파일에 저장된 IAT 정보를 새롭게 덤프된 파일에도 갱신(IAT 복구)해줘야 하는데 이 역시 x96dbg에서는 Scylla 플러그인으로 쉽게 수행할 수 있다.
실행 결과를 비교하면 위와 같은 것을 볼 수 있다. 덤프 및 복구된 파일은 원본 파일보다 크기가 줄어든 것이 특징이다.
[출처 | index-of.co.uk/Reverse-Engineering/Unpacking%20%5Bezbeat%5D.pdf]
[출처 | decompilation - Unpacking binaries in a generic way - Reverse Engineering Stack Exchange]
'리버싱 > 기초 지식' 카테고리의 다른 글
Study03 - 기초 지식(IAT) (0) | 2020.11.15 |
---|---|
Study 02 - 기초 지식(Register, Call Stack, Prologue/Epilogue) (0) | 2020.08.29 |
Study 01 - 기초 지식(PE) (0) | 2020.08.26 |