逆向工程核心原理笔记-6

 此章节就是给多种示例程序,进行调试练习。

0x00 windows 服务程序的调试

 服务(Service)程序由SCM(Service Control Manager,服务控制管理器)管理。运行服务程序时,需要由控制器(Service Controller)执行启动命令。控制器向SCM提出服务控制请求,SCM向服务程序传递控制命令,并接收其返回的值。如下图所示:

image-20230525161651579

 注意:控制器无法直接向服务程序下达命令,必须通过 SCM 传达。下图是windows的服务控制器:

image-20230525161942822

服务启动过程

image-20230525162049234

 所有服务程序都是由服务控制器调用SartService() API启动的,若服务为自启动服务,则由SCM调用StartService启动。

 启动过程如下:

1
2
3
4
5
6
7
8
9
(1)服务控制器调用StartService
1.1 服务控制器调用StartService时,SCM会创建相应服务进程,然后执行服务进程的EP代码。
(2)服务进程调用StartServiceCtrlDispatcher
2.1 为了以服务形式运行,服务进程在内部调用StartServiceCtrlDispatcher,来注册服务主函数SvcMain的地址。
2.2 调用StartServiceCtrlDispatcher时,返回服务控制器的StartService函数
2.3 SCM调用服务进程的服务主函数SvcMain
(3)服务进程调用SetServiceStatus
3.1 虽然已经创建了服务进程,但尚未以服务形式运行。当前状态仍为SERVICE_START_PENDING
3.2 服务主函数SvcMain内部调用SetServiceStatus(SERVICE RUNNING)API后,才正式以服务进程形式运行

DebugMe1.exe 讲解

DebugMe1.exe进程以2种形式运行,一种为常规运行形式,负责服务的安装与删除;另一种由SCM以服务形式运行。执行完DebugMe1.exe install后发现存在一个SvcTest服务。之后启动服务,发现服务进程DebugMe1.exe开始运行注意到:SvcTest服务的进程(DebugMe1.exe)是以services.exe进程的子进程形式运行的。所有服务进程都以services.exe进程的子进程形式运行。Services.exe进程就是SCM。

DebugMe1.exe的作用是:经过一定时间间隔输出调试字符串。源代码&注释如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
#include <windows.h>
#include <tchar.h>
#include <stdio.h>

#define SVCNAME L"SvcTest"

VOID InstallService(LPCTSTR szSvcName, LPCTSTR szPath);
VOID UninstallService(LPCTSTR szSvcName);
VOID WINAPI SvcMain(DWORD argc, LPCTSTR *argv);
VOID WINAPI SvcCtrlHandler( DWORD dwCtrl );

SERVICE_STATUS_HANDLE g_hServiceStatusHandle = NULL;
SERVICE_STATUS g_ServiceStatus = {SERVICE_WIN32_OWN_PROCESS, 0, 0xFF, 0, 0, 0, 0};

// 程序入口
void _tmain(int argc, TCHAR *argv[])
{
TCHAR szPath[MAX_PATH] = {0,};
// 定义服务表
SERVICE_TABLE_ENTRY DispatchTable[] =
{
{ SVCNAME, (LPSERVICE_MAIN_FUNCTION)SvcMain },
{ NULL, NULL }
};

// 无参数
if( argc == 1 )
{
if (!StartServiceCtrlDispatcher( DispatchTable ))
{
_tprintf(L"StartServiceCtrlDispatcher() failed!!! [%d]\n",
GetLastError());
}
}
// 有参数
else if( argc == 2 )
{
// 获取当前进程的路径名
if( !GetModuleFileName(NULL, szPath, MAX_PATH) )
{
_tprintf(L"GetModuleFileName() failed! [%d]\n",
GetLastError());
return;
}

if( _tcsicmp(argv[1], L"install") == 0 )
{
InstallService(SVCNAME, szPath);
return;
}
else if( _tcsicmp(argv[1], L"uninstall") == 0 )
{
UninstallService(SVCNAME);
return;
}
else
{
_tprintf(L"Wrong parameters!!!\n");
}
}

_tprintf(L"\nUSAGE : %s <install | uninstall>\n", argv[0]);
}

// 相当于StartService
VOID InstallService(LPCTSTR szSvcName, LPCTSTR szPath)
{
SC_HANDLE schSCManager = NULL;
SC_HANDLE schService = NULL;
DWORD dwError = 0;

schSCManager = OpenSCManager(
NULL, // local computer
NULL, // ServicesActive database
SC_MANAGER_ALL_ACCESS); // full access rights
if( NULL == schSCManager )
{
_tprintf(L"InstallService() : OpenSCManager failed (%d)\n", GetLastError());
return;
}

// 创建服务
schService = CreateService(
schSCManager, // SCM database
szSvcName, // name of service
szSvcName, // service name to display
SERVICE_ALL_ACCESS, // desired access
SERVICE_WIN32_OWN_PROCESS, // service type
SERVICE_DEMAND_START, // start type
SERVICE_ERROR_NORMAL, // error control type
szPath, // path to service's binary
NULL, // no load ordering group
NULL, // no tag identifier
NULL, // no dependencies
NULL, // LocalSystem account
NULL); // no password
if( NULL == schService )
{
dwError = GetLastError();
_tprintf(L"InstallService() : CreateService failed (%d)\n", dwError);
if( ERROR_SERVICE_EXISTS == dwError )
_tprintf(L" -> The specified service already exists.\n");
goto _EXIT;
}

_tprintf(L"InstallService() : Service installed successfully\n");

_EXIT:
if( schService ) CloseServiceHandle(schService);
if( schSCManager) CloseServiceHandle(schSCManager);
}

VOID UninstallService(LPCTSTR szSvcName)
{
SC_HANDLE schSCManager = NULL;
SC_HANDLE schService = NULL;
SERVICE_STATUS ss = {0,};
DWORD dwError = 0;

schSCManager = OpenSCManager(
NULL, // local computer
NULL, // ServicesActive database
SC_MANAGER_ALL_ACCESS); // full access rights
if( NULL == schSCManager )
{
_tprintf(L"UninstallService() : OpenSCManager failed (%d)\n", GetLastError());
return;
}

schService = OpenService(
schSCManager, // SCM database
szSvcName, // name of service
SERVICE_INTERROGATE |
DELETE); // need delete access
if( NULL == schService )
{
dwError = GetLastError();
if( dwError != ERROR_SERVICE_DOES_NOT_EXIST )
_tprintf(L"UninstallService() : OpenSCManager failed (%d)\n", dwError);

goto _EXIT;
}

ControlService(schService, SERVICE_CONTROL_INTERROGATE, &ss);
if( ss.dwCurrentState != SERVICE_STOPPED )
{
_tprintf(L" -> Service is running! Stop the service!!!\n");
goto _EXIT;
}

if( !DeleteService(schService) )
_tprintf(L"UninstallService() : DeleteService failed (%d)\n", GetLastError());
else
_tprintf(L"Service uninstalled successfully\n");

_EXIT:
if( schService ) CloseServiceHandle(schService);
if( schSCManager ) CloseServiceHandle(schSCManager);
}

VOID WINAPI SvcMain(DWORD argc, LPCTSTR *argv)
{
// Service Control Handler
// 注册服务处理函数SvcCtrlHandler
g_hServiceStatusHandle = RegisterServiceCtrlHandler(
SVCNAME,
SvcCtrlHandler);
if( !g_hServiceStatusHandle )
{
OutputDebugString(L"RegisterServiceCtrlHandler() failed!!!");
return;
}

// 服务进程调用SetServiceStatus
// Service Status -> SERVICE_RUNNING
g_ServiceStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(g_hServiceStatusHandle, &g_ServiceStatus);

// Print debug string
while( TRUE )
{
OutputDebugString(L"[SvcTest] service is running...");
Sleep(3 * 1000); // 3 sec
}
}

VOID WINAPI SvcCtrlHandler(DWORD dwCtrl)
{
switch(dwCtrl)
{
case SERVICE_CONTROL_STOP:
g_ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING;
SetServiceStatus(g_hServiceStatusHandle, &g_ServiceStatus);

g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
SetServiceStatus(g_hServiceStatusHandle, &g_ServiceStatus);

OutputDebugString(L"[SvcTest] service is stopped...");
break;

default:
break;
}
}

补充:

1
2
3
Windows 服务:扩展服务和标准服务
1. 扩展服务是由第三方应用程序提供的服务,通常是在安装应用程序时一起安装的。
2. 标准服务是 Windows 操作系统自带的服务,它们与操作系统的正常运行有关。

服务进程调试

 服务程序的调试与普通程序不同,我们需要将调试器附加到SCM运行的服务进程上

 为什么?(1)服务进程由SCM运行;(2)服务核心代码主要存在于服务主函数(SvcMain)中;(3)服务主函数(SvcMain)由SCM正常调用。我们要调试的是服务主函数(SvcMain),但使用调试器打开服务程序的可执行文件并开始调试时,服务主函数并不运行,所以调试时需要将SCM运行的服务进程附加到调试器。由于此时的服务主函数(SvcMain)可能已开始运行。因此,需要在SCM创建服务进程并运行EP代码附加到调试器(具体怎么说后面说)。服务程序的主要代码存在于服务主函数(SvcMain)与服务处理函数(SvcHandler)中。

 另一种解决方法:将调试位置强制指定为服务主函数(SvcMain)(如:OllyDbgNew orign here),然后再调试。

调试位置强制指定

 跟着书做就可以。

 对于EXE文件形态的Windows服务程序而言,必须在其EP代码内部调用StartServiceCtriDispatcher,将服务函数(SvcMain)的地址通知给SCM。对于DLL文件形式的 Windows 服务而言,服务主函数(SvcMain)为导出函数,SCM会调用运行导出函数,所以不需要另外调用StartServiceCtrlDispatcher

 需要注意,由于服务进程不是由 SCM正常启动运行的,所以调用与服务相关的部分 API时可能引发异常。为了避免这种异常可以设置调试器选项,从而忽略某些异常。

将调试器附加到SCM运行的服务进程

 跟着书做就可以。

 下图是调试流程图,其中EP代码指的是SvcMain

image-20230525173204399

 以上操作流程的核心是,将服务进程附加到调试器前要进入无限循环,使服务进程的重要代码无法运行。但是,有超时时间。启动服务后,SCM会在一定时间内等待服务状态变为STATUS_RUNNING。若规定时间(一般是30s)内服务状态未改变,SCM就会引发ERROR_SERVICE_REQUEST_TIMEOUT错误,然后终止相关服务进程。

 先安装服务程序。

 30s内我们要附加到调试器并恢复EP代码,时间不够用。所以把30s改了。

 改完之后,开始设置无限循环。0xEB 0xFE是无限循环指令,为什么?操作码0xEB是近距离JMP 指令,带有1个字节大小的值,该值为有符号数,指的是与Next EIP 的相对距离,计算时有如下公式:

1
JumpAddress = Next_EIP + 0xFE(-2)

 启动服务程序。SCM会主动找我们改过的服务程序,并陷入循环。我的win7出现了书中的情况,就算改了30s,系统也会自动杀掉,所以没有实际跟进后续实验流程。

0x01 自我创建程序的调试

 有些应用程序在运行过程中可以将自身创建为子进程运行,这种方式称为自我创建。相同的可执行文件在以父进程运行和以子进程运行时分别表现出不同的行为特征。

 实验DebugMe2.exe,父进程用来在控制台窗口中输出字符串,并运行子进程。而子进程在消息窗口中输出字符串。

工作原理

image-20230525183107408

1
2
3
4
5
6
(1)创建子进程。
父进程运行时,main函数就会被调用执行,以挂起模式创建子进程。
子进程以挂起模式创建后,所需的DLL被加载进来,但子进程的主线程处于暂停状态。
(2)更改EIP。
当前子进程的主线程处于暂停状态,先获取其上下文,然后将EIP修改为指定地址值即可。
(3)恢复主线程。

 源代码&注释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <windows.h>
#include <tchar.h>
#include <stdio.h>

// 子进程
void ChildProc()
{
MessageBox(NULL, L"This is a child process!", L"DebugMe2", MB_OK);

ExitProcess(0);
}

// 程序入口
void _tmain(int argc, TCHAR *argv[])
{
TCHAR szPath[MAX_PATH] = {0,};
STARTUPINFO si = {sizeof(STARTUPINFO),};
PROCESS_INFORMATION pi = {0,};
CONTEXT ctx = {0,};

_tprintf(L"This is a parent process!\n");

// 获得当前程序路径
if( !GetModuleFileName(NULL, szPath, sizeof(TCHAR) * MAX_PATH) )
{
printf("GetModuleFileName() failed! [%d]\n", GetLastError());
return;
}

// Create Child Process
// 创建子进程,挂起状态
if( !CreateProcess(
szPath,
NULL,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED,
NULL,
NULL,
&si,
&pi) )
{
printf("CreateProcess() failed! [%d]\n", GetLastError());
return;
}

// Change EIP
// 获得子进程的主线程的context结构体
ctx.ContextFlags = CONTEXT_FULL;
if( !GetThreadContext(pi.hThread, &ctx) )
{
printf("GetThreadContext() failed! [%d]\n", GetLastError());
return;
}
// 修改EIP
ctx.Eip = (DWORD)ChildProc;

if( !SetThreadContext(pi.hThread, &ctx) )
{
printf("SetThreadContext() failed! [%d]\n", GetLastError());
return;
}

// Resume Main Thread
// 唤醒子进程的主线程
if( -1 == ResumeThread(pi.hThread) )
{
printf("ResumeThread() failed! [%d]\n", GetLastError());
return;
}

WaitForSingleObject(pi.hProcess, INFINITE);

CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}

调试

 对于attach程序来说,由于子进程是调试中新创建的,所以首先要调试父进程,查看子进程的EP被修改为哪一地址。

 这要求我们要从启动时就开始调试,可以在EP地址处设置无限循环来解决。还有一种解决办法,JIT(Just-In-Time)调试法

 可不可以在子进程创建的瞬间用调试器调试?不可以,因为此时子进程是挂起的,调试器无法调试被挂起的进程。

 可不可以直接用调试器attach子进程?应该可以,没试过。

JIT调试

运行中的进程发生异常时,OS会自动运行调试器,附加发生异常的进程。由于可以从异常发生的位置开始调试,所以采用这种方式很容易把握出现异常的原因。

 之后按照书上P626之前来就行。

0x02 PE映像切换调试

 先运行某个进程,然后将其虚拟内存中的PE映像切换为另一个PE映像,这称为PE映像切换。

工作原理

 PE映像切换:先以挂起模式运行某个进程(A.exe),然后将完全不同的一个PE文件(B.exe)的PE映像映射到A.exe进程内存空间,并在A.exe进程的内存空间中运行。修改PE映像后,进程名称仍为原来的A.exe,但实际映射在进程内存中的PE映像为B.exe,所以最终会产生与原来 (A.exe)完全不同的行为动作。此时,A.exe为外壳进程,B.exe为内核进程。

补充:

1
2
CUI:控制台程序
GUI:图形窗口程序

 书中的实例中,给出了DebugMe3.exefake.exereal.exefake.exe在命令行中输出字符串,real.exe在图形化窗口上输出字符串,运行DebugMe3.exe fake.exe real.exe,发现虽然运行着fake.exe,但是输出图形化窗口。

大量的调试分析是逃不过的,这里为了加快速度,所以对于DebugMe3.exe的分析直接按书上的来了,但是要记住,这都是之前欠下的,以后还要还回去。

 main函数的流程图如下:

image-20230525223046447

 接下来分析SubFunc_1函数。经过分析,此函数是将real.exe读入内存,real.exe的内存地址叫做MEM_FILE_REAL_EXE

 继续分析SubFunc_2函数。首先,此函数调用了GetThreadContext,获得了fake.exe的主线程上下文。之后,通过主线程上下文获得fake.exe的PEB,并调用ReadProcessMemory获得fake.exe的内存映射地址。代码如下所示:

image-20230528105555214

注:由于当前fake.exe进程是以挂起模式创建的,处于暂停状态。进程被创建出来时,PE装载器就会将PEB结构体的地址设置给上下文中的EBX寄存器。

 接下来,获取real.exe文件的ImageBase地址。如下所示:

image-20230528110023406

image-20230528110033991

 EDI寄存器的值为MEM_FILE_REAL_EXE地址(real.exe的内存地址)。所以EDI+3C指的就是IMAGE_DOS_HEADER结构体的elfanew成员(NT头的偏移)。之后,EAX+EDI=elfanew+StartofPEIMAGE_NT_HEADER结构体的起始地址,EAX+EDI+34指的是IMAGE_OPTION_HEADER.ImageBase成员。

 之后,比较fake.exe进程的实际映射地址与real.exe文件的ImageBase值。若两值相同,由于fake.exe的PE映像已经映射到某地址处,而此地址也是real.exe的PE映像要映射的地址。若将real.exe强行映射到该地址处,就会发生冲突,所以必须先卸载fake.exe的PE映像的映射。由于fake.exe进程处于挂起状态,所以卸载PE映像时不会发生错误。使用ZwUnmapViewOfSection来卸载,如下所示:

image-20230528111252905

 若两值不同,不必非得卸载fake.exe的PE映像,可以先在fake.exe进程的虚拟内存空间(4G内存)中为real.exe的PE映像分配所需空间,然后将real.exe映射进去就可以了。接下来还要告知PE装载器,fake.exe进程的PE映像是real.exeImageBase地址。如下所示,调用WriteProcessMemory,将fake.exe进程的PEB.ImageBase值修改为real.exe文件的ImageBase值:

image-20230528112327560

 至此,SubFunc_2分析完毕。

 接下来分析SubFunc_3函数。此函数负责把real.exe文件映射到fake.exe进程。首先,使用VirtualAllocEx,为real.exe的PE映像分配内存(之前读取的是real.exe文件,而不是映像)(分配的内存是real.exeImageBase地址)。其次,将real.exe映射到fake.exe进程:(1)映射PE文件头。具体来说,就是调用WriteProcessMemory,将real.exe的PE文件头写入到刚刚分配的内存区域。(2)映射PE节区。反复调用WriteProcessMemory,映射PE节区。循环结束后,real.exe文件被完全映射至real.exeImageBase。(3)修改EP。获取fake.exe的主线程上下文后,如下所示:

image-20230528121643027

 如上图所示,eax存的值是fake.exe原来的EP地址,eip存的值为ntdll!RtlUserThreadStart的起始地址,因此,我们可以总结程序挂起后重新运行之后做的事:处于挂起状态的fake.exe进程恢复运行后,首先会调用ntdll!RtlUserThreadStart,跳转至EP地址处(Eax)。由于前面已经将fake.exe进程的PE映像替换为real.exe,所以需要将Eax修改为real.exe的EP地址(使用SetThreadContext)。最后,调用ResumeThread恢复进程。

 总结一下流程:

image-20230528124823078

调试

 如何调试real.exe呢?

 将real.exe文件映射到fake.exe进程前,向real.exe的EP代码设置无限循环。此时,借助调试器的附加功能将其附加到调试器即可调试。

0x03 Debug Blocker调试

Debug Blocker是一种反调试技术,指的是进程以调试模式运行自身或其他可执行文件的技术。父进程是调试器,子进程是被调试者,且程序在作为调试器与被调试者时分别执行不同的代码。

注:使用CreateProcess创建进程时,若选用了DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS参数,则创建出的父子进程会形成调试器与被调试者的关系。

Debug Blocker特征

 调试器与被调试者关系中,调试进程与被调试进程是一种父子关系。

 被调试进程不能被其他调试器调试。若想调试被调试进程,必须先切断原调试器与被调试者的关系。

 终止调试进程的同时也终止被调试进程。当强制终止调试进程以切断调试器-被调试者关系时,被调试进程也会同时终止。

 调试器操作被调试者的代码。调试器用来操纵被调试进程的运行分支,生成或修改执行代码等。因此,缺少调试进程的前提下,仅凭被调试进程无法正常运行。

 调试器处理被调试进程中发生的异常。被调试进程中发生异常时,进程会暂停,控制权转移到调试进程。此时调试器可以修改被调试者的执行分支,此外也可以对被调试进程内部的加密代码解密,或者向寄存器、栈中存入某些特定值等。

调试

 调试某个程序,程序逻辑是:父进程会在控制台窗口输出字符串,而子进程会在消息框输出字符串。

 程序首先使用CreateMutexW创建互斥体对象,之后调用GetLastError,并于0xB7比较,如果相同,则说明以子进程运行,否则程序以父进程运行。父进程调用CreateProcessW,该函数使用DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS参数(调试模式)创建自身进程。以Debug模式运行后,父进程为调试器,子进程为被调试者。

 Debug Blocker的程序的核心代码一般都在子进程中运行。我们在0xB7比较阶段更改分支,进入子进程。子程序第一行代码为lea eax, eax,由于lea的第2操作数一般为内存,所以会报非法指令异常。这样就会把控制权交给父进程,获得控制权后,父进程会处理子进程中发生的异常,同时还会做一些其他事情。我们可以猜想:父进程(调试器)获取控制权后可能会使用这些指令对加密的代码解密,或者将执行分支修改为其他地址。到这里,我们已经不能继续采用这种方式调试了。要继续调试程序,必须详细分析父进程(调试器)的运行代码。

 接下来继续调试父进程。发现父进程中调用了WaitForDebugEvent,等待被调试进程发生Debug事件。WaitForDebugEvent如下:

image-20230528191714515

 若调用WaitForDebugEvent,将在wMilliseconds时间内等待被调试进程发生Debug。被调试进程发生异常时将返回WaitForDebugEvent,且相关异常信息就会填充到IpDebugEvent指针所指的DEBUG_EVENT结构体。此结构体定义如下:

image-20230528191905862

 然后会比较Debug码与1(Exception)的关系,我们只需要关注等于1的时候即可。具体过程见书中,比较复杂,笔记中并未记全。

 补充:条件记录断点(Conditional Log Break Point:CLBP),满足某个条件,记录到日志中。

 总结一下这个程序的执行流程:(子进程的两次指令异常,此时交还给父进程处理)

image-20230528202748310

image-20230528203243095

 上述程序调试需要多次调试,子进程与父进程都调试。那如果我们需要直接调试它的子进程(被调试进程),该如何处理呢?

 方法1:静态。详细分析调试过程,得到解码代码,然后直接修改程序的PE文件或进程内存,从而达到用OllyDbg调试器调试的目的。

 方法2:动态。(1)使用OllyDbg调试父进程,调试运行到要分析的地方(或者子进程代码完成解码的地方);(2)在子进程中要分析的代码处设置无限循环;(3)将父进程从子进程中分离(Detach);(4)附加OllyDbg调试器到子进程。具体看书中P669中的内容。

我不是什么天才,但只要不断努力,照样能成为代码逆向分析高手。

 送给自己。

留言

© 2024 wd-z711

⬆︎TOP