HelloWorld.exe 실행 파일을 디버깅(Debugging)하여 어셈블리 언어로 변환된 main() 함수를 찾아 기본적인 디버거의 사용법과 어셈블리 명령어에 대한 공부
x32dbg
로 열어줍니다.본 문에서는 32bit OS를 이용하여 OllyDbg 프로그램을 사용하였지만, 64bit OS에서는 실행이 되지 않기 때문에 x32dbg 프로그램에 예제 파일을 사용하여 실습합니다.
메인 화면 구성
Code Window | 기본적으로 disassembly code를 표시하여 각종 comment, label을 보여주며, 코드를 분석하여 loop, jump 위치 등의 정보를 표시합니다. |
---|---|
Register Window | CPU register 값을 실시간으로 표시하며 특정 register들은 수정도 가능합니다. |
Dump Window | 프로세스에서 원하는 memory 주소 위치를 Hex와 ASCII/유니코드 값으로 표시하고 수정도 가능합니다. |
Stack Window | ESP register가 가리키는 프로세스 stack memory를 실시간으로 표시하고 수정도 가능합니다. |
EP(EntryPoint)란 Windows 실행 파일(EXE, DLL, SYS 등)의 코드 시작점을 의미합니다. 프로그램이 실행될 때 CPU에 의해 가장 먼저 실행되는 코드 시작 위치입니다.
책의 32bit OS를 예시로 들겠습니다.
Address | Instruction | Disassembled code | comment |
---|---|---|---|
004011A0 | E8 67150000 | CALL 0040270C | 0040270C (40270C 주소의 함수를 호출) |
004011A5 | E9 A5FEFFFF | JMP 0040104F | 0040104F (0040104F 주소로 점프) |
Address | 프로세스의 가상 메모리(Virtual Address:VA) 내의 주소 |
---|---|
Instruction | IA32(or x86) CPU 명령어 |
Disassembled code | OP code를 보기 쉽게 어셈블리로 변환한 코드 |
comment | 디버거에서 추가한 주석(옵션에 따라 약간씩 다르게 보입니다.) |
40270C 주소의 함수를 호출(CALL)한 후 40104F 주소로 점프(JMP)하라
"x32dbg/x64dbg의 기본 명령어 사용법
명령어 | 단축키 | 설명 |
---|---|---|
Restart | [Ctrl+F2] | 다시 처음부터 디버깅 시작(디버깅을 당하는 프로세스를 종료하고 재실행) |
Step Into | [F7] | 하나의 OP code 실행(CALL 명령을 만나면, 그 함수 코드 내부로 따라 들어감) |
Step Over | [F8] | 하나의 OP code 실행(CALL 명령을 만나면, 따라 들어가지 않고 그냥 함수 자체를 실행) |
Execute till Return | [Ctrl+F9] | 함수 코드 내에서 RET 명령어까지 실행(함수 탈출 목적) |
EP 코드의 4011A0 주소에서 Step Into[F7]
명령어를 이용해 40270C 함수 안으로 따라갈 수 있습니다.
Execute till Return[Ctrl+F9]
입니다.[F7] 명령으로 한 줄씩 내려오다보면 401056 주소의 CALL 402524 함수 호출 명령어를 만납니다.
마찬가지로 [F7] 명령을 통해 함수 내부로 들어가봅니다.
하지만 main() 함수라 보기에는 어려운 것을 알 수 있습니다. MessageBox() API 호출 코드
가 보이지 않기 때문입니다.
[Ctrl+F9] 명령어를 사용하여 RETN 명령어 위치까지 디버깅을 진행하고, [F7] 또는 [F8] 명령을 통해 402524 함수를 탈출합니다.
위와 같은 방식으로 내려가면서 함수 호출을 만나면 함수 내부로 들어가보고 main() 함수가 아니라면 해당 함수를 탈출해 디버깅을 진행합니다. (함수 내부에 반복문이 존재하면 함수 탈출을 진행할 때 시간이 오래걸릴 수 있습니다.)
디버깅을 하며 내려가다보면 401144 주소의 CALL 401000 명령어를 확인할 수 있습니다. [F7] 명령으로 함수 내부로 들어갑니다.
MessageBoxW() API를 호출하는 코드
와 함께 API의 파라미터인 "www.reversecore.com"과 "Hello World!"
문자열이 확인됩니다.
이 코드와 문자열은 HelloWorld.cpp의 소스코드 내용과 일치하기 때문에 401000 함수가 main() 함수입니다.
추가적인 디버깅 명령어 사용 방법입니다.
명령어 | 단축키 | 설명 |
---|---|---|
Go to | Ctrl+G | 원하는 주소로 이동(코드/메모리를 확인할 때 사용. 실행되는 것은 아님) |
Execute till Cursor | F4 | cursor 위치까지 실행(디버깅하고 싶은 주소까지 바로 갈 수 있음) |
Comment | ; | Comment 추가 |
User-defined comment | 마우스 우측 메뉴 Search for User-defined comment | |
Label | : | Label 추가 |
User-defined label | 마우스 우측 메뉴 Search for User-defined label | |
Set/Reset BreakPoint | F2 | BP 설정/해제 |
Run | F9 | 실행(BP가 걸려있으면 그 곳에서 실행이 정지됨) |
Show the current EIP | * | 현재 EIP 위치를 보여줍니다. |
Show the previous Cursor | - | 직전 커서 위치를 다시 보여줍니다. |
Preview CALL/JMP address | Enter | 커서가 CALL/JMP 등의 명령어에 위치해 있다면, 해당 주소를 따라가서 보여줌(실행되는 것이 아님. 간단히 함수 내용을 확인할 때 유용함) |
디버거를 재실행할 때마다 처음(EP) 코드부터 새로 시작하기 때문에 불편합니다. 그래서 대부분 디버깅을 진행하면서 중요한 포인트(주소)를 지정해 놓은 후 그 포인트로 빠르게 갈 수 있는 방법을 기록해둡니다.
1) Goto 명령
Go to 다이얼로그에 주소를 입력해두면 Go to(Ctrl+G)
명령으로 이동할 수 있습니다.
Execute till cursor[F4] 명령
으로 해당 커서가 놓인 주소까지 실행하고 40104F 주소부터 편하게 디버깅을 진행합니다. (공통)
2) BP 설치
BP(Break Point)를 [F2] 키
를 이용해 설치할 수 있습니다. (가장 많이 사용되는 방법입니다.)
디버거는 현재 실행 위치에서부터 프로세스를 실행하다가 BP가 걸린 곳에서 멈추게 됩니다.
메인 메뉴의 View - BreakPoints를 선택(Alt+B)하면 BreakPoints 목록을 확인할 수 있습니다.
3) 주석
프로그래밍에서와 마찬가지로 디버깅에서도 주석은 매우 중요합니다. [;] 단축키로 주석(Comment)을 달고, 아무 주소에서 마우스 우측 메뉴 Search for - User - defined comment 항목을 선택하면 사용자가 입력한 주석이 표시됩니다.
해당 주석을 더블 클릭하면 그 주소로 이동할 수 있습니다.
4) 레이블
레이블(Label)은 원하는 주소에 특정 이름을 붙여주는 유용한 기능입니다.
원하는 주소에 커서를 위치하여 단축키 [:]를 이용해 레이블을 입력하면 주소를 해당 레이블로 표시합니다. 마찬가지로 원하는 레이블을 더블 클릭하면 해당 주소로 이동합니다.
디버깅을 수행하다보면 알 수 있듯이 실행 파일의 EP 주소에 바로 main() 함수가 나타나는 것이 아니고 사용한 개발 도구의 Stub Code가 나타납니다. 찾고자하는 main() 함수는 EP 코드로부터 한참 떨어져 있습니다.
이런 main() 함수를 바로 찾아갈 수 있는 방법이 있다면, 디버깅에 큰 도움이 될 것입니다.
main() 함수의 MessageBox() 함수 호출 코드를 찾고자 한다고 가정합니다. 실행 파일을 디버깅(Step Over[F8]) 하다보면 언젠가는 MessageBox() 함수가 실행되어 메시지 박스가 출력될 것입니다.
이것을 코드 실행 방법의 원리
라고 합니다. 프로그램의 기능이 명확한 경우에 명령어를 하나하나 실행하면서 원하는 위치를 찾아가는 것입니다. 이 방법은 코드의 크기가 작고 기능이 명확한 경우에 사용할 수 있습니다. 코드의 크기가 크고 복잡한 경우에는 적절하지 않습니다.
특정 주소에서 MessageBox() API 호출 코드가 보이고 그 함수의 다른 주소에 PUSH 명령어를 확인할 수 있습니다. PUSH 명령어에는 메시지 박스에 표시된 문자열을 스택(Stack)에 저장하여 MessageBoxW() 함수에 파라미터로 전달하고 있습니다.
Win32 응용 프로그램에서는 API 함수의 파라미터를 스택을 이용해서 전달합니다. VC++을 사용하면 기본 문자열은 유니코드가 되고, 문자열 처리 API 함수들도 전부 유니코드 계열의 함수로 변경됩니다.
마우스 우측 메뉴 - Search for - All referenced text strings
Olldbg는 디버깅할 프로그램을 처음 로딩할 때 사전 분석 과정을 거칩니다. 프로세스 메모리르르 쭉 훑어서 참조되는 문자열과 호출되는 API들을 뽑아내서 따로 목록으로 정리를 해놓습니다.
All referenced text strings
명령을 사용하면 프로그램 코드에서 참조되는 문자열들을 보여줍니다.
문자열 Hell World!나 www,reversecore,com 을 더블 클릭하면 main() 함수의 MessageBoxW() 호출 코드로 바로 이동할 수 있습니다.
VC++에서는 static 문자열을 기본적으로 유니코드(UNICODE) 형식으로 저장합니다. static 문자열이란 프로그램 내부에 하드코딩(Hard Coding) 되어 있는 문자열을 의미합니다.
여기서는 일단 코드와 데이터 영역이 서로 나뉘어져 있다는 것
만 기억해줍니다.
마우스 우측 메뉴 - Search for - All intermodular calls
Windows 프로그래밍에서 모니터 화면에 뭔가를 출력하려면 Win32 API를 사용하여 OS에게 화면 출력을 요청해야 합니다. 즉 프로그램이 화면에 뭔가를 출력했다는 얘기는 프로그램 내부에서 Win32 API를 사용하였다는 뜻입니다.
이 때 프로그램의 기능을 보고 사용되었을 법한 Win32 API 호출을 예상하여, 그 부분을 찾을 수 있다면 디버깅이 매우 간편해질 것입니다.
위에서 사용한 실행 파일에서는 메시지 박스를 출력하기 때문에 분명 user32.MessageBoxW() API를 사용하였을 것입니다.
Olldbg의 사전 분석 기능 중에는 문자열 뿐만 아니라 사용되는 API 함수 목록을 뽑아내는 기능이 있습니다.
코드에서 사용된 API 호출 목록만 보고 싶을 때는 All intermodular calls
명령을 사용하면 됩니다.
더블 클릭하면 해당 주소로 이동할 수 있습니다. 이런 식으로 코드에서 사용된 API를 예상할 수 있을 때 이 방법을 사용하면 쉽게 원하는 부분을 찾아낼 수 있습니다.
마우스 우측 메뉴 - Search for - Name in all modules
Olldbg가 모든 실행 파일에 대해서 API 함수 호출 목록을 추출할 수 있는 것은 아닙니다.
Packer/Protector를 사용하여 실행 파일을 압축(또는 보호)해버리면, 파일 구조가 변경되여 Ollydbg에서 API 호출 목록을 볼 수 없습니다. 심지어 디버깅 자체가 매우 어려워집니다.
이런 경우에는 프로세스 메모리에 로딩된 라이브러리(DLL 코드)에 직접 BP를 걸어 볼 수 있습니다. API라는 것은 OS에서 제공한 함수이고, 실제로 API는 c드라이브 Windows\system32 폴더에 *.dll 파일 내부에 구현되어 있습니다.
간단히 말하자면 우리가 만든 프로그램이 어떤 의미 있는 일(각종 I/O)을 하려면 반드시 OS에서 제공된 API를 사용해서 OS에게 요청해야 하고, 그 API가 실제 구현된 시스템 DLL 파일들은 우리 프로그램의 프로세스 메모리에 로딩되어야 합니다.
Olldbg에서 확인하려면 View - Memory 메뉴(단축키 Alt+M)
를 선택합니다.
Olldbg의 또 다른 기본 해석 기능은 프로세스 실행을 위해서 같이 로딩된 시스템 DLL 파일이 제공하는 모든 API 목록을 보여주는 것입니다.
Name in all modules
명령을 사용합니다. 나타나는 창에서 Name으로 정렬시키고 MessageBoxW를 타이핑하면 자동 검색 됩니다.
USER32 모듈에서 Export type의 MessageBoxW 함수를 더블 클릭하면 USER32.dll에 구현된 실제 MessageBoxW 함수가 나타납니다.
주소를 보면 HelloWorld.exe 실행 파일에서 사용되는 주소와 확연히 틀리다는 것을 알 수 있습니다. 이 곳에 BP를 설치[F2]하고 실행[F9]해봅니다.
(만약 HelloWorld.exe 프로그램에서 MessageBoxW() API를 호출한다면 결국 이 곳에서 실행이 멈추게 되는 간단한 원리입니다.)
예상대로 MessageBoxW 코드 시작에 설치한 BP에서 실행이 멈췄습니다. 레지스터(Register) 창의 ESP 값이 나오는데 이것이 프로세스 스택의 주소입니다. 아래는 스택 윈도우입니다.
ESP의 값 19FF18에 있는 리턴 주소 401014는 HelloWorld.exe의 main 함수 내의 MessageBoxW 함수 호출 바로 다음의 코드입니다.
실제 main 함수에서 RETN 명령어에서 디버깅이 멈추고 Step Into[F7] or Step Out[F8] 명령으로 빠져나가면 리턴 주소 401014로 갈 수 있습니다.
리버싱에서 패치는 빼놓을 수 없는 아주 중요한 주제입니다. 패치 기술을 이용하여 기존 응용 프로그램의 버그를 수정하거나 또는 새로운 기능을 추가시킬 수도 있습니다.
패치 대상은 파일 혹은 메모리가 될 수 있으며, 프로그램의 코드와 데이터 모두 패치가 가능합니다. 실습 목표는 HelloWorld.exe의 메시지 박스에 표시되는 "Hello World!" 문자열을 다른 문자열로 패치하는 것입니다.
앞에서 이미 MessageBoxW의 호출하는 부분을 찾았고 패치시킬 "Hello World" 문자열의 주소도 찾아냈습니다.
우선 main() 함수의 주소 401000에 BP를 설정[F2]하고 실행[F9]합니다.
가장 쉬운 두 가지 방법입니다.
1. 문자열 버퍼를 직접 수정
2. 다른 메모리 영역에 새로운 문자열을 생성하여 전달
1) 문자열 버퍼를 직접 수정
MessageBoxW 함수의 전달인자 4092A0의 문자열("Hello World!") 버퍼를 직접 수정하는 방법입니다.
덤프 창에서 Go to 명령[Ctrl+G]으로 4092A0 주소로 가서 [Ctrl+E] 단축키로 Edit 다이얼로그를 띄웁니다.
"Hello World!" 유니코드(UniCode) 문자열이 차지하는 영역은 4092A0~4092B9입니다. 유니코드는 알파벳 한 글자당 2바이트가 필요합니다. 이 영역을 새로운 문자열로 덮어쓸 예정입니다.
만약 원본 문자열보다 큰 문자열을 덮어쓸 때는 그 뒤의 데이터를 훼손하지 않도록 매우 조심해야 합니다. 만약 뒤에 매우 중요한 데이터가 있다면, 프로그램에서 메모리 참조 에러가 발생할 수 있습니다.
위와 같이 Edit 다이얼로그의 유니코드 항목에 "Hello Reversing" 문자열을 입력합니다.
유니코드의 문자열은 2바이트 크기의 NULL로 끝나야 한다는 것을 주의해야합니다.
유니코드 항목에서는 NULL을 입력할 수 없기 때문에 밑의 Hex 항목에서 2바이트 크기의 NULL(00 00)
을 입력합니다.
다시 main() 함수를 보면 명령어와 파라미터의 주소는 그대로이지만 MessageBoxW() 함수에 전달되는 파라미터의 내용(문자열) 자체가 변경되었습니다.
[F9] 키를 눌러 실행시켜보면 메시지 박스에 패치된 문자열이 확인됩니다. 이런 문자열 버퍼 내용을 직접 수정하는 방법의 장점은 사용하기에 가장 간단하다는 것입니다.
단점은 기존 문자열 버퍼 크기 이상의 문자를 입력하기 어렵다는 제약 조건입니다.
일반적으로 실행 파일에서 사용되는 문자열들은 실제 크기보다 조금 여유 있게 저장되어 있습니다. 운이 좋다면 문자열 뒤쪽에 있는 버퍼를 조금 침범하더라도 프로그램 실행에 문제가 없는 경우도 있습니다. 하지만 권장하는 방법은 아니며 이런 불안 요소들이 계속 쌓이면 전체 시스템의 안정성이 떨어집니다.
위에서 수행한 패치 작업은 메모리에 임시적으로 한 것이라서 디버거가 종료되면 패치했던 내용은 사라집니다. 그렇기 때문에 변경한 내용을 영구히 보존하려면 별도의 실행 파일로 저장해야 합니다.
dump 창에서 변경된 내용("Hello Reversing" 문자열)을 선택하여 마우스 우측 버튼 - Copy to executable file 메뉴
를 선택하면 Hex 창이 나타납니다.
여기서 다시 마우스 우측 버튼 - Save file 메뉴
를 선택하여 파일 이름을 지정하고 저장하면 완료됩니다.
2) 다른 메모리 영역에 새로운 문자열을 생성
만약 원본 문자열보다 더 긴 문자열("Hello Reversing World!")로 패치해야 한다면 문자열 버퍼를 직접 수정하는 방법은 잘 맞지 않습니다.
디버거를 재실행([Ctrl+F2])하고 main() 함수 401000으로 이동합니다.
401007 주소의 PUSH 4092A0 명령은 (MessageBoxW() 함수 호출을 위해서) 4092A0 주소의 "Hello World!" 문자열을 파라미터로 전달하고 있습니다.
MessageBoxW() 함수는 파라미터로 입력된 주소의 문자열을 출력해주기 때문에, 만약 이 문자열 주소를 변경해서 전달하면 메시지 박스에는 변경된 문자열이 출력될 것입니다.
즉 적당한 메모리 영역에 패치하고자 하는 긴 문자열을 적어놓고 MessageBoxW() 함수에게 그 주소를 파라미터로 넘겨주는 것입니다. 전혀 다른 문자열 주소를 넘겨준다고 말할 수 있습니다.
여기서 고려해야 할 점은 '메모리 어느 영역에 문자열을 써야 하는지' 입니다. 이 것은 실행 파일 형식(PE File Format)과 가상 메모리(Virtual Address) 구조를 알고 있어야 합니다. 이번에는 임의로 적절한 영역을 선택합니다.
위의 방법 1)에서 수정한 문자열 주소는 4092A0입니다. 이 부분을 다시 dump 창으로 열어 스크롤을 밑으로 내리다 보면 해당 메모리 영역이 NULL padding
영역으로 끝나는 것을 볼 수 있습니다. 이곳은 프로그램에서 사용되지 않는 NULL padding 영역입니다.
프로그램이 메모리에 로딩될 때 최소 기본 단위(보통 1000)가 있습니다. 비록 프로그램 내에서는 메모리를 100 크기만큼만 사용한다고 해도 실제로 메모리에 로딩될 때는 최소 기본 단위인 1000만큼의 크기가 잡히는 것입니다. 나머지 크기의 사용되지 않는 영역은 그냥 NULL로 채워집니다.
NULL padding 영역을 문자열 버퍼로 사용하여 MessageBoxW 함수에 넘겨주면 될 것 같습니다. 적당한 위치(409F50)에 패치 문자열("Hello Reversing World!!!")를 써줍니다.
버퍼를 새로 구성하였습니다. 이제 MessageBoxW() 함수에게 새로운 버퍼 주소(409F50)를 파라미터로 전달해야 합니다.
그러기 위해서는 코드를 수정해야 하는데, Code 창에서 Assemble 명령을 사용해서 코드를 수정하겠습니다.
커서를 401007 주소 위치에 넣고 Assemble 명령(단축키 [Space])을 주면 Assemble 창이 나타납니다.
Assemble 창에 'PUSH 409F50' 명령어를 입력하고 실행[F9]하면 패치된 문자열로 결과가 나타납니다. 긴 문자열을 패치하는 방법입니다.
Assemble 창에서는 사용자가 원하는 어셈블리 명령어를 입력할 수 있습니다. 입력하는 즉시 코드에 반영되며 실행도 가능합니다. 디버깅의 강력한 기능 중의 하나가 실행 중인 프로세스의 코드는 동적으로 패치할 수 있다는 점입니다.
그러나 위의 수정된 코드를 파일로 만들면 제대로 동작하지 않을 것입니다. 409F50 메모리 주소 때문입니다. 실행 파일이 메모리에 로딩되어 프로세스로 실행될 때 파일이 그대로 메모리로 로딩되는 것이 아니라, 어떤 규칙에 의해서 올라가게 됩니다.
그 과정에서 프로세스 메모리가 존재하는데, 그에 해당하는 파일 옵셋(offset)이 존재하지 않는 경우가 많습니다. 이번 경우도 메모리 409F50에 대응하는 파일 offset이 존재하지 않습니다. 이를 정확히 이해하기 위해서는 PE File Format
을 알아야 합니다.
명령어 | 단축키 | 설명 |
---|---|---|
Step Into | F7 | CALL 명령을 만나면 그 함수 코드 내부로 따라 들어감 |
Step Over | F8 | CALL 명령을 만나면 따라 들어가지 않고 그냥 함수 자체를 실행함 |
Restart | Ctrl+F2 | 다시 처음부터 디버깅 시작 |
Go to | Ctrl+G | 원하는 주소를 찾아감 |
Run | F9 | 실행(BP가 걸려있으면 그 곳에서 실행이 정지됨) |
Execute till return | Ctrl+F9 | 함수 코드 내에서 RETN 명령어까지 실행 |
Execute till cursor | F4 | cursor 위치까지 실행함 |
Comment | ; | Comment 추가 |
User-defined comment | 마우스 메뉴 Search for-User-defined comment | 사용자가 입력한 comment 목록 보기 |
Label | : | Label 추가 |
User-defined label | 마우스 메뉴 Search for-User-defined label | 사용자가 입력한 Label 목록 보기 |
Breakpoint | F2 | BP(BreakPoint) 설정/해제 |
All referenced text strings | 마우스 메뉴 Search for-All referenced text strings | 코드에서 참조되는 문자열 보기 |
All intermodular calls | 마우스 메뉴 Search for-All intermodular calls | 코드에서 호출되는 모든 API 함수 보기 |
Edit data | Ctrl+E | 데이터 편집 |
Assemble | Space | 어셈블리 코드 작성 |
Copy to executable file | 마우스 메뉴 Copy to executable file | 파일의 복사본 생성(변경 사항 반영됨) |
명령어 | 설명 |
---|---|
CALL XXXX | XXXX 주소의 함수를 호출 |
JMP XXXX | XXXX 주소로 점프 |
PUSH XXXX | 스택에 XXXX 저장 |
RETN | 스택에 저장된 복귀 주소로 점프 |
Ollydbg의 'Edit data'와 'Assemble' 기능 이용
용어
용어 | 설명 |
---|---|
VA(Virtual Address) | 프로세스의 가상 메모리 |
OP code(OPeration code) | CPU 명령어(바이트 code) |
PE(Portable Executable) | Windows 실행 파일(exe, dll, sys 등) |
ⓒ 리버싱 핵심 원리
분야가 달라서 너무 재밌게 봤습니다. 이게 혹시 리버스 엔지니어링인가요??