反调试&反反调试&OD插件的编写

windows异常分发回顾

异常的分发一共有多少轮?

一共有两轮。 KiDispatchExcption 函数的最后一个参数 FirstChance 表示当前是第几次进行异常的分发,另一个函数 RaiseException 最后一个参数也表示当前是第几次分发

通过什么可以区分当前所处的是 R0 还是 R3

在 windows 下,代码被分为了 R3 和 R0 权限,CS 段寄存器的最低两位就表示当前所处的是 3环(用户) 还是 0环(内核),可以通过 mov eax, cs + test eax, 1 区分

异常的产生方式有多少种

CPU 满足特定的条件之后,内部主动产生的异常,类似 int 3(IDT)

用户通过 RaiseException 构建 ExceptionRecord 主动抛出异常(KiDispatchException)

编译器会为用户自定义的 __try __except 添加怎样的异常处理函数

在同一个函数内,无论用户编写了多少个 SEH,编译器只会安装一个 except_handler4

当用户模式下产生异常时,SEH 函数会在什么时候被调用

int3 -> idt(3) -> _KiTrap03 -> CommonDispatchException -> KiDispatchExceptijon

-> KeUserExceptionDispatcher(3) -> RtlDispatchException(3) -> RtlpExecuteHandlerForException(3) -> except_handler4 -> except_handler4_common

-> 用户通过 _try _except 安装的异常处理函数

在 R0 中异常是如何被传递给三环调试器的

DbgkForwardException -> DbgkpSendApiMessage -> 三环调试器

R0 和 R3 的 RtlDispathException 有什么区别

KiDispatchException(0) -> RtlDispathException(0) -> SEH

KiUserExceptionDispatcher(3) -> RtlDispathException(3) -> VEH SEH UEF (VCH)

反调试与反反调试

反调试技术术语高级逆向分析技术范畴,并且涉及面异常广阔,几乎任何技术在经过精心的构造后都可以变成反调试技术

未公开的数据结构查询网站:https://www.vergiliusproject.com/

  • 静态反调试:一般在调试开始时阻拦调试者,调试者只需要找到原因后可一次性突破
  • 动态反调试:一般在调试过程中阻拦调试者,可在调试的过程中被频繁触发,因此需要调试者随时关注

对于所有的用户层 PEB 静态反调试,可以在程序正式的运行之前先挂起用户程序,然后修改相应的字段为非调试模式,再继续执行

BeginDebug

BeginDebug: 调试标记位

当程序被调试的时候,BegingDebugged 字段保存的是1

1
2
3
4
5
6
7
8
__asm
{
; 通过 FS : [0x30] 获取到 PEB 结构体的地址
mov eax, dword ptr fs:[0x30]

; 通过 PEB 偏移为 0x02 的地方获取到 BeingDebugged
movzx eax, byte ptr [eax + 0x02]
}

IsDebuggerPresent

IsDebuggerPresent:原理同上,检查 PEB.BeingDebugged 字段

1
2
3
4
if (IsDebuggerPresent())
printf("调试状态\n");
else
printf("非调试状态\n");

NTGlobalFlag

NTGlobalFlag:在当前进程处于调试状态时值为 0x70 ,可以通过判定此值来确定是否被调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int NtGlobalFlag = 0;

__asm
{
; 通过 TEB 偏移为 0x30 找到 PEB 结构
mov eax, dword ptr fs : [0x30]

; 通过 PEB 偏移为 0x68 的地方找到 NtGlobalFlag
mov eax, dword ptr[eax + 0x68]

; 将结果保存到变量,目的是方便比较
mov NtGlobalFlag, eax
}

bool result = NtGlobalFlag == 0x70 ? true : false;

_HEAP

_HEAP:没有被公开的结构体,不同版本的 NT 内核可能对这个结构体有不同的实现

ProcessHeap 的 Flags 与 ForceFlags 在正常情况下应为 2 与 0 ,处于调试状态时值会发生改变,此方法在 NT 5.X 版本以上无效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int Flags = 0, ForceFlags = 0;

__asm
{
; 通过 fs : [0x30] 可以找到 PEB 的地址
mov eax, dword ptr fs:[0x30]

; 通过 PEB 偏移为 0x18 的位置找到 ProcessHeap(_HEAP)
mov eax, dword ptr [eax + 0x18]

; 通过 _HEAP 偏移为 0x400x44 的字段找到两个标志
mov ecx, dword ptr [eax + 0x40]
mov Flags, ecx
mov ecx, dword ptr [eax + 0x44]
mov ForceFlags, ecx
}

printf("%08X %08X\n", Flags, ForceFlags);

bool result = (Flags != 2 || ForceFlags != 0);

ProcessDebugPort

NTQueryInformationProcess() 是一个可以同时在 R0 及 R3 运行的函数,它的主要作用是查看进程相关的各种信息

根据想要查看的信息类别不同,我们给其第二个参数 ProcessInformationClass 传的值也就不同,根据 ProcessInformationClass 的类别可知,此函数可以查看大概60余种进程相关的信息

ProcessDebugPort 可以获取目标进程的调试端口,如果目标进程未处于调试状态,此端口为0,否则为0xFFFFFFFF

1
2
3
4
5
// 包含头文件
#include <iostream>
#include <windows.h>
#include <winternl.h>
#pragma comment(lib,"ntdll.lib")
1
2
3
4
5
6
7
8
9
10
int nDebugPort = 0;

NtQueryInformationProcess(
GetCurrentProcess(), // 目标进程句柄
ProcessDebugPort, // 查询信息类型
&nDebugPort, // 输出查询信息
sizeof(nDebugPort), // 查询类型大小
NULL); // 实际返回数据大小

bool result = nDebugPort == 0xFFFFFFFF ? true : false;

ProcessDebugObjectHandle

ProcessDebugObjectHandle 可以获取目标进程的调试对象句柄,如果未处于调试状态则获取的值为 NULL,如果当前的程序被调试了,那么保存的就是非零值

1
2
3
4
5
6
7
8
9
10
HANDLE hProcessDebugObjectHandle = 0;

NtQueryInformationProcess(
GetCurrentProcess(), // 目标进程句柄
(PROCESSINFOCLASS)0x1E, // 查询信息类型
&hProcessDebugObjectHandle, // 输出查询信息
sizeof(hProcessDebugObjectHandle), // 查询类型大小
NULL); // 实际返回大小

bool result = hProcessDebugObjectHandle ? true : false;

ProcessDebugFlag

ProcessDebugFlag 可以获取目标进程的调试标记,如果处于调试状态其值为1,否则为0

1
2
3
4
5
6
7
8
9
10
BOOL bProcessDebugFlag = 0;

NtQueryInformationProcess(
GetCurrentProcess(), // 目标进程句柄
(PROCESSINFOCLASS)0x1F, // 查询信息类型
&bProcessDebugFlag, // 输出查询信息
sizeof(bProcessDebugFlag), // 查询类型大小
NULL); // 实际返回大小

bool result = bProcessDebugFlag ? false : true;

ProcessBasicInformation

ProcessBasicInformation 可以获取指定进程的父进程 PID ,我们可以将其与 Explorer.exe(资源管理器) 的PID进行对比,如果不匹配则证明此进程不是被双击运行的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct PROCESS_BASIC_INFORMATION {
ULONG ExitStatus; // 进程返回码
PPEB PebBaseAddress; // PEB地址
ULONG AffinityMask; // CPU亲和性掩码
LONG BasePriority; // 基本优先级
ULONG UniqueProcessId; // 本进程PID
ULONG InheritedFromUniqueProcessId; // 父进程PID
}stcProcInfo;

// 查询到进程相关的基本信息,需要提供一个结构体进行接收
NtQueryInformationProcess(GetCurrentProcess(), ProcessBasicInformation,
&stcProcInfo, sizeof(stcProcInfo), NULL);

DWORD ExplorerPID = 0;
DWORD CurrentPID = stcProcInfo.InheritedFromUniqueProcessId;

// 以资源管理器的类名查询到资源管理所在的进程PID
GetWindowThreadProcessId(FindWindow(L"Progman", NULL), &ExplorerPID);

// 如果相同就说明没有被调试
bool result = ExplorerPID == CurrentPID ? false : true;

NtQuerySystemInformation

可以使用 NtQuerySystemInformation() 函数获取当前系统是否开启调试模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 保存了和系统调试相关的属性
struct _SYSTEM_KERNEL_DEBUGGER_INFORMATION
{
BOOLEAN KernelDebuggerEnabled;
BOOLEAN KernelDebuggerNotPresent;
} DebuggerInfo = { 0 };

// 查询当前系统的调试情况
NtQuerySystemInformation(
(SYSTEM_INFORMATION_CLASS)0x23, // 查询信息类型
&DebuggerInfo, // 输出查询信息
sizeof(DebuggerInfo), // 查询类型大小
NULL); // 实际返回大小
return DebuggerInfo.KernelDebuggerEnabled;

ZwSetInformationThread

可以使用 ZwSetInformationThread() 函数主动脱离调试器,使自己与调试器的调试关系分离,进而达到反调试的目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef enum THREAD_INFO_CLASS {
ThreadHideFromDebugger = 17
};

// 一个未公开的函数,需要手动的使用 GetProcAddress 获取
typedef NTSTATUS(NTAPI* ZW_SET_INFORMATION_THREAD)(
IN HANDLE ThreadHandle,
IN THREAD_INFO_CLASS ThreadInformaitonClass,
IN PVOID ThreadInformation,
IN ULONG ThreadInformationLength);

void ZSIT_DetachDebug()
{
ZW_SET_INFORMATION_THREAD Func = (ZW_SET_INFORMATION_THREAD)
// 加载了 ntdll 模块,因为函数保存在这个模块中
GetProcAddress(LoadLibrary(L"ntdll.dll"),
// 函数的名称
"ZwSetInformationThread");

// 对调试器隐藏当前的线程,原理上就是让 DbgkpSendApiMessage 函数
// 不向建立了调试关系的调试器发送调试信息。
Func(GetCurrentThread(), ThreadHideFromDebugger, NULL, NULL);
}

FindWindow

FindWindow() 函数查询窗口名

1
2
3
4
if (FindWindow(L"OllyDbg", NULL))
printf("存在调试器\n");
else
printf("没检测到调试器\n");

插件加载的原理

  • 应用程序如何找到自己的插件:

所有支持插件的应用程序,都会存在一个插件路径,用户提供的就应该放置到这个路径底下,应用程序通过遍历文件的方式来确保能够找到插件。

  • 插件的存在形式是什么?

插件的存在形式通常都是 dll 文件,这些文件可以是任何后缀名结尾的,并不影响插件本身的功能

  • 应用程序如何识别插件路径下的模块是否是插件?

一个合格的插件,应该能够提供相应的导出函数说明当前插件的名称,版本以及能够支持的应用程序。

  • 插件是如何提供功能的?

通过实现指定的导出函数可以提供相应的功能。

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
#include <vector>
#include <string>
#include <iostream>
#include <windows.h>
using namespace std;

// 定义一个函数指针的别名
using f_query = bool (*)(int v, char name[20], char version[20]);
using f_run = void (*)();

// 用于保存插件信息的结构体
typedef struct _PLUGIN_INFO
{
char name[20];
char version[20];
HMODULE module;
} PLUGIN_INFO, *PPLUGIN_INFO;

// 定义容器用于保存所有的插件
vector<PLUGIN_INFO> plugins;

// 初始化函数
void init()
{
// 保存找到的文件的信息
WIN32_FIND_DATAA FileInfo = { 0 };

// 1. 遍历当前的插件目录,搜索所有的模块
HANDLE FindHandle = FindFirstFileA(".\\plugin\\*.plugin", &FileInfo);

// 2. 如果说第一个文件查找成功就继续查找下一个
if (FindHandle != INVALID_HANDLE_VALUE)
{
do {
// 保存当前插件的信息
PLUGIN_INFO plugin_info = { 0 };

// 模块文件的路径
string path = string(".\\plugin\\") + FileInfo.cFileName;

// 尝试使用 LoadLibrary 加载模块
plugin_info.module = LoadLibraryA(path.c_str());

// 判断一下这是不是一个有效的模块
if (plugin_info.module != NULL)
{
// 判断当前有没有导出指定函数
f_query f = (f_query)GetProcAddress(plugin_info.module, "query");

// 如果函数获取成功,并且函数返回true就表示这是一个有效的插件

if (f != nullptr && f(1, plugin_info.name, plugin_info.version))
{
printf("插件: [%s %s] 已经被加载了\n",
plugin_info.name, plugin_info.version);

plugins.push_back(plugin_info);
}
}

} while (FindNextFileA(FindHandle, &FileInfo));
}
}

// 运行期间的函数
void run()
{
// 可以遍历插件列表,调用其中约定好的函数
for (auto& p : plugins)
{
// 获取函数,这个函数可以不提供
f_run f = (f_run)GetProcAddress(p.module, "run");

if (f) f();
}
}

// 清理资源的函数
void release()
{
// 一般会在 release 调用插件的清理函数
for (auto& p : plugins)
{
// 获取函数,这个函数可以不提供
f_run f = (f_run)GetProcAddress(p.module, "release");

if (f) f();
}
}

// 结束进程前调用的函数
void my_exit()
{
// 程序结束前应该卸载插件
for (int i = 0; i < plugins.size(); ++i)
{
// 将插件卸载(从内存空间移除)
// 必须在卸载所有可能调用插件函数之后卸载插件
FreeLibrary(plugins[i].module);
}
}

int main()
{
// 通常在程序运行的时候加载插件
init();

run();

release();
my_exit();

return 0;
}

实例插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <windows.h>
#include <iostream>

// 插件导出一个约定好的函数,函数告诉了应用程序自己的名称版本等信息
extern "C" __declspec(dllexport) bool query(int v, char name[20], char version[20])
{
// 参数一: 传入的参数,告诉插件自己的版本信息
// 参数二: 传出的参数,告诉应用程序自己的名称
// 参数三: 传出的参数,告诉应用程序自己的版本

// 如果目标应用不是 1.0 版本,该插件就不支持
if (v != 1)
return false;

memcpy(name, "plugin2", 8);
memcpy(version, "2.0", 4);

return true;
}

extern "C" __declspec(dllexport) void run()
{
printf("这是插件提供的功能");
}

OD插件的编写

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
#include "Plugin/od_plugin.h"
#pragma comment(lib, "Plugin/ollydbg.lib")

#include <winternl.h>
#pragma comment(lib,"ntdll.lib")

// 1. 包含头文件和链接LIB
// 2. 项目属性->C/C++->命令行选项 \J(使用无符号char)

#define PLUGINNAME L"testplugin" // 插件名称
#define MY_VERSION L"0.00.01" // 插件版本

// 创建用于保存菜单的数组,菜单必须以一个全 0 结构结尾
t_menu MainMenu[2] = { 0 };
t_menu DasmMenu[2] = { 0 };

// 必须导出的函数,用于提供插件的名字和版本号,并且检查是否符合要求
extc int __cdecl ODBG2_Pluginquery(
int ollydbgversion,
ulong *features,
wchar_t pluginname[SHORTNAME],
wchar_t pluginversion[SHORTNAME])
{
// 1. 检查OllyDBG的兼容版本
if (ollydbgversion < 201)
return 0;

// 2. 设置OllyDBG插件的名称与版本
wcscpy_s(pluginname, SHORTNAME, PLUGINNAME); // 设置插件名称
wcscpy_s(pluginversion, SHORTNAME, MY_VERSION); // 设置插件版本
// 3. 返回需要的API版本

return PLUGIN_VERSION;
};

// 菜单的回调函数,返回值是菜单的显示方式
int MenuFunc(t_table *pt, wchar_t *name, ulong index, int mode)
{
// 如果是第一次显示菜单,会是 MENU_VERIFY 方式
if (mode == MENU_VERIFY)
{
if (index == 1)
return MENU_CHECKED | MENU_NORMAL;
else
return MENU_NORMAL;
}

if (mode == MENU_EXECUTE)
{
if (index == 1)
MessageBoxA(0, "主菜单", "插件", MB_OK);
else
MessageBoxA(0, "汇编菜单", "插件", MB_OK);
}

return MENU_NORMAL;
}

// 插件第二个被调用的函数,可以不被提供
extc int __cdecl ODBG2_Plugininit(void)
{
// 对插件的初始化操作

// 设置主菜单的信息
MainMenu[0].index = 1; // 用于区分是哪一个菜单
MainMenu[0].name = (WCHAR*)L"主菜单"; // 菜单的名称
MainMenu[0].help = (WCHAR*)L"主菜单"; // 主菜单
MainMenu[0].menufunc = MenuFunc; // 菜单的回调函数

// 设置汇编菜单的信息
DasmMenu[0].index = 2;
DasmMenu[0].name = (WCHAR*)L"汇编菜单";
DasmMenu[0].help = (WCHAR*)L"汇编菜单";
DasmMenu[0].menufunc = MenuFunc;

return 0;
};

// 当有任何的菜单需要相应的时候,都会调用这个函数
extc t_menu* __cdecl ODBG2_Pluginmenu(wchar_t *type)
{
// 1. 判断是否为主菜单弹出请求,是则弹出主菜单
if (!wcscmp(type, PWM_MAIN))
return MainMenu;

// 2. 判断是否为CPU窗口右键菜单弹出请求,是则弹出右键菜单
if (!wcscmp(type, PWM_DISASM))
return DasmMenu;

// 3. 返回空表示不设置菜单
return NULL;
};

// 返回不同时刻产生的重要的信息
pentry(void) ODBG2_Pluginnotify(int code, void *data, ulong parm1, ulong parm2)
{
// 可以在这里进行反反调试
static int First = TRUE;

// 执行唯一的一次反调试
if (code == PN_NEWPROC && First)
{
DWORD Size = 0;

// 获取进程的句柄
HANDLE Handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, parm1);

// 保存进程的基本信息
PROCESS_BASIC_INFORMATION Base = { 0 };

// 获取被调试进程PEB
NtQueryInformationProcess(Handle,
ProcessBasicInformation,
&Base, sizeof(Base), &Size);

DWORD Peb = (DWORD)Base.PebBaseAddress;

// 写入数据
WriteProcessMemory(Handle, (LPVOID)(Peb + 2),"", 1, &Size);

First = FALSE;
}
}

插件测试环境

1
链接: https://pan.baidu.com/s/1Ycgi-D8abbE6bvKanLIVQw 提取码: gphs