[리버싱 핵심원리] 1부 2장 정리 - Hello World!
본 게시글의 내용은 리버싱 핵심원리를 보며 복습 겸 정리를 위해 작성하였습니다.
책 내용과 일부 다를 수 있고, 이해를 돕기 위한 저만의 예시도 일부 포함되어 있습니다.
본 게시글에서 사용되는 소스코드와 파일은 리버싱 핵심원리에서 제공하는 파일을 사용하며, 사용하는 도구는 다를 수 있습니다.
Hello World! 프로그램
#include "windows.h"
#include "tchar.h"
int _tmain(int argc, TCHAR *argv[])
{
MessageBox(NULL, L"Hello World", L"www.reversecore.com", MB_OK);
return 0;
}
Hello World!
를 출력하는 프로그램은 아마도 모든 개발자가 프로그래밍 언어를 처음 배울 때 작성해보는 프로그램이 아닐까 싶습니다.
책의 내용과는 별개로 첫 프로그래밍 언어로 C 언어를 독학으로 배웠는데, 검은색 바탕에 흰색 텍스트만 출력되는 터미널 창에 Hello, World!를 출력했는 데 정말 하기 싫었습니다. 당시엔 게임마냥 눈에 바로 보이는 그런 걸 만들고 싶었거든요. 그래서 프로그래밍 언어를 조금 공부하다가 그만두는 계기가 되기도 했답니다. 😎
위 소스코드를 Visual Studio에서 Release 모드로 빌드하면 코드를 최적화하기 때문에 실행 파일의 크기가 작아지고 작성되는 코드도 간결해집니다. Visual Studio와 같은 통합개발환경과 컴파일러의 사용이 익숙하지 않은 분들은 리버싱 핵심원리에서 제공하는 파일을 사용해주세요.
디버거와 어셈블리 언어
통합개발환경이나 컴파일러를 통해 소스코드를 빌드하면 실행 파일이 생성됩니다. 이러한 과정을 거치는 이유는 C 언어는 사람이 이해하기 쉬운 언어이고 이를 기계가 이해하기 쉬운 언어인 기계어로 번역하기 위해 거칩니다. 하지만 기계어는 이진수 또는 16진수로 표현되어 사람이 이해하기 매우 어렵기 때문에 조금 더 이해하기 쉽도록 디버거(Debugger)라는 도구를 사용합니다. 디버거의 디스어셈블러(Disassembler)를 통해 기계어를 어셈블리(Assembly) 언어로 번역해 그나마 이해하기 쉬운 언어로 보여줍니다.
기계어 | 어셈블리어 |
0xB8 0x04 0x00 0x00 0x00 | MOV EAX, 4 |
어셈블리어 기준으로 EAX
레지스터에 4라는 값을 할당하는 코드인데, 이를 기계어로 보면 이해할 수 없습니다. 아... 폰 노이만 선생... 당신은 도대체...
Hello World! 디버깅
🚀 목표
HelloWorld.exe
실행 파일을 디버깅하여 어셈블리어로 변환된 main()
함수를 찾아보고, 이러한 과정을 거치면서 기본적인 디버거의 사용법과 어셈블리 명령어에 대해 배워봅시다.
디버깅 시작
올리디버거라 불리는 OllyDbg는 더 이상 사용하지 않습니다. 엄청 오래됐고 개발자가 더 이상 추가 지원을 하지 않기 때문에 비교적 유사한 디버거인 x64dbg를 사용합니다. 더 나은 인터페이스와 기능을 제공하고 있습니다.
리버서(Reverser)들이 일반적으로 파일을 분석할 때 소스코드 없이 실행 파일만 가지고 분석하기 때문에 x64dbg와 같은 디버거 도구를 사용합니다.
x64dbg는 OllyDbg의 뒤를 잇는(개인 생각) 디버거로, 오픈 소스이자 무료로 공개되고 있고 꾸준히 업데이트하여 지원해주고 있습니다. 더 나은 인터페이스와 기능 그리고 플러그인 기능을 제공해주고 있습니다.
본인의 리버싱 실력이 ㅈ고수에 버금 갈 정도로 올라갔다면 Hex-Rays의 IDA를 사용해보세요. 현존하는 디버거 중 매우 강력하다고 평가 받고 있고 디컴파일의 성능이 매우 좋습니다. 무료 버전과 유료 버전이 있는데 무료 버전으로도 충분합니다.
실행
x96dbg.exe
실행 파일을 실행하면 위 사진과 같이 런처 창이 나타납니다. 오늘 우리가 다룰 HelloWorld.exe
실행 파일은 32Bit의 환경에서 개발된 프로그램이기 때문에 런처에서 x32dbg 버튼을 클릭해주세요.
여러분의 모니터만큼이나 자주 보게 될 x64dbg의 기본 실행 화면입니다. 이미지 1-1에 보이는 기본 구성 화면에 대해 설명하겠습니다.
(1) 코드 창(Code Window) | 어셈블리 코드와 주석, 라벨 등을 표시하고 코드를 분석하여 루프 및 점프되는 영역을 시각화하여 보여줍니다. |
(2) 레지스터 창(Register Window) | CPU의 레지스터 값을 실시간으로 표시하며, 일부 레지스터는 수정이 가능합니다. |
(3) 덤프 창(Dump Window) | 프로세서의 메모리 주소와 값을 16진수나 유니코드로 표시하고 수정이 가능합니다. |
(4) 스택 창(Stack Window) | ESP 레지스터가 가르키는 프로세서의 스택 메모리를 실시간으로 표시하고 수정이 가능합니다. |
파일 열기
HelloWorld.exe
실행 파일을 x32dbg가 실행된 프로그램에 드래그 앤 드롭하거나 파일 메뉴를 통해 열어주세요. 파일을 여는 단축키는 F3입니다.
x32dbg 프로그램에 파일을 불러오면 책과는 다른 주소에서 멈춘 게 이미지 1-2에서 보입니다. 시스템 중단점의 주소에 멈춘 것이기 때문에 책에 나오는 주소인 0x4011A0
에서 멈추려면 설정에서 중단점을 바꿔야 합니다.
설정 메뉴에서 환경설정을 클릭하신 후 이벤트 탭에서 시스템 중단점의 체크를 해제해주세요. 그리고 저장 버튼을 눌러주세요.
Ctrl + F2 단축키를 누르면 불러온 실행 파일을 다시 시작할 수 있습니다. 다시 시작하시면 책에서 언급하는 0x4011A0
주소에서 멈춘 것을 확인할 수 있습니다.
디버거에서 멈춘 해당 주소는 EP(EntryPoint) 코드로, HelloWorld.exe
실행 파일의 시작 주소입니다. 해당 EP 코드에서 가장 눈에 띄는 코드는 아래의 CALL과 JMP 명령입니다.
EP(EntryPoint)는 Windows 실행 파일(exe, dll, sys 등)의 코드 시작점을 의미합니다. 프로그램이 실행될 때 CPU에 의해 가장 먼저 실행되는 코드 시작 위치라 생각하시면 됩니다.
EP는 보통 프로그램의 메인 함수가 위치하는 곳이기 때문에 매우 중요한 역할을 합니다. EP의 위치는 이후 배우게 될 PE 헤더의 AddressOfEntryPoint에서 확인할 수 있습니다.
Address | Instruction | Disassembled Code |
004011A0 | E8 67150000 | call helloworld.40270C |
004011A5 | E9 A5FEFFFF | jmp helloworld.40104F |
Address | 프로세스의 가상 메모리(Virtual Address, VA) 내의 주소 |
Instruction | IA32(또는 x86) CPU 명령어 |
Disassembled Code | 어셈블리어로 변환된 코드 |
위 두 줄의 어셈블리 코드는 의미가 매우 명확합니다.
40270C 주소의 함수를 호출한 후 40104F 주소로 점프하라!
계속 디버깅을 진행하여 main()
함수에서 MessageBox()
함수의 호출을 찾는 것을 목표로 해봅시다.
40270C 함수 따라가기
EP 코드인 0x4011A0
주소에서 F7 키를 누르면 0x40270C
함수 안으로 들어갈 수 있습니다. F7 키는 Step Into라 하여 하나의 명령(OP Code)을 실행하는데 CALL
명령을 만나면 해당 함수의 내부 코드로 들어갑니다.
갑자기 그만두고 싶어지는 어셈블리 코드가 나타났습니다. 우리는 아직 어셈블리어를 잘 모르기 때문에 지금 이 코드를 반드시 해석하여 이해할 필요는 없습니다.
과거 올리디버거에서 기본적으로 호출되는 API를 보여준 것 같은데, x64dbg는 그렇지 않습니다. 호출되는 API의 이름과 매개변수의 정보를 확인하려면 별도의 플러그인 설치가 필요합니다.
이러한 기능을 해주는 게 바로 xAnalyzer입니다. 설치 방법은 해당 깃허브의 README.md에 작성되어 있으니 생략합니다.
0x40270C
주소 라인에서 마우스 우클릭하시면 컨텍스트 메뉴가 나타납니다. 최하단에 있는 xAnalyzer에서 Analyzer function 메뉴 아이템을 클릭해주세요. 해당 기능은 선택한 함수를 분석하여 호출되고 있는 API의 정보를 출력합니다.
이미지 1-8처럼 사용된 API의 정보가 출력되는 걸 확인할 수 있습니다. 그런데 호출되는 API의 이름을 보면 처음에 작성한 소스코드에 없는 API 입니다. 그렇기 때문에 0x40270C
함수는 우리가 찾고 있는 메인 함수가 아닌 것 같습니다. 사실 이 부분은 컴파일러가 프로그램의 실행을 위해 추가한 Stub Code입니다. 이 Stub Code는 컴파일러와 버전에 따라 작성되는 모양이 달라집니다.
단축키 F8을 눌러 Step Over하여 0x4027A1
까지 진행합니다. Step Over는 Step Into와 다르게 CALL
명령을 만나면 함수 내부로 진입하지 않고 함수 자체를 실행합니다.
0x4027A1
주소에 RET
또는 RETN
명령어가 있습니다. RET
는 함수의 끝에서 사용되고 함수가 호출되었던 원래 주소로 되돌아갑니다. RET
는 리턴(Return)의 약자입니다. 위 REN
명령의 경우 리턴 주소는 0x4011A5
입니다.
0x4027A1
까지 Step Over하거나 Ctrl + F9 단축키를 누르면 함수의RET
명령을 수행하기 전까지 명령을 실행합니다. 이 기능을 Execute till Return이라 합니다.
0x4027A1
에서 Step Into나 Step Over하면 이미지 1-4에서 보았던 0x4011A5
주소로 오게됩니다.
40104F 점프문 따라가기
0x4011A5
주소의 JMP 명령을 실행하면 0x40104F
주소로 이동하게 됩니다.
이번에도 프로그램을 꺼버리고 싶은 욕구가 드는 코드들이 나타났지만... 이 역시 컴파일러가 생성한 Stub Code입니다. 이 코드를 따라가다 보면 우리가 찾는 메인 함수가 나타납니다.
리버싱이 처음이라면 우리가 보는 코드가 사용자가 작성한 코드인지 컴파일러가 작성한 Stub Code인지 알아보기 어렵습니다. 디버깅을 자주 하면서 Stub Code를 눈에 익혀두면 향후 디버깅 시 이러한 부분은 생략하면서 분석 속도를 높일 수 있습니다.
메인 함수 찾기
단순 무식한 방법이지만 0x40104F
주소에서 부터 명령어를 하나하나 실행하다 보면 우리가 원하는 메인 함수를 찾을 수 있습니다. 단순하면서 무식한 방법이지만 이러한 과정은 리버싱을 처음 배우는 분들에겐 디버깅하는 데 있어 큰 도움이 됩니다.
0x40104F
주소에서부터 Step Over를 하여 CALL
명령을 만나면 Step Into하여 해당 함수의 내부로 진입해주세요. 그리고 해당 함수의 내부에서 MessageBox()
API가 호출되고 있는 지 확인해주세요. 왜냐하면 우리는 메인 함수에서 MessageBox()
API를 호출하는 걸 작성했기 때문입니다. 없으면 Execute till Return을 통해 함수를 바로 나오도록 합시다. 같은 방식으로 진행하다보면 0x4010E4
주소에 오게됩니다.
0x4010E4
주소에 있는 CALL <JMP.&GetCommandLineW>
명령은 Win32 API의 호출 코드입니다. 지금 우리가 배우고 있는 단계에서는 굳이 Step Into 할 필요 없습니다. 그리고 0x4010EE
의 CALL
부분을 Step Into해서 진입하면 내부의 반복문이 존재하기 때문에 탈출하는 데 시간이 꽤 소요됩니다.
별 무리 없이 디버깅하셨다면 이미지 1-12처럼 0x401144
주소를 만나게 됩니다. 0x401144
주소에 명령을 보면 CALL helloworld.401000
이 있습니다. Step Into로 해당 함수의 내부로 진입해봅시다.
MessageBoxW()를 호출하는 명령이 있고 해당 API의 넘겨지는 매개변수의 값으로 문자열 "www.reversecore.com"와 "Hello World!"가 보입니다. 우리가 소스코드에서 작성한 내용과 일치하는 곳을 발견하였습니다. 맞습니다. 바로 이곳이 메인 함수입니다.
xAnalyzer의 Analyze function 기능을 실행하면
MessageBoxW()
API의 정보가 나타납니다. 넘겨지는 매개변수의 정보를 확인할 수 있습니다.
- Ctrl + G 키를 누르면 원하는 주소로 이동할 수 있는 기능을 사용할 수 있습니다.
- F2 키를 누르면 선택한 주소에 BP(BreakPoint)를 설치할 수 있고, F9 키를 누르면 BP가 설치된 주소까지 실행할 수 있습니다.
Hello World! 문자열 패치
Hello World!
의 문자열을 Hello Reversing
문자열로 출력되도록 패치해봅시다. 우리는 이 패치 기술을 이용해 기존 응용 프로그램의 버그를 수정하거나 기능을 추가하는 등 다양한 작업을 수행할 수 있습니다.
앞에서 메인 함수를 찾았기 때문에 메인 함수 주소(0x401000
)에 F2 키를 눌러 BP(BreakPoint)를 설정하고 F9 키를 눌러 실행해주세요. 그러면 메인 함수의 내부까지 진입하게 됩니다.
문자열 버퍼 수정
문자열을 수정하는 간단한 두 가지 방법이 있습니다. 문자열 버퍼를 직접 수정하는 것과 다른 메모리 영역에 문자열을 작성한 후 이를 불러오는 것입니다. 하나씩 살펴봅시다.
이미지 1-13을 보시면 "Hello World!"
의 문자열이 저장된 곳은 0x4092A0
주소입니다.
0x401007
주소 라인에서 마우스 우클릭하시면 컨텍스트 메뉴가 나타나는데, 덤프에서 따라가기(Follow in Dump) 메뉴에서 helloworld.004092A0 메뉴 아이템을 클릭해주세요. 그러면 좌측 하단에 있는 덤프 창에서 이동된 주소를 확인할 수 있습니다.
이미지 1-15를 보시면 Hello World!
문자열이 저장된 공간을 확인할 수 있습니다. 여기서, 문자열 버퍼의 공간은 0x4092A0
~ 0x4092B9
입니다. 유니코드 문자열로 저장되었기 때문에 한 문자 당 2Bytes씩 차지합니다.
이미지 1-16과 같이 0x4092A0
~ 0x4092BF
까지 마우스로 드래그하여 선택한 후 Ctrl + E를 눌러주세요.
이미지 1-17과 같이 데이터 수정 창이 나타나는데, UNICODE에 작성된 Hello World!
문자열을 Hello Reversing
으로 변경하신 후 OK 버튼을 눌러주세요.
덤프 창에서 문자열 데이터가 변경되었는 지 확인하신 후, 마지막 2Bytes가 NULL 데이터로 되어 있는 지 확인해주세요. 유니코드 문자열은 마지막에 2Bytes 크기의 NULL 데이터로 끝나야합니다 .(0x00의 값 두 번)
덤프 창에서 값을 더블클릭하여 하나씩 수정할 수 있습니다.
코드 창으로 돌아와 xAnalyer의 기능을 다시 사용하면 Hello Reversing
문자열로 패치된 걸 확인할 수 있습니다. F9 키를 눌러 실행해봅시다.
실행 결과를 보시면 우리가 패치한 문자열이 출력되는 걸 확인할 수 있습니다.
원본
Hello World!
문자열보다Hello Reversing
문자열의 길이가 훨씬 더 깁니다. 보통 원본 문자열 길이 뒤쪽에 의미있는 데이터가 존재할 수 있기 때문에 원본 문자열의 길이를 넘어가는 문자열 데이터로 덮어쓰지 않습니다. 어디까지나 실습을 위해 그렇게 했을 뿐입니다.
파일 생성
위에서 우리가 수행한 패치 작업은 임시적으로 메모리의 내용을 수정한 것이라 디버거가 종료되면(HelloWorld.exe 프로세서가 종료되면) 패치했던 내용은 그대로 사라집니다. 따라서 우리가 변경한 내용을 그대로 저장하려면 x64dbg의 패치 기능을 이용해야 합니다.
마우스 우클릭하시면 Patches(패치)라는 메뉴를 찾을 수 있습니다. 단축키는 Ctrl + P입니다. 패치 기능을 통해 우리가 수정한 내역(패치 내역)을 파일에 적용할 수 있습니다.
패치 기능을 실행하시면 패치 창이 나타나는데, 우리가 수정한 내역이 나타납니다.
하단 우측에 있는 파일 패치(Patch File) 버튼을 클릭해주세요.
HelloWorld.exe
실행 파일의 사본을 만드신 후(백업) 사본 파일에 저장하시면 우리가 작업한 내역이 해당 파일에 그대로 적용됩니다. 패치된 실행 파일을 실행하여 Hello Reversing
문자열이 출력되는 지 확인해보세요.
다른 메모리 영역에 문자열 데이터 생성하여 전달하기
만약 원본 문자열보다 훨씬 긴 문자열 내용으로 패치해야 한다면 앞에 방법은 적절하지 않습니다. HelloWorld.exe
를 다시 열어주신 후 메인 함수로 이동해주세요.
0x401007
주소를 보시면 PUSH 명령을 통해 MessageBoxW
함수에 문자열 매개변수를 전달하고 있습니다. MessageBoxW
함수는 매개변수로 넘겨진 주소의 문자열을 출력하기 때문에 이 주소를 변경해서 전달하면 변경된 문자열이 출력될 수 있습니다.
어느 메모리 영역에 문자열 데이터를 작성해야 할까요? 이는 추후 배우게 될 PE 파일 구조와 가상 메모리에 대한 개념을 알고 있어야 하기 때문에 잠시 궁금증은 접어두기로하고 이번 실습에선 임의로 적절한 영역을 선택합니다.
덤프 창에서 원본 문자열 주소(0x4092A0
)로 이동하신 후 아래로 스크롤하다 보면 0x00(NULL)로 채워진 영역을 발견할 수 있습니다. 이 영역은 프로그램에서 사용되지 않는 NULL 패딩(Padding)입니다.
이곳애 문자열 데이터를 작성하여 주소를 넘겨주면 될 것 같습니다. 적당한 위치(0x409F50
)에 Hello Reversing!!!
문자열을 작성해봅시다.
문자열 데이터를 작성하였으니 MessageBoxW
함수에 넘겨지는 문자열 주소를 우리가 임의로 새롭게 작성한 주소(0x409F50
)으로 변경해봅시다.
0x401007
주소 라인을 선택하신 후 Space Bar 키를 누르시면 명령어 수정 창이 나타납니다. 아래의 명령어를 입력한 후 OK를 눌러주세요.
push 0x409F50
F9 키를 눌러 실행하면 Hello Reversing!!!
문자열이 출력되는 걸 확인할 수 있습니다.
위 방식대로 작업을 수행 후 패치 파일을 만들어 실행하면 문자열이 출력되지 않습니다. 이유는 0x409F50 주소 때문입니다. 실행 파일이 메모리에 로딩되어 프로세스로 실행될 때 어떠한 규칙으로 인해 그대로 메모리 적재되지 않고 올라가게 되기 때문에 그렇습니다. 정확히 이해하려면 PE 파일 구조에 대해 알아야하기 때문에 자세한 설명은 생략합니다.
주제 가리지 않고 잡다하게 다 하는 블로그