3.1 디버기
프로세스를 디버깅 하려면 어떤 식으로든 먼저 해당 프로세스에 연결해야 한다. 디버깅할 프로세스를 실행시키거나 이미 실행되어 있는 프로세스에 붙여야 한다.
프로세스를 실행시키는 경우에는 해당 프로세스의 코드가 실행되기 전에 제어를 할 수 있다는 장점이 있다. 이는 악성코드나 기타 다른 형태의 악의적인 코드를 분석할 때 편리하다.
프로세스에 붙이는 것은 단지 이미 실행중인 프로세스에 연결하는 것이다. 따라서 프로세스가 시작되면서 실행되는 코드를 건너 뛸 수 있으며 관심이 있는 특정 영역의 코드만을 분석할 수 있다.
프로세스를 실행시켜 디버깅 하는 것은 디버거가 바이너리를 직접 실행시키는 것이다. 이를 위해선 CreateProcessA() 함수를 호출하고, 특정 플래그의 값을 설정함으로써 프로세스를 디버깅 모드로 실행시킬 수 있다.
3.1.1 CreateProcessA() 함수 분석
BOOL CreateProcessA(
LPCSTR lpApplicationName, // 실행할 프로세스의 경로 (NULL 가능)
LPSTR lpCommandLine, // 명령줄 인자
LPSECURITY_ATTRIBUTES lpProcessAttributes, // 프로세스 보안 속성 (NULL = 기본값)
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 스레드 보안 속성 (NULL = 기본값)
BOOL bInheritHandles, // 부모 프로세스의 핸들 상속 여부
DWORD dwCreationFlags, // 프로세스 생성 옵션 (디버깅 모드 가능)
LPVOID lpEnvironment, // 환경 변수 블록 (NULL = 기본 환경 사용)
LPCSTR lpCurrentDirectory, // 프로세스의 작업 디렉터리 (NULL = 부모와 동일)
LPSTARTUPINFOA lpStartupInfo, // 프로세스 시작 정보 구조체
LPPROCESS_INFORMATION lpProcessInformation // 생성된 프로세스 정보 구조체
);
1. CreateProcessA()의 주요 인자 설명
인자 | 설명 |
lpApplicationName | 실행할 파일의 경로 (NULL이면 lpCommandLine에서 지정) |
lpCommandLine | 실행할 명령어 (예: "C:\\Windows\\System32\\notepad.exe") |
lpProcessAttributes | 프로세스 보안 속성 (NULL 기본값) |
lpThreadAttributes | 스레드 보안 속성 (NULL 기본값) |
bInheritHandles | 부모 프로세스의 핸들을 상속할지 여부 (FALSE 권장) |
dwCreationFlags | 프로세스 생성 옵션 (DEBUG_PROCESS를 사용하면 디버깅 가능) |
lpEnvironment | 환경 변수 (NULL이면 부모 환경 사용) |
lpCurrentDirectory | 프로세스의 실행 디렉터리 (NULL 기본값) |
lpStartupInfo | 프로세스의 시작 정보를 저장하는 구조체 |
lpProcessInformation | 생성된 프로세스 정보를 반환하는 구조체 |
2. dwCreationFlags 값 (디버깅 관련)
플래그 | 설명 |
DEBUG_PROCESS | 디버거를 연결한 상태에서 실행됨 (디버거 없이 실행 불가) |
DEBUG_ONLY_THIS_PROCESS | 이 프로세스만 디버깅 (자식 프로세스는 디버깅되지 않음) |
CREATE_SUSPENDED | 프로세스를 생성하되 즉시 실행하지 않고 중단된 상태로 둠 |
CREATE_NEW_CONSOLE | 새로운 콘솔 창에서 실행 |
3. CreateProcessA()로 실행된 프로세스를 디버깅하는 방법
- CreateProcessA()를 DEBUG_PROCESS 옵션과 함께 호출하여 프로세스를 시작합니다.
- WaitForDebugEvent()를 사용하여 디버그 이벤트를 수신합니다.
- ContinueDebugEvent()로 디버깅 이벤트를 처리하고 프로세스를 계속 실행합니다.
- 디버깅이 끝나면 CloseHandle()을 호출하여 프로세스를 정리합니다.
4. CreateProcessA()를 활용한 디버거 기능 확장
- 중단점(Breakpoint) 설정 : WriteProcessMemory()를 사용하여 특정 주소에 INT 3 (0xCC) 삽입.
- 메모리 덤프 : ReadProcessMemory()로 특정 메모리 값을 읽어 분석.
- 프로세스 실행 상태 제어 : SuspendThread() 및 ResumeThread()로 특정 스레드를 중지/재개.
- 디버그 이벤트 분석 : EXCEPTION_DEBUG_EVENT, CREATE_THREAD_DEBUG_EVENT 등 다양한 디버그 이벤트를 처리.
3.1.2 디버거 코드 작성
먼저 계산기를 실행시키고 그 계산기의 PID를 받아오는 간단한 디버거를 만들어보자.
(1) my_debugger_defines.py
#my_debugger_defines.py
from ctypes import *
# ctypes 형태의 타입을 MS 타입으로 매핑
BYTE = c_ubyte
WORD = c_ushort
DWORD = c_uint32
LPBYTE = POINTER(BYTE)
LPTSTR = c_char_p
HANDLE = c_void_p
PVOID = c_void_p
LPVOID = c_void_p
UINT_PTR = c_uint64
SIZE_T = c_size_t
# 상수
DEBUG_PROCESS = 0x00000001
CREATE_NEW_CONSOLE = 0x00000010
PROCESS_ALL_ACCESS = 0x001F0FFF
INFINITE = 0xFFFFFFFF
DBG_CONTINUE = 0x00010002
# CreateProcessA() 함수를 위한 구조체
class STARTUPINFO(Structure):
_fields_ = [
("cb", DWORD),
("lpReserved", LPTSTR),
("lpDesktop", LPTSTR),
("lpTitle", LPTSTR),
("dwX", DWORD),
("dwY", DWORD),
("dwXSize", DWORD),
("dwYSize", DWORD),
("dwXCountChars", DWORD),
("dwYCountChars", DWORD),
("dwFillAttribute", DWORD),
("dwFlags", DWORD),
("wShowWindow", WORD),
("cbReserved2", WORD),
("lpReserved2", LPBYTE),
("hStdInput", HANDLE),
("hStdOutput", HANDLE),
("hStdError", HANDLE),
]
class PROCESS_INFORMATION(Structure):
_fields_ = [
("hProcess", HANDLE),
("hThread", HANDLE),
("dwProcessId", DWORD),
("dwThreadId", DWORD),
]
(2) my_debugger.py
from ctypes import *
from my_debugger_defines import *
kernel32 = windll.kernel32
class Debugger:
def __init__(self):
pass
def load(self, path_to_exe):
# dwCreation flag determines how to create the process
# set creation_flags = CREATE_NEW_CONSOLE if you want
# to see the calculator GUI
creation_flags = DEBUG_PROCESS
# instantiate the structs
startupinfo = STARTUPINFO()
process_information = PROCESS_INFORMATION()
# The following two options allow the started process
# to be shown as a separate window. This also illustrates
# how different settings in the STARTUPINFO struct can affect
# the debuggee.
startupinfo.dwFlags = 0x1
startupinfo.wShowWindow = 0x0
# We then initialize the cb variable in the STARTUPINFO struct
# which is just the size of the struct itself
startupinfo.cb = sizeof(startupinfo)
if kernel32.CreateProcessA(
path_to_exe.encode('utf-8'), # Ensure bytes string
None,
None,
None,
False, # BOOL must be explicitly set
creation_flags,
None,
None,
byref(startupinfo),
byref(process_information)
):
print("[*] We have successfully launched the process!")
print("[*] The Process ID I have is: %d" % process_information.dwProcessId)
else:
print("[*] Error with error code %d." % kernel32.GetLastError())
(3) my_test.py
import my_debugger
from my_debugger_defines import *
debugger = my_debugger.Debugger() # 클래스명을 대문자로 변경 (PEP8 스타일 적용)
debugger.load("C:\\WINDOWS\\system32\\calc.exe")
위 코드를 실행시키면 계산기 프로그램의 GUI는 나오지 않는다. 그 이유는 프로세스가 디버거로부터 실행을 계속하게 하는 명령을 받지 못해 아직 화면에 GUI를 그리지 못했기 때문이다.
이제는 실행중인 프로세스에 디버거를 붙이는 코드를 살펴볼 차례이다. 프로세스를 붙이기 위해선 먼저 해당 프로세스에 대한 핸들을 구해야 한다. 프로세스 핸들을 얻기 위한 함수는 OpenProcess()이다. 이는 kernel32.dll에서 export 하는 함수이며 프로토 타입은 다음과 같다.
HANDLE WINAPI OpenProcess(
DWORD dwDesiredAccess, // 접근 권한 플래그
BOOL bInheritHandle, // 핸들 상속 여부
DWORD dwProcessId // 열고자 하는 프로세스 ID (PID)
);
📌 매개변수 설명
1. dwDesiredAccess (필수)
- 프로세스에 대해 어떤 작업을 수행할 것인지 권한을 지정하는 플래그. (필수)
2. bInheritHandle (필수)
- 자식 프로세스에서 핸들을 상속할 수 있는지 여부 (TRUE = 상속 가능, FALSE = 상속 불가능).
3. dwProcessId (필수)
- 열고자 하는 프로세스의 PID (프로세스 ID).
📌 반환 값
- 성공하면 프로세스 핸들(HANDLE) 반환.
- 실패하면 NULL을 반환하고, GetLastError()를 사용하여 오류 코드 확인.
🛠 dwDesiredAccess (프로세스 접근 권한 플래그)
권한 플래그설명
PROCESS_ALL_ACCESS | 모든 권한을 가짐 |
PROCESS_VM_READ | 해당 프로세스 메모리를 읽을 수 있음 (ReadProcessMemory) |
PROCESS_VM_WRITE | 해당 프로세스 메모리에 값을 쓸 수 있음 (WriteProcessMemory) |
PROCESS_CREATE_THREAD | 해당 프로세스에서 새로운 스레드를 생성할 수 있음 |
PROCESS_QUERY_INFORMATION | 프로세스 정보 조회 가능 |
PROCESS_TERMINATE | 프로세스를 종료할 수 있음 |
PROCESS_SUSPEND_RESUME | 프로세스를 일시 중지/재개할 수 있음 |
📌 "PROCESS_ALL_ACCESS"를 사용하면 모든 권한을 가지지만, 보안 정책 때문에 실패할 가능성이 있음.
❗ 주의할 점
1. 관리자 권한이 필요할 수 있음
- PROCESS_ALL_ACCESS는 관리자 권한이 있어야 실행 가능.
- Python 실행 시 관리자 권한으로 실행 필요.
2. 64비트 vs 32비트 문제
- 64비트 프로세스를 32비트 프로그램에서 열려 하면 실패할 수 있음.
- 같은 아키텍처(32비트 ↔ 32비트, 64비트 ↔ 64비트)에서 실행해야 함.
3. 핸들 누수 방지 (CloseHandle())
- OpenProcess()로 얻은 핸들은 CloseHandle()로 반드시 해제해야 함.
'게임해킹 > 파이썬 해킹 프로그래밍' 카테고리의 다른 글
2장 디버거 (0) | 2024.12.29 |
---|---|
1장 개발 환경 구축 (1) | 2024.12.27 |