본문 바로가기
게임해킹/리버싱 핵심원리(나뭇잎책)

1장 리버싱 스토리 / 2장 HelloWorld 리버싱

by HHack 2024. 8. 8.
반응형

1.1 리버싱 분석 방법

(1) 정적 분석

- 파일을 실행하지 않고 파일의 겉모습을 관찰하여 분석하는 방법

- 파일의 종류(EXE, DLL, DOC, ZIP 등), 크기, 헤더(PE)정보, Import/Export API, 내부 문자열, 실행 압축 여부, 등록 여부, 디버깅 정보, 디지털 인증서 등의 내용을 확인

(2) 동적 분석

- 파일을 직접 실행시켜서 그 행위를 분석하고, 디버깅을 통하여 메모리 상태 등을 자세히 살펴보는 방법

- 파일, 레지스트리, 네트워크 등을 관찰하면서 프로그램의 행위를 분석

- 디버거를 이용하여 프로그램 내부 구조와 동작원리를 분석할 수 있다.

 

1.2 소스코드, Hex Code, Assembly Code

(1) 소스코드 : 소스코드를 빌드하면 exe 실행파일이 생성된다.

(2) Hex Code : 컴퓨터가 이해할 수 2진수가 16진수로 바뀐 코드

(3) Assembly Code : 사람이 좀 더 이해할 수 있게 유니코드로 바뀐 코드

 

1.3 패치와 크랙

(1) 패치 : 프로그램의 파일 혹은 실행 중인 프로세스 메모리의 내용을 변경하는 작업(합법)

(2) 크랙 : 패치와 달리 의도가 비합법적이고 비도덕적인 경우(불법)


2.1 디버거와 어셈블리어

- 어셈블리 언어는 CPU에 종속되어 있다. 즉, Intel과 ARM은 어셈블리 명령어 형태가 다르다.

- 디버거는 기계어를 어셈블리 언어로 번역해서 보여주는 역할을 한다.

 

2.2 디버깅

- x64dbg 화면 구성

x64dpg 화면 구성

 Code Window - 기본적으로 디스어셈블리 코드를 표시하며 각종 Comment, Label을 보여준다.
- 코드를 분석하여 Loop, Jump 위치 등의 정보를 표시한다.
 Register Window - CPU Register 값을 실시간으로 표시하며 특정 Register 들은 수정도 가능합니다.
 Dump Window - 프로세스에서 원하는 메모리 주소 위치를 Hex와 Ascii/유니코드 값으로 표시하며 수정도 가능하다.
 Stack Window - ESP Register가 가리키는 프로세스 Stack Memory를 실시간으로 표시하고 수정도 가능하다.

 

2.2.1 EP(Entry Point)

- 프로그램이 실행되는 시작 지점

Entry Point

2.2.2 x64dbg 기본 명령어 및 Stub Code

(1) 디버거 기본 명령어

Restart [Ctrl + F2] 다시 처음부터 디버깅 시작(디버깅을 당하는 프로세스를 종료하고 재실행)
Step Into [F7] 하나의 Op-Code를 실행한다. CALL 명령을 만나면 그 함수 코드 내부로 따라 들어간다.
Step Over [F8] 하나의 Op-Code를 실행한다. CALL 명령을 만나면 따라 들어가지 않고 그냥 함수 자체를 실행한다.
Execute till Return [Ctrl + F9] 함수 코드 내에서 RETN 명령어까지 실행(함수 탈출 목적)

(2) Stub Code

- 빨간박스 영역의 함수들은 코드에서 호출되는 API 함수들입니다.

- 원본 소스코드에서 사용된 적 없는 API들이 호출되고 있습니다.

- 이러한 컴파일러에서 프로그램을 실행시키기 위해 추가시킨 코드들을 Stub Code 라고 합니다.

- 따라서 컴파일러 종류에 따라 혹은 버전에 따라 Stub Code의 형태는 모두 다릅니다.

API 함수들

2.2.4 main 함수 찾기

- 계속 디버깅을 하다보면 성공적으로 진행됐다면 아래의 순서대로 주소들을 만나게 됩니다.

4011A0 -> 40270C -> 40104F -> 402524 -> 4010E4 -> 401144

main() 함수

(1) MessageBoxW() API

- MessageBoxW() API 함수를 불러오는걸로 보아 이 부분이 Main 함수라고 유추할 수 있습니다.

- 또한 출력하고자 하는 "Hello World!" 문자열을 가지고 있습니다.

 

2.3 디버거 좀 더 능숙하게 다루기

2.3.1 디버거 명령어

 명령어  단축키  설명
 Go to   Ctrl + G  원하는 주소로 이동.(실행 되는 것은 아님)
 Execute till Cursor  F4  커서 위치까지 실행(디버깅하고 싶은 주소까지 바로 이동)
 Comment  ;  Comment 추가
 Label  :  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 등의 명령어에 위치해 있다면 해당 주소를 따라가서
 보여줌(실행되는 것은 아님)

 

2.3.2 베이스캠프를 설치하는 4가지 방법

◆ 베이스캠프 : 중간중간 코드에서 분석을 원하는 중요 포인트(주소)를 지정해 놓는 지점

 

(1) Goto 명령(Ctrl + G)

Goto를 이용하여 원하는 주소로 이동

 

(2) BP 설치(F9)

디버거는 현재 실행 위치에서부터 프로세스를 실행하다가 BP가 걸린 곳에서 멈추게 된다.

BreakPoint 설치

 

메인 메뉴의 View - BreakPoints를 선택하면 BreakPoint 목록이 나타납니다. (Alt + B)

View - BreakPoints(Alt + B)

참고로 목록에서 원하는 BreakPoint를 더블클릭하면 해당 주소로 이동합니다.

 

(3) 주석(;)

";" 단축키로 주석을 달고 이 주석을 찾아가는 방법도 있습니다.

"베이스캠프"라고 주석을 단 모습

 

"보기 -> 주석" 항목을 선택하면 아래의 그림과 같이 모든 주석들을 볼 수 있습니다.

보기 - 주석

빨간 글씨로 표시된 부분이 커서의 위치입니다. 주식 위치와 겹쳐지면 발간 글씨로 나타납니다. 해당 주석을 더블클릭 하면 그 주소로 이동할 수 있습니다.

 

(4) 레이블(:)

레이블(Label)은 원하는 주소에 특정 이름을 붙여주는 아주 유용한 기능입니다.

: 단축키를 이용한 레이블 추가

레이블을 붙이면 레이블을 붙인 주소를 해당 레이블로 표시합니다.

40104F 주소가 Base Camp로 바뀌어있다.

레이블도 마찬가지로 검색이 가능합니다. "보기 - 레이블"을 누르면 사용자가 지정한 레이블이 표시됩니다.

"보기 - 레이블"

원하는 레이블을 더블클릭 하면 해당 주소로 갈 수 있습니다. 그 주소에 커서를 놓은 후 "Execute till cursor[F4]" 명령어를 써주면 그 위치에서부터 디버깅이 가능합니다.

 

2.4 원하는 코드 빨리 찾아내는 4가지 방법

실행 파일의 EP 주소에 바로 main()이 나타나는 것이 아니고 개발도구의 Stub Code가 나타나는 것을 알았습니다. 이와 같이 우리가 원하는 main() 함수로 바로 찾아갈 수 있는 방법을 알아보겠습니다.

 

2.4.1 코드 실행 방법

프로그램을 실행해보면 main() 함수의 MessageBox() 함수가 실행되어 "Hello World!" 메시지 박스가 출력된 것을 확인할 수 있습니다. 이것이 바로 코드 실행 방법의 원리입니다. 즉, 프로그램의 기능이 명확한 경우에 명령어를 하나하나 실행하면서 원하는 위치를 찾아가는 방법을 의미합니다. 이 방법은 코드의 크기가 작고 기능이 명확한 경우에 사용할 수 있습니다.

 

명령어를 한 줄씩 실행하다보면[F8] 어느 순간 "Hello World!" 메시지 박스가 출력됩니다.디버거를 재실행[Ctrl + F2]을 하면 특정 함수를 호출한 이후에 메시지 박스가 나타나는걸 볼 수 있습니다.

main() 함수 진입점

401044 주소에 있는 CALL 401000 명령어에서 호출되는 주소로 가보면[F7], 그곳이 우리가 찾는 main() 함수의 코드 영역입니다.

main() 함수

401000E에 있는 MessageBoxW() API 호출 코드가 보입니다. 그리고 그 위에 Title과 Text 문자열도 보입니다. 위 문자열을 Stack에 저장하여 MessageBoxW() 함수에 파라미터로 전달하고 있습니다.

즉, main()함수로 정확히 찾아왔습니다.

 

2.4.2 문자열 검색 방법

마우스 우측 메뉴 -> 다음을 찾기 -> 모든 모듈 -> 문자열 참조 (x64dbg)

모든 문자열 참조

위의 사진에서 보면 우리가 찾고자 하는 "Hello World!" 라는 문자열을 찾을 수 있습니다. 이를 더블클릭하면 해당 주소로 이동합니다.

 

※ 참고

VS에서는 static 문자열을 기본적으로 유니코드 형식으로 저장합니다. static 문자열이란 프로그램 내부에 하드코딩 되어 있는 문자열을 의미합니다. 

덤프창에서 확인한 유니코드로 저장되어 있는 Hello World!

2.4.3 API 검색 방법(1) - 호출 코드에 BP

마우스 우측 메뉴 -> 다음을 찾기 -> 모든 모듈 -> 모듈간 호출 (x64dbg)

 

Windows 프로그래밍에서 모니터 화면에 뭔가를 출력했다면 Win32 API를 사용하여 OS에게 화면출력을 요청해야 합니다. 즉, 프로그램이 화면에 뭔가를 출력했다는 얘기는 프로그램 내부에서 Win32 API를 사용하였다는 뜻입니다. 그렇다면 프로그램의 기능을 보고 사용되었을 법한 Win32 API 호출을 예상하여 그 부분을 찾을 수 있다면 디버깅이 매우 간편해질 것입니다.

 

위 실습 예제인 HelloWolrd.exe 파일은 메시지 박스를 출력하므로 분명 user32.MessageBoxW() API를 사용하였을 것입니다. 이 정보를 활용하여 API 함수 호출 목록을 확인하여 원하는 부분을 쉽게 찾아낼 수 있습니다.

모듈간 호출 화면

2.4.4 API 검색 방법(2) - API 코드에 직접 BP

마우스 우측 메뉴 -> 다음을 찾기 -> 현재 모듈 -> Names (x64dbg)

 

x64dbg가 모든 실행 파일에 대해서 API 함수 호출 목록을 추출할 수 있는 것은 아닙니다. Packer/Protector를 사용하여 실행파일을 압축(또는 보호) 하면 파일 구조가 변경되어 API 호출 목록이 보이지 않습니다.

 

이런 경우 프로세스 메모리에 로딩된 라이브러리(DLL 코드)에 직접 BP를 걸어보는 것입니다. API 라는 것은 OS에서 제공한 함수이고, 실제로 API는 C:/Windows/system32 폴더에 *.dll 파일 내부에 구현되어 있습니다. 간단히 말해서 우리가 만든 프로그램이 어떤 의미 있는 일(각종 I/O)을 하려면 반드시 OS에서 제공된 API를 사용해서 OS에게 요청해야하고, 그 API가 실제 구현된 시스템 DLL 파일들은 우리 프로그램의 프로세스 메모리에 로딩되어야 합니다.

 

위의 나와있는 방법을 통해 프로세스 실행을 위해 같이 로딩된 시스템 DLL 파일이 제공하는 모든 API 목록을 확인해보겠습니다.

각 모듈 별 API 목록

 

이중 User32 모듈에서 Export type의 MessageBoxW 함수를 선택합니다.

user32.dll 의 MessageBoxW 찾기
user32.dll 의 MessageBoxW 함수로 이동

이곳에 BP를 설치[F2]하고, 실행[F9] 합니다.

user32.dll의 MessageBoxW 코드의 BP에서 실행이 멈춤

레지스터 창의 ESP를 확인해보면 19FF18로 나와있는데 이는 프로세스 스택 주소입니다.

아래의 기본값을 보면 자세하게 설명이 나와있습니다.

ESP 주소 기준으로 그 뒤의 코드 설명
ollydbg에 나와있는 설명

 

ESP의 값 19FF18에 있는 Return 주소인 4010014는 HelloWorld.exe의 main 함수 내의 MessageBoxW 함수 호출 바로 다음의 코드입니다. 위와같이 직접 API 함수를 찾아 원하는 코드로 빠르게 찾아갈 수 있습니다.

 

2.5 "Hello World!" 문자열 패치

2.5.1 패치

패치 기술을 이용하여 기존 응용 프로그램의 버그를 수정하거나 또는 새로운 기능을 추가시킬 수 있습니다. 패치 대상은 파일 혹은 메모리가 될 수 있으며, 프로그램의 코드와 데이터 모두 패치가 가능합니다.

 

먼저 디버거를 실행하여 main 함수의 시작주소(401000)까지 실행합니다.

main() 함수

2.5.2 문자열을 패치하는 두 가지 방법

(1) 문자열 버퍼를 직접 수정

MessageBoxW 함수의 전달인자 4092A0의 문자열("Hello World!") 버퍼를 직접 수정하는 방법입니다. 덤프 창에서 Go to 명령[Ctrl + G]으로 4092A0 주소로 갑니다. 

Dump 창에서 Go to 명령어 실행

그리고 4092A0 주소를 마우스로 선택한 후 [Ctrl + E] 단축키로 Edit 창을 띄웁니다.

원하는 Hello World 문자열 선택
Hello Reversing으로 문자열 변경

유니코드 문자열은 2바이트의 크기의 NULL로 끝나야 합니다. 따라서 Hex창에서 "00 00"을 직접 넣어줍니다.

※ 참고 

- 일반적으로는 원본 문자열 뒤쪽에 어떤 의미 있는 데이터가 존재할 수 있기 때문에 원본 문자열의 길이를 넘어가는 문자열로 덮어쓰는 것은 위험합니다.

 

main() 함수를 보면 기존의 "Hello World!"로 되어 있던 문자열이 "Hello Reversing"으로 바뀐것을 확인할 수 있습니다.

main() 함수에서 문자열 변경 확인

이제 F9를 실행해보면 메시지 박스에 패치된 문자열이 나오게 됩니다.

패치된 메시지 박스

문자열 버퍼 내용을 직접 수정하는 방식은 사용하기에 가장 간단하지만 단점은 기존 문자열 버퍼 크기 이상의 문자를 입력하기 어렵다는 점입니다.

 

◆ 파일로 생성하기

위에서 수행한 패치 작업은 메모리에 임시적으로 한 것이라 디버거가 종료되면(HelloWorld.exe 프로세스가 종료되면) 패치됐던 내용은 사라집니다. 따라서 변경한 내용을 영구적으로 보존하려면 별도의 실행 파일로 저장해야 합니다.

x64dbg에는 기본적으로 Scylla 플러그인이 설치되어 있습니다. 이를 이용하면 실행 파일로 저장할 수 있습니다.

 

Scylla를 실행하여 File탭 -> Dump를 실행하면 원하는 경로에 현재 패치한 파일을 exe 파일로 저장할 수 있습니다.

Scylla를 이용하여 덤프파일 저장

(2) 다른 메모리 영역에 새로운 문자열을 생성하여 전달

만약 원본 문자열보다 더 긴 문자열로 패치해야 한다면 앞선 방법으로는 잘 맞지 않습니다. 이런 경우에 사용할 수 있는 방법을 알아보겠습니다.

main() 함수

main() 함수를 보면 401007 주소의 push helloworld.4092A0 명령은 MessageBoxW() 함수 호출을 위해 4092A0 주소의 "Hello World!" 문자열을 파라미터로 전달하고 있습니다. 만약 이 문자열의 주소를 변경하여 전달한다면 메시지 박스에는 변경된 문자열이 출력될 것입니다. 즉, 적당한 메모리 영역에 패치하고자 하는 긴 문자열을 적어 놓고 MessageBoxW() 함수에게 그 문자열의 주소를 파라미터로 넘겨주는 것입니다.

 

※ 참고 - 메모리 영역 정하기

문제는 "메모리 어느 영역에 문자열을 써야하는가?" 입니다. 이는 실행 파일 형식(PE File Format)과 가상 메모리(Virtual Address) 구조를 알고 있어야 합니다.

 

4092A0 주소 이후에 내리다 보면 409FF0 까지 NULL Padding 영역으로 끝이 나는걸 볼 수 있습니다.

 

※ 참고 - NULL Padding이란?

프로그램이 메모리에 로딩될 때 최소 기본단위(보통 1000)이 있습니다. 비록 프로그램 내에서 메모리를 100만 사용한다고 하더라도 실제로 메모리에 로딩될 때는 최소 기본 단위인 1000만큼의 크기가 잡혀 있습니다. 이를 채우기 위해 NULL로 채워진 영역을 NULL Padding 영역이라고 합니다.

NULL Padding

이 영역을 이용해 새로운 문자열을 써보겠습니다.

Hello Reversing!!!!! 문자열

이 작업만으로는 메시지 박스의 문자열이 변경되지 않습니다. 버퍼를 새로 구성하였으니 MessageBoxW() 함수에게 새로운 버퍼 주소(409F50)를 파라미터로 전달해줘야 합니다. 그러기 위해서 Code 창에서 Assemble 명령을 사용해 코드를 수정해보도록 하겠습니다.

 

커서를 401007 주소에 놓고 Assemble 명령(단축키 [Space])을 실행합니다. 이후 아까 "Hello Reversing!!!!!"을 저장한 주소인 409F50으로 어셈블을 변경합니다.

Asseble 명령어로 "Hello Reversing!!!!!" 문자열 주소로 변경

이후 실행[F9]하면 새로 입력한 문자열 "Hello Reversing!!!!!"으로 출력됩니다.

변경된 문자열 확인

위처럼 디버깅의 강력한 기능 중 하나로 실행중인 프로세스의 코드를 동적으로 패치할 수 있다는 점입니다.

 

※ 참고 - 실행 파일 오류나는 이유

실행 파일로 만들어 실행하면 아까와 달리 정상적으로 실행되지 않습니다. 그 이유는 409F50 메모리 주소 때문입니다. 실행 파일이 메모리에 로딩되어 프로세스로 실행될 때 파일이 그대로 메모리로 로딩되는 것이 아니라 어떠한 규칙에 의해서 올라가게 됩니다. 그 과정에서 프로세스 메모리는 존재하는데, 그에 해당하는 파일 오프셋이 존재하지 않는 경우가 많습니다. 위의 경우도 메모리 409F50에 대응되는 파일 오프셋이 존재하지 않는 것이죠. 이를 정확히 이해하긴 위해선 PE File Format을 알아야 합니다.(13장)

실행 파일이 제대로 동작하지 않음

 

반응형

'게임해킹 > 리버싱 핵심원리(나뭇잎책)' 카테고리의 다른 글

7장 스택 프레임  (2) 2024.09.04
6장 abex' crackme #1 분석  (0) 2024.08.30
5장 스택  (2) 2024.08.29
4장 IA-32 Register 기본 설명  (1) 2024.08.17
3장 리틀 엔디언 표기법  (1) 2024.08.16