Nicky_home⛄

Nicky模拟人生.Log

逆向工程核心原理

逆向工程核心原理

分析Hello World!

目标是在main()函数中找出调用MessageBox()函数的代码

image-20250104165730660

Win32 API函数( OllyDbg 注释中的红色 API函数调用部分)

2.4.3 API检索法(1):在调用代码中设置断点
鼠标右键莱单-Searchfor-Allintermodular calls【查找-所有模块调用】

目标是修改字符

image-20250104174758163

目的修改长字符 超过原来的长度

image-20250104175517580

在调试器里F9运行可以

image-20250104175932870

但是保存到文件会运行失败 因为修改的地址

可执行文件被加载到内存并以进程形式运行时,文件并非原封不动地被载入内存,而是要遵循一定规则进行。这一过程中,通常进程的内存是存在的,但是相应的文件偏移(ofset)并不存在。上面示例中,与修改字符后选定的内存地址对应的文件偏移就不存在,所以修改后的程序无法正常运行。

字节序

image-20250104180844966

image-20250104180243652

采用大端序存储数据时,内存地址低位存储数据的高位,内存地址高位存储数据的低位,这是一种最直观的字节存储顺序;

采用小端序存储数据时,地址高位存储数据的高位,地址低位存储数据的低位,这是一种逆序存储方式,保存的字节顺序被倒转,它是最符合人类思维的字节序。

存储器

基本程序运行寄存器
图4-1来自IA-32用户手册,描述了基本程序运行寄存器的组织结构,它由4类寄存器组成。

通用寄存器(GeneralPurposeRegisters,32位,8个)
段寄存器(SegmentRegisters,16位,6个)
程序状态与控制寄存器(Program Status and ControlRegisters,32位,1个)
指令指针寄存器(ImstructionPointer,32位,1个)

image-20250105204353938

通用寄存器

image-20250105205216061

各寄存器的名称如下所示
EAX:(针对操作数和结果数据的)累加器

EBX:(DS 段中的数据指针)基址寄存器

ECX:(字符串和循环操作的)计数器

EDX:(I/O指针)数据寄存器

以上4个寄存器主要用在算术运算(ADD、SUB、XOR、OR等)指令中,常常用来保存常量与变量的值。某些汇编指令(MUL、DIV、LODS等)直接用来操作特定寄存器,执行这些命令后,仅改变特定寄存器中的值。
此外,ECX与EAX也可以用于特殊用途。循环命令(LOOP)中,ECX用来循环计数(1oopcount ),每执行一次循环,ECX都会减1。EAX一般用在函数返回值中,所有Win32API函数都会先把返回值保存到EAX再返回。

EBP:(SS段中栈内数据指针)扩展基址指针寄存器

ESI:(字符串操作源指针)源变址寄存器

EDI:(字符串操作目标指针)目的变址寄存器

ESP:(SS段中栈指针)栈指针寄存器

以上4个寄存器主要用作保存内存地址的指针。ESP指示栈区域的栈顶地址,某些指令(PUSH、POP、CALL、RET)可以直接用来操作ESP栈区域管理是程序中相当重要的部分,请不要把ESP用作其他用途。EBP表示栈区域的基地址,函数被调用时保存ESP的值,函数返回时再把值返回ESP,保证栈不会崩溃(这称为栈帧(StackFrame)技术。ESI和EDI与特定指令(LODS、STOS、REP、MOVS等)一起使用,主要用于内存复制。

段是一种内存保护技术,它把内存划分为多个区段,并为每个区段赋予起始地址、范围、访问权限等,以保护内存。此外,它还同分页技术(Paging)一起用于将虚拟内存变更为实际物理内存。段内存记录在SDT(SegmentDescriptorTable,段描述符表)中,而段寄存器就持有这些SDT的索引(index)。

各段寄存器的名称如下
CS:Code Segment,代码段寄存器
SS: Stack Segment,栈段寄存器
DS:Data Segment,数据段寄存器
ES:Extra(Data)Segment,附加(数据)段寄存器
FS:Data Segment,数据段寄存器
GS:Data Segment,数据段寄存器

顾名思义,CS寄存器用于存放应用程序代码所在段的段基址,SS寄存器用于存放栈段的段基址,DS寄存器用于存放数据段的段基址。ES、FS、GS寄存器用来存放程序使用的附加数据段的段基址。

image-20250105210524570

3.程序状态与控制寄存器
EFLAGS:Flag Register,标志寄存器

ZF(ZeroFlag,零标志)、OF(OverflowFlag,溢出标志)、CF(Carry Flag,进位标志 )

ZF
若运算结果为0,则其值为1(True),否则其值为0(False)。

OF
有符号整数(signedinteger)溢出时,OF值被置为1。此外,MSB(MostSignificant Bit,最高有效位)改变时,其值也被设为1。

CF
无符号整数(unsignedinteger)溢出时,其值也被置为1。

4.指令指针寄存器
●EIP:Instruction Pointer,指令指针寄存器

指令指针寄存器保存着CPU要执行的指令地址,其大小为32位(4个字节)。程序运行时,CPU会读取EIP中一条指令的地址,传送指令到指令缓冲区后,EIP寄存器的值自动增加,增加的大小即是读取指令的字节大小。这样,CPU每次执行完一条指令,就会通过EIP寄存器读取并执行下一条指令

5.1 栈
栈内存在进程中的作用如下:
(1)暂时保存函数内的局部变量。
(2)调用函数时传递参数。
(3)保存函数返回后的地址。
栈其实是一种数据结构,它按照后进先出的原则存储数据

abex’crackme#1

File: abexcm1-voiees.exex
SHA1: 7d7e1656c79f19c6eb6b74d01be1d56da6a9a38c

image-20250106101203739

Imports

image-20250106101318385

运行

image-20250106101224977

image-20250106101244809

ida

确定磁盘类型进行跳转

image-20250106101557865

od

image-20250106102906501

inc 将指定的操作数加 1,dec 将指定操作数减 1 。

这个程序的crack点在与跳转到正确提示,所以把je改成jmp就可以。

栈帧

栈帧就是利用EBP(栈帧指针,请注意不是ESP)寄存器访问栈内局部变量、参数、函数返回地址等的手段。

image-20250105220704314

abex’crackme#2

运行

image-20250106114457784

image-20250106114518023

基本信息

image-20250106114543107

IDA反汇编失败

OD

目的:跳转到成功

搜字符串定位到比较和跳转的地方 修改跳转

image-20250106132908080

这里是操作字符串的地方,修改跳转,让它直接到成功那里

image-20250106133644920

image-20250106133451361

目的:找正确字符串

找到这个位置 这个位置是出现输入字符出现的地方

image-20250106135922639

这里做加法edi+0x64

image-20250106140903778

加完的结果是A5

所以这个加密算法是每个NAME+0x64

那字符”ABCD” 转换成ASCII码后+0x64 最后就是A5A6A7A8

image-20250106142317731

函数调用约定

提问1.函数执行完成后,栈中的参数如何处理?
回答1.不用管。
由于只是临时使用存储在栈中的值,即使不再使用,清除工作也会浪费CPU资源。再向栈存入其他值时,原有值会被自然覆盖掉,并且栈内存是固定的,所以既不能也没必要释放内存。
提问2.函数执行完毕后,ESP值如何变化?
回答2.ESP值要恢复到函数调用之前,这样可引用的栈大小才不会缩减。

函数调用约定:

cdecl:调用者负责处理栈
stdcall:被调用者负责清理栈
fastcall

cdecl例子:

找主函数:

image-20250106160044142

调用者负责处理栈:

image-20250106161759442

stdcall例子:

被调用者负责清理栈:

image-20250106161713638

fastcall
fastcall方式与stdcall方式基本类似,但该方式通常会使用寄存器(而非栈内存)去传递那些需要传递给函数的部分参数(前2个)。若某函数有4个参数,则前2个参数分别使用ECX、EDX寄存器传递。

crackme

11.1 运行
首先运行要破解的文件。弹出消息对话框,显示2条信息。

删除所有烦人的 Nags(唠叨)

查找 registration code

image-20250106162834912

11.2.1 目标(1):去除消息框

需要保证第二个窗口依旧能弹出

调试到这个位置

image-20250106170313368

如果直接nop会出错

使用修改跳转的方法patch

image-20250106170709248

实际上修改的是msvbvm50.dll

11.2.3 目标(2):查找注册码

image-20250106172114956

这个比较明显

image-20250106173242647

PE文件格式

image-20250106181132447

13.2.2 VA&RVA
VA指的是进程虚拟内存的绝对地址,RVA(RelativeVirtualAddress,相对虚拟地址)指从某个基准位置(ImageBase)开始的相对地址。VA与RVA满足下面的换算关系。
RVA+ImageBase=VA

PE头内部信息大多以RVA形式存在。原因在于,PE文件(主要是DLL)加载到进程虚拟内存的特定位置时,该位置可能已经加载了其他PE文件(DLL)。此时必须通过重定位将其加载到其他空白的位置,若PE头信息使用的是VA,则无法正常访问。因此使用RVA来定位信息,即使发生了重定位,只要相对于基准位置的相对地址没有变化,就能正常访问到指定信息。

13.3 PE 头

image-20250106215730841

image-20250106220331487

13.3.5 NT头:可选头

设置错误将导致文件无法正常运行,

[^]: 它被称为 “可选头” 主要是历史和格式定义上的原因,而不是因为其内容真的可有可无。

#1.Magic
为IMAGE_OPTIONAL_HEADER32结构体时,Magic码为10B;

为IMAGEOPTIONALHEADER64结构体时,Magic码为20B。
#2.AddressOfEntryPoint
AddressOfEntryPoint持有EP的RVA值。该值指出程序最先执行的代码起始地址,相当重要#3.lmageBase
进程虚拟内存的范围是0~FFFFFFFF(32位系统)。PE文件被加载到如此大的内存中时ImageBase指出文件的优先装人地址。
EXE、DLL文件被装载到用户内存的0~7FFFFFFF中,SYS文件被载人内核内存的80000000~FFFFFFFF中。

一般而言,EXE文件其ImageBase的值为00400000,DLL文件的ImageBase值为10000000。执行PE文件时,PE装载器先创建进程,再将文件载人内存,然后把EIP寄存器的值设置为ImageBase+AddressOfEntryPoint.

#4.SectionAlignment, FileAlignment

PE文件的Body部分划分为若干节区,这些节存储着不同类别的数据。FileAlignment指定了节区在磁盘文件中的最小单位,而SectionAlignment则指定了节区在内存中的最小单位

#5.SizeOflmage
指定PE Image在虚拟内存中所占空间的大小。
#6.SizeOfHeader
用来指出整个PE头的大小。
#7.Subsystem
该Subsystem值用来区分系统驱动文件(.sys)与普通的可执行文件(,exe,*.dll )。

image-20250106222436032

#8.NumberOfRvaAndSizes

用来指定DataDirectory(IMAGE_OPTIONAL_HEADER32结构体的最后一个成员)数组的个数。
#9.DataDirectory

DataDirectory是由IMAGE DATA DIRECTORY结构体组成的数组,数组的每项都有被定义的值。

image-20250106222618403

内存与映像

image-20250106223146460

映射

RVA to RAW

(1)查找RVA所在节区

(2)使用简单的公式计算文件偏移(RAW)。

image-20250106223312583

DLL

DLL的加载方式:一种是“显式链接”(ExplicitLinking),程序使用DLL时加载使用完毕后释放内存;另一种是“隐式链接”(Implicit Linking),程序开始时即一同加载DLL。

重定位

普通DLL文件的IageBase为10000000,所以经常会发生DLL重定位。但是Windows系统DLL文件(kernel32/user32/gdi32等)拥有自身固有的ImageBase,不会出现DIL重定位。

Q.前面的讲解中提到,执行文件加载到内存时会根据lmagebase确定地址,那么2个notepad程序同时运行时lmagebase都是10000000,它们会侵占彼此的空间区域,不是这样吗?

A.生成进程(加载到内存)时,OS会单独为它分配4GB大小的虚拟内存。虚拟内存与实际物理内存是不同的。同时运行2个notepad时,各进程分别在自身独有的虚拟内存空间中所以它们彼此不会重叠。这是由OS来保障的。因此,即使它们的Imagebase一样也完全没问题。

OD跟踪命令

image-20250107101030393

从可执行文件中删除.reloc节区

先用peview定位节区内容 用Detect It Easy也可以

文件再用WinHex打开找到节区

image-20250107102228651

直接把数据删掉

然后修改文件头以保证文件能正常运行

第一个修改的数据 节区数

image-20250107102627270

位置在

image-20250107102818166

第二个值是映像大小

image-20250107103211917

通过将.reloc节区的原始大小(E40十六进制,即3648十进制)与 SectionAlignment(1000十六进制,即4096十进制)进行比较,由于原始大小不是对齐值的整数倍,按照向上取整的规则,将其扩展到1000(十六进制)

image-20250107103332189

所以需要11000-1000

内嵌补丁练习

内嵌补丁即内嵌代码补丁(Inline Code Patch),当难以直接修改指定代码时,插入并运行被称为“洞穴代码”(Code Cave)的补丁代码后,对程序实现打补丁。该技术经常用于对象程序经过运行时压缩(或加密处理)而难以直接修改的情况。

image-20250107142606226

目的:修改弹窗字符

image-20250107135256290

image-20250107135304878

image-20250107153235979

ecx = 0x154 是循环次数

ebx=004010f5,用ebx异或44,解密(4010f5~401248)区域

004010B0 |. E8 08000000 call unpackme.004010BD

image-20250107153923074

第一个循环

ecx = 0x7F是循环次数

ebx = 004010f5,用ebx异或7,解密(401007~401085)区域

第二个循环

ecx = 0x154是循环次数

ebx = 004010f5,用ebx异或11,解密(4010f5~401248)区域

做了三次异或解密 其中同一个区域做了两次

004010B6 |. E8 7EFFFFFF call unpackme.00401039

image-20250107155440070

ecx = 0x154是循环次数

循环0x154次将ebx的值累加

ebx=004010F5,是那个做了两次异或的区域,将(4010f5~401248)区域累计相加,结果存edx

0040105D . E8 28000000 call unpackme.0040108A

image-20250107160803378

ecx = 0x17是循环次数

ebx = 0040124A,用ebx异或17,解密(40124A~401260)区域

出来之后与一串字符做对比

image-20250107161048472

对比结果决定是否跳转到 验证错误的提示

image-20250107161222220

对比成功即 文件未被修改过则跳转至文件OEP

image-20250107161318330

未能识别的 右键分析 可以看到是函数跳转表

image-20250107161609550

DialogBoxParam函数,此函数的结构是

“`c++
INT_PTR DialogBoxParamA(
[in, optional] HINSTANCE hInstance,
[in] LPCSTR lpTemplateName,
[in, optional] HWND hWndParent,
[in, optional] DLGPROC lpDialogFunc,
[in] LPARAM dwInitParam
);

直接转到数据区域

![image-20250107162443493](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244063.png)

 这个程序 是在进入OPE前做的CRC验证 所以如果在后面修改代码会被发现

但是如果在他做完CRC验证 也就是不修改这段做验证的代码

在空白区构造一段新的代码 让程序做完验证 直接跳转 就可以起到修改效果的作用 同时 也完成了CRC验证

这种做法也就是本章所提的,插入并运行被称为“洞穴代码”(Code Cave)的补丁代码。

这里是验证通过跳转到OPE

![image-20250107165541367](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244064.png)

从401280开始往后就是空白区 

"Expecto Patronum\0"17

"Alohomora\0"10

写好了如下:

![image-20250107173144711](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244065.png)

mov ecx, 0x11 ; 0x11为要拷贝的字符串的字符长度(包括’\0’)> “Expecto Patronum\0″17->0x11
mov esi, 00401280 ; 让ESI指向第一个需要拷贝的字符串所在地址
mov edi, 00401123 ; 修改的目标字符串所在地址
rep movsb ; <
> rep movs byte ptr es:[edi],byte ptr ds:[esi]
;这个指令意思就是将ESI指向的地址的值以1字节方式拷贝到EDI指向的地址中
;重复执行ECX次,每次执行后ESI+1,EDI+1,ECX-1

mov ecx, 0xA ; 0xA为要拷贝的字符串的字符串长度(包括’\0’)> “Alohomora\0″10->0xA
mov esi, 00401290 ; 让ESI指向第二个需要拷贝的字符串所在地址
mov edi, 0040110A ; 修改的目标字符串所在地址
rep movsb ; <
> rep movs byte ptr es:[edi],byte ptr ds:[esi] ; 作用同上

jmp 0040121E ; 跳转至OEP

然后需要到修改CRC到OPE的函数

让其跳转到洞穴代码这里 00401280

![image-20250107173328672](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244066.png)

。。。。。。。。。。。。。运行失败

![image-20250107180021783](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244067.png)

问题在于 00401083属于加密区域

![image-20250107173843370](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244068.png)

所以这里的跳转指令也需要 修改成加密后的形态

使整条指令与7异或

E9 xor 07 = EE
12 xor 07 = 15
02 xor 07 = 05

![image-20250107180301710](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244069.png)

保存

![image-20250107180320138](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244070.png)

![image-20250107180350418](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244071.png)

有点问题 替换没有完全成功 原因是编辑

"Expecto Patronum\0" 的时候 没有加00   \0表示这个字符串在此处结束

![image-20250107182335673](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244072.png)

修改一下

![image-20250108100117134](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244073.png)

![image-20250108100126903](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244074.png)

![image-20250108100134110](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244075.png)

## Windows消息钩取

发生事件后,消息进入OS message queue

![image-20250108104138888](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244076.png)

SetWindowsHookEx()

![image-20250108104455541](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244077.png)

钩子过程(hook procedure)是由操作系统调用的回调函数。安装消息钩子时,钩子过程需要存在于某个DLL内部,且该DLL的示例句柄(instancehandle)即是hMod。

若 dwThreadlD 参数被设置为0,则安装的钩子为全局钩子(GlobalHook),它会影响到运行中的(以及以后要运行的)所有进程。

## DLL注入

向某个进程注入DLL时主要使用以下三种方法:
DLL注入方法
创建远程线程(CreateRemoteThread()API)

使用注册表(AppInit_DLLs值)

消息钩取(SetWindowsHookEx()API)





创建远程线程(CreateRemoteThread()API)

调试DLL注入过程

1.运行notepad.exe获取PID

![image-20250108133614018](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244078.png)

2.OD打开 附加已打开的notepad.exe程序-->运行

3.调试设置

![image-20250108134528852](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244079.png)

4.cmd运行

InjectDll.exe notepad的PID myhack.dll

(这个技巧感觉调试指定dll的时候会好用)

![image-20250108133450199](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244080.png)

这样就能直接跳转到需要调试的dll处 在调试前需要取消中断于新模块DLL

![image-20250108135009895](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244081.png)



使用注册表(AppInit_DLLs值)

进行DLL注入的第二种方法是使用注册表。Windows操作系统的注册表中默认提供了AppInt_DLLs与LoadApplnit_DLLs两个注册表项。在注册表编辑器中,将要注入的DLL的路径字符串写入AppInt_DLLs项目,然后把LoadApplnit_DLLs的项目值设置为1。重启后,指定DLL会注入所有运行进程。该方法操作非常简单,但功能相当强大。

上述方法的工作原理是,User32.dll被加载到进程时,会读取AppInt_DLLs 注册表项,若有值,则调用 LoadLibrary()API加载用户DLL。所以,严格地说,相应DLL并不会被加载到所有进程,而只是加载至加载 user32.dl 的进程。

AppInt_DLLs 和 LoadApplnit_DLLs 的历史背景
在Windows操作系统的早期版本中,AppInt_DLLs和LoadApplnit_DLLs是用于应用程序初始化相关的机制。它们涉及到在应用程序启动过程中加载特定的动态链接库(DLL)来执行初始化代码。
当前状态
在现代 Windows 操作系统(如 Windows 10 及以上版本)的常规应用开发和运行环境中,这些机制已经很少被直接使用。

23.6 SetWindowsHookEx()
注入DLL的第三个方法就是消息钩取,即用SetWindowsHookEx()API安装好消息“钩子”,然后由OS将指定DLL(含有“钩子”过程)强制注入相应(带窗口的)进程。

## DLL卸载

DLL卸载(DLLEjection)是将强制插入进程的DLL弹出的一种技术,其基本工作原理与使用CreateRemoteThread()API进行DLL注人的原理类似。
DLL卸载的工作原理
前面我们学习过使用CreateRemoteThread() API进行DLL注入的工作原理,概括如下:
驱使目标进程调用LoadLibrary()API

同样,DLL卸载工作原理也非常简单:
驱使目标进程调用FreeLibrary()API

也就是说,将FreeLibrary()API的地址传递给CreateRemoteThread()的lpStartAddress参数,并把要卸载的DLL的句柄传递给lpParameter参数。

每个 Windows内核对象(Kernel Object)都拥有一个引用计数(Reference Count)代表对象被使用的次数。调用10次LoadLibrary(“a.dll”),a.dll的引用计数就变为 10,卸载 a.dll 时同样需要调用10次Freelibrary()(每调用一次LoadLibrary(),引用计数会加 1;而每调用一次 Freelibrary(,引用计数会减1)。因此,卸载 DLL时要充分考虑好“引用计数”这个因素。

24.2.1获取进程中加载的 DLL 信息
hSnapshot=CreateToolhelp32Snapshot(TH32CSSNAPMODULE,dWPID);

24.2.2 获取目标进程的句柄
hProcess=openProcess(PROCESS_ALL_ACCESS,FALSE,dwPID);

24.2.3 获取 FreeLibrary()API地址
hModule=GetModuleHandle(L"kernel32.dll”);

pThreadProC=(LPTHREAD_START_ROUTINE)GetProcAddress(hModule, "FreeLibrary");

24.2.4 在目标进程中运行线程
hThread=CreateRemoteThread(hProcess,NULL,0,pThreadProcme.modBaseAddr,0.NULL)

## 通过修改PE加载DLL

目标:修改TextView.exe的导入表,让它能在运行时自动加载myhack3.dll

1.确认导入表空间

导入表的偏移和大小

![image-20250108153926788](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244082.png)

RVA:84CC ~ 852F(整体大小为:0`*`14`*`5 = 0`*`64字节)  84CC+63=852F

![image-20250108154030165](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244083.png)

在 PEView 工具栏将视图改为 File Offset,可以看到 IDT 的文件偏移为 76CC

![image-20250108162010750](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244084.png)

通过文件偏移定位在WinHex中的位置

76CC-772F(76CC+63)

![image-20250108162115607](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244085.png)

要添加myhack3.dll 需要添加它的IID,一个IID的大小是固定的即20字节,14h。

导入表的尾部非空白区 存在其他数据 所以 需要把整个导入表移动到空白区更多的位置 再添加IID

用复制功能把数据复制到7E80的位置   不能用剪切 会破坏其他数据结构。。。

![image-20250108170428985](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244086.png)

修改导入表的偏移和大小

![image-20250108171304124](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244087.png)

RVA》=》RAW

84CC - 6000 + 5200 = 76CC.

7E80-5200+6000=8C80

![image-20250108172315594](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244088.png)

6000取自

![image-20250108174524448](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244089.png)



5200取自

![image-20250108174546758](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244090.png)



现在添加新的IID结构

根据

![image-20250108173106885](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244091.png)

![image-20250108173959047](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244092.png)

dummy是导出函数的名称

前半是实际 INT 、Name、IAT的指针的地址,后半是对应的数值。

接着需要修改IAT的节区属性,需要改成 WRITE(可写)属性

向原属性(ChAracteristics)40000040 添加 IMAGE_SCN_MEM_WRITE(80000000)属性值,执行 bit OR 运算,最终属性值变为 C0000040 

[^PE 文件节属性(Characteristics)的位表示方式]: 在 PE 文件格式中,节(Section)的属性是通过位来表示的。每个位代表一种特定的属性,这些属性可以是可读(`IMAGE_SCN_MEM_READ`)、可写(`IMAGE_SCN_MEM_WRITE`)、可执行(`IMAGE_SCN_MEM_EXECUTE`)等。例如,给定的原始属性值`40000040`和要添加的`IMAGE_SCN_MEM_WRITE`属性值`80000000`都是以十六进制表示的位掩码。在这种位掩码表示方式中,每一位(或者一组位)都有其特定的含义,用于控制节的行为和访问权限等特性。
[^为什么使用位或(bit OR)运算来添加属性]: 原始属性值`40000040`已经包含了一些已设置的属性。当要添加`IMAGE_SCN_MEM_WRITE`属性时,不能简单地覆盖原有的属性,因为可能会丢失原始属性中其他重要的信息(如可能已经设置的可读等属性)。通过位或运算,只有新属性对应的位(在`IMAGE_SCN_MEM_WRITE`属性值`80000000`中为 1 的位)在结果中会被设置为 1,如果原始属性中对应的位已经是 1,那么结果中该位仍然是 1;如果原始属性中对应的位是 0,那么通过位或运算,这个位会被新属性设置为 1。这样就实现了在不破坏原始属性中其他已设置位的情况下,添加新的属性。

- 例如,将
  40000040 和 80000000 进行位或运算:
  - 以二进制形式来看,`40000040`(十六进制)= `01000000000000000000000001000000`(二进制),`80000000`(十六进制)= `10000000000000000000000000000000`(二进制)。
  - 根据位或运算规则,逐位计算得到结果为`11000000000000000000000001000000`(二进制),转换回十六进制就是`C0000040`,成功添加了`IMAGE_SCN_MEM_WRITE`属性,同时保留了原始属性中的其他信息。

[[分享\]通过修改PE文件加载DLL-软件逆向-看雪-安全社区|安全招聘|kanxue.com](https://bbs.kanxue.com/thread-268897.htm)

![image-20250108180430969](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244093.png)

以上就全部修改好了

![image-20250108182403653](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244094.png)

## 代码注入

代码注入是一种向目标进程插入独立运行代码并使之运行的技术,它一般调用CreateRemoteThread()API以远程线程形式运行插入的代码,所以也被称为线程注入。

采用DLL注入技术时,整个DLL会被插入目标进程,代码与数据共存于内存,所以代码能够正常运行。与此不同,代码注入仅向目标进程注入必要的代码,要想使注人的代码正常运行,还必须将代码中使用的数据一同注入(并且要通过编程将已注人数据的地址明确告知代码 )。

代码注入示例

目标:向notepad.exe进程注入简单的代码,注入后会弹出消息框。

OD载入notepad.exe F9使程序处于运行状态

设置调试选项

![image-20250109154606738](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244095.png)


从火绒剑或者其他程序 获知notepad的PID 

做注入

![image-20250109154644103](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244096.png)

程序暂停在被注入的位置

![image-20250109155042209](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244097.png)

![image-20250109155439772](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244098.png)

需要注入的数据 以参数的形式传入

注入过程大致如下

1、设置THREAD_PARAM结构体

![image-20250109160101259](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244099.png)

2、调用API

![image-20250109160206589](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244100.png)

## 使用汇编语言写注入代码

使用汇编语言编写ThreadProc()

跳转到这个位置 这个位置是代码顶端 将在这个位置 汇编编写ThreadProc()

![image-20250109161727705](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244101.png)

开始汇编 

这一段其实是借od来做汇编编程 选哪段位置都不重要 写好后的汇编代码会拷贝出来 再使用

![image-20250109164621007](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244102.png)

地址00401033处编入的是数据 右键分析

![image-20250109164744776](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244103.png)

整段编写完成

![image-20250109170054574](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244104.png)

保存后 用OD打开patch后的文件 跳转到这个位置401000 数据跟随

右键复制到文件

![image-20250109171246973](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244105.png)

![image-20250109171259495](https://image-hosting-210.oss-cn-beijing.aliyuncs.com/undefined202502252244106.png)

处理数据

0x55,0x8B,0xEC,0x8B,0x75,0x08,0x68,0x6C,0x6C,0x00,0x00,0x68,0x33,0x32,0x2E,0x64,0x68,0x75,0x73,0x65,0x72,0x54,0xFF,0x16,0x68,0x6F,0x78,0x41,0x00,0x68,0x61,0x67,0x65,0x42,0x68,0x4D,0x65,0x73,0x73,0x54,0x50,0xFF,0x56,0x04,0x6A,0x00,0xE8,0x0C,0x00,0x00,0x00,0x52,0x65,0x76,0x65,0x72,0x73,0x65,0x43,0x6F,0x72,0x65,0x00,0xE8,0x14,0x00,0x00,0x00,0x77,0x77,0x77,0x2E,0x72,0x65,0x76,0x65,0x72,0x73,0x65,0x63,0x6F,0x72,0x65,0x2E,0x63,0x6F,0x6D,0x00,0x6A,0x00,0xFF,0xD0,0x33,0xC0,0x8B,0xE5,0x5D

编写注入程序Codelnjection2.cpp

```c
// CodeInjection2.cpp
// reversecore@gmail.com
// http://www.reversecore.com

#include "windows.h"
#include "stdio.h"

typedef struct _THREAD_PARAM 
{
    FARPROC pFunc[2];               // LoadLibraryA(), GetProcAddress()
} THREAD_PARAM, *PTHREAD_PARAM;

BYTE g_InjectionCode[] = 
{
    0x55, 0x8B, 0xEC, 0x8B, 0x75, 0x08, 0x68, 0x6C, 0x6C, 0x00,
    0x00, 0x68, 0x33, 0x32, 0x2E, 0x64, 0x68, 0x75, 0x73, 0x65,
    0x72, 0x54, 0xFF, 0x16, 0x68, 0x6F, 0x78, 0x41, 0x00, 0x68,
    0x61, 0x67, 0x65, 0x42, 0x68, 0x4D, 0x65, 0x73, 0x73, 0x54,
    0x50, 0xFF, 0x56, 0x04, 0x6A, 0x00, 0xE8, 0x0C, 0x00, 0x00,
    0x00, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x43, 0x6F,
    0x72, 0x65, 0x00, 0xE8, 0x14, 0x00, 0x00, 0x00, 0x77, 0x77,
    0x77, 0x2E, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x63,
    0x6F, 0x72, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x00, 0x6A, 0x00,
    0xFF, 0xD0, 0x33, 0xC0, 0x8B, 0xE5, 0x5D, 0xC3
};

/*
004010ED    55               PUSH EBP
004010EE    8BEC             MOV EBP,ESP
004010F0    8B75 08          MOV ESI,DWORD PTR SS:[EBP+8]       ; ESI = pParam           
004010F3    68 6C6C0000      PUSH 6C6C                      
004010F8    68 33322E64      PUSH 642E3233
004010FD    68 75736572      PUSH 72657375
00401102    54               PUSH ESP                           ; - "user32.dll"
00401103    FF16             CALL DWORD PTR DS:[ESI]            ; LoadLibraryA("user32.dll")
00401105    68 6F784100      PUSH 41786F
0040110A    68 61676542      PUSH 42656761
0040110F    68 4D657373      PUSH 7373654D
00401114    54               PUSH ESP                           ; - "MessageBoxA"
00401115    50               PUSH EAX                           ; - hMod
00401116    FF56 04          CALL DWORD PTR DS:[ESI+4]          ; GetProcAddress(hMod, "MessageBoxA")
00401119    6A 00            PUSH 0                             ; - MB_OK (0)
0040111B    E8 0C000000      CALL 0040112C
00401120                     <ASCII>                            ; - "ReverseCore", 0
0040112C    E8 14000000      CALL 00401145
00401131                     <ASCII>                            ; - "www.reversecore.com", 0
00401145    6A 00            PUSH 0                             ; - hWnd (0)
00401147    FFD0             CALL EAX                           ; MessageBoxA(0, "www.reversecore.com", "ReverseCore", 0)
00401149    33C0             XOR EAX,EAX                        
0040114B    8BE5             MOV ESP,EBP
0040114D    5D               POP EBP                            
0040114E    C3               RETN
*/


BOOL InjectCode(DWORD dwPID)
{
    HMODULE         hMod            = NULL;
    THREAD_PARAM    param           = {0,};
    HANDLE          hProcess        = NULL;
    HANDLE          hThread         = NULL;
    LPVOID          pRemoteBuf[2]   = {0,};

    hMod = GetModuleHandleA("kernel32.dll");

    // set THREAD_PARAM
    param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
    param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");

    // Open Process
    if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS,               // dwDesiredAccess
                                  FALSE,                            // bInheritHandle
                                  dwPID)) )                         // dwProcessId
    {
        printf("OpenProcess() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    // Allocation for THREAD_PARAM
    if( !(pRemoteBuf[0] = VirtualAllocEx(hProcess,                  // hProcess
                                         NULL,                      // lpAddress
                                         sizeof(THREAD_PARAM),      // dwSize
                                         MEM_COMMIT,                // flAllocationType
                                         PAGE_READWRITE)) )         // flProtect
    {
        printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    if( !WriteProcessMemory(hProcess,                               // hProcess
                            pRemoteBuf[0],                          // lpBaseAddress
                            (LPVOID)&param,                         // lpBuffer
                            sizeof(THREAD_PARAM),                   // nSize
                            NULL) )                                 // [out] lpNumberOfBytesWritten
    {
        printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    // Allocation for ThreadProc()
    if( !(pRemoteBuf[1] = VirtualAllocEx(hProcess,                  // hProcess
                                         NULL,                      // lpAddress
                                         sizeof(g_InjectionCode),   // dwSize
                                         MEM_COMMIT,                // flAllocationType
                                         PAGE_EXECUTE_READWRITE)) ) // flProtect
    {
        printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    if( !WriteProcessMemory(hProcess,                               // hProcess
                            pRemoteBuf[1],                          // lpBaseAddress
                            (LPVOID)&g_InjectionCode,               // lpBuffer
                            sizeof(g_InjectionCode),                // nSize
                            NULL) )                                 // [out] lpNumberOfBytesWritten
    {
        printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    if( !(hThread = CreateRemoteThread(hProcess,                    // hProcess
                                       NULL,                        // lpThreadAttributes
                                       0,                           // dwStackSize
                                       (LPTHREAD_START_ROUTINE)pRemoteBuf[1],
                                       pRemoteBuf[0],               // lpParameter
                                       0,                           // dwCreationFlags
                                       NULL)) )                     // lpThreadId
    {
        printf("CreateRemoteThread() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    WaitForSingleObject(hThread, INFINITE); 

    CloseHandle(hThread);
    CloseHandle(hProcess);

    return TRUE;
}

注入代码本身同时包含着代码所需的字符串数据。所以_THREAD_PARAM结构体中并不包含字符串成员。

编写汇编代码,将生成的指令内联在注入程序的源代码中,进而将其注入目标进程。

调试

用OD打开notepad.exe 设置调试选项 设置中断于新线程 使用命令行做注入

image-20250109172848386

image-20250109173742081

001B0000    55              push ebp
001B0001    8BEC            mov ebp,esp

//THREAD_PARAM结构体指针
//typedef struct _THREAD_PARAM 
//{
//    FARPROC pFunc[2];               // LoadLibraryA(), GetProcAddress()
//} THREAD_PARAM, *PTHREAD_PARAM;

001B0003    8B75 08         mov esi,dword ptr ss:[ebp+0x8]

ESI –> 数据跟随 –> 长型 –> 地址

image-20250109174724265

//"user32.dll"
001B0006    68 6C6C0000     push 0x6C6C
001B000B    68 33322E64     push 0x642E3233
001B0010    68 75736572     push 0x72657375

//LoadLibraryA
001B0015    54              push esp
001B0016    FF16            call dword ptr ds:[esi]     ; kernel32.LoadLibraryA

//"MessageBoxA\0"
001B0018    68 6F784100     push 0x41786F       
001B001D    68 61676542     push 0x42656761     
001B0022    68 4D657373     push 0x7373654D 

//GetProcAddress
001B0027    54              push esp
001B0028    50              push eax                        ; user32.77D10000
001B0029    FF56 04         call dword ptr ds:[esi+0x4]     ; kernel32.GetProcAddress

001B002C    6A 00           push 0x0

//int MessageBox(
//  HWND hWnd,              // handle of owner window
//  LPCTSTR lpText,         // address of text in message box 
//  LPCTSTR lpCaption,      // address of title of message box
//  UINT uType              // style of message box
//);

001B002E    E8 0C000000     call 001C003F       ;MessageBoxA()

001C002C    6A 00           push 0x0

//“ReverseCore” 001B0033--001B003E
001B0033    52              push edx
001B0034    65:76 65        jbe short 001c009c
001B0037    72 73           jb short 001C00AC
001B0039    65:43           inc ebx
001B003B    6f              outs dx,dword ptr ds:[esi]
001B003C    72 65           jb short 001C00A3
001B003E    00E8            add al,ch

//“www.reversecore.com” 001B003F--001B0057
001B003F    E8 14000000     call 001C0058
001B0044    77 77           ja short 001C00BD
001B0046    77 2E           ja short 001C0076
001B0048    72 65           jb short 001C00AF
001B004A    76 65           jbe short 001C00B1
001B004C    72 73           jb short 001C00C1
001B004E    65:636F 72      arpl word ptr gs:[edi+0x72],bp
001B0052    65              gs:
001B0053    2E:636F 6D      arpl word ptr cs:[edi+0x6D],bp
001B0057    006A 00         add byte ptr ds:[edx],ch

001B0058    6A 00           push 0x0
001B005A    FFD0            call eax       ; user32.MessageBoxA
001B005C    33C0            xor eax,eax

001B005E    8BE5            mov esp,ebp
001B0060    5D              pop ebp
001B0061    C3              retn

API钩取

29.1 钩取
代码逆向分析中,钩取(Hooking)是一种截取信息、更改程序执行流向、添加新功能的技术。钩取的整个流程如下:
使用反汇编器/调试器把握程序的结构与工作原理;
开发需要的“钩子”代码,用于修改Bug、改善程序功能;
灵活操作可执行文件与进程内存,设置“钩子”代码。

API就是应用程序使用资源的接口

29.3 API钩取
通过API钩取技术可以实现对某些Win32API调用过程的拦截,并获得相应的控制权限。使用API钩取技术的优势如下:
在 API调用前/后运行用户的“钩子”代码。
查看或操作传递给AP的参数或AP数的返回值
取消对 API的调用,或更改执行流,运行用户代码。

image-20250109182532012

image-20250109182625231

静态方法针对的是“文件”,而动态方法针对的是进程内存。一般API钩取技术指动态方法,当然在某些非常特殊的情形下也可以使用静态方法。

调试

因为调试的权限高,调试器拥有被调试者(被调试进程)的所有权限(执行控制、内存访问等),所以可以向被调试进程的内存任意设置钩取函数。可以利用这一点做钩取。

注入

DLL注入
使用DLL注入技术可以驱使目标进程强制加载用户指定的DLL文件。使用该技术时,先在要注入的DLL中创建钩取代码与设置代码,然后在DllMain中调用设置代码,注人的同时即可完成API钩取。
代码注入

代码注入技术不像DLL注入技术那样针对的是完整的PE映像,而是在执行代码与数据被注入的状态下直接获取自身所需API地址来使用的。

记事本WriteFile()API钩取

对想钩取的进程进行附加操作,使之成为被调试者

“钩子”:将API起始地址的第一个字节修改为0xCC

调用相应 API时,控制权转移到调试器
执行需要的操作(操作参数、返回值等)

脱钩:将0xCC恢复原值(为了正常运行API)

运行相应 API(无0xCC的正常状态);
“钩子”:再次修改为 0xCC(为了继续钩取);

控制权返还被调试者。

BOOL WriteFile(
HANDLE  hFile,//文件句柄
LPCVOID lpBuffer,//数据缓存区指针
DWORD   nNumberOfBytesToWrite,//你要写的字节数
LPDWORD lpNumberOfBytesWritten,//用于保存实际写入字节数的存储区域的指针
LPOVERLAPPED lpOverlapped//OVERLAPPED结构体指针
);

看下hookdbg.cpp

#include "windows.h"
#include "stdio.h"

LPVOID g_pfWriteFile = NULL;
CREATE_PROCESS_DEBUG_INFO g_cpdi;
BYTE g_chINT3 = 0xCC, g_chOrgByte = 0;

int main(int argc, char* argv[])
{
    DWORD dwPID;

    if( argc != 2 )
    {
        printf("\nUSAGE : hookdbg.exe <pid>\n");
        return 1;
    }

    // Attach Process
    dwPID = atoi(argv[1]);
    if( !DebugActiveProcess(dwPID) )
    {
        printf("DebugActiveProcess(%d) failed!!!\n"
               "Error Code = %d\n", dwPID, GetLastError());
        return 1;
    }

    // 调试器循环
    DebugLoop();

    return 0;
}

void DebugLoop()
{
    DEBUG_EVENT de;
    DWORD dwContinueStatus;

    // 等待被调试者发生事件
    while( WaitForDebugEvent(&de, INFINITE) )
    {
        dwContinueStatus = DBG_CONTINUE;

        // 被调试进程生成或附加事件
        if( CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode )
        {
            OnCreateProcessDebugEvent(&de);
        }
        // 异常事件
        else if( EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode )
        {
            if( OnExceptionDebugEvent(&de) )
                continue;
        }
        // 被调试进程终止事件
        else if( EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode )
        {
            // 被调试者终止->调试器终止
            break;
        }

        // 再次运行被调试者
        ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
    }
}

DebugLoop()函数处理3种调试事件

  1. EXIT_PROCESS_DEBUG_EVENT 被调试进程终止时会触发该事件。
  2. CREATE_PROCESS_DEBUG_EVENT 被调试进程生成或附加事件。
  3. EXCEPTION_DEBUG_EVENT—OnExceptionDebugEvent()—处理被调试者的INT3指令

OnCreateProcessDebugEvent() 被调试进程启动(或者附加)时即调用执行该函数。

BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde)
{
    // WriteFile() API 地址
    g_pfWriteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");

    // API “钩子” - WriteFile()
    // 更改第一个字节为0xCC (INT3)
    // originalbyte是g_chOrgByte备份
    memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));
    ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile, 
                      &g_chOrgByte, sizeof(BYTE), NULL);
    WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, 
                       &g_chINT3, sizeof(BYTE), NULL);

    return TRUE;
}

OnExceptionDebugEvent()

BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde)
{
    CONTEXT ctx;
    PBYTE lpBuffer = NULL;
    DWORD dwNumOfBytesToWrite, dwAddrOfBuffer, i;
    PEXCEPTION_RECORD per = &pde->u.Exception.ExceptionRecord;

    // 是断点异常时
    if( EXCEPTION_BREAKPOINT == per->ExceptionCode )
    {
        // 断点地址为WriteFile()API地址时
        if( g_pfWriteFile == per->ExceptionAddress )
        {
            // #1. Unhook
            // 将0xCC恢复为original byte
            WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, 
                               &g_chOrgByte, sizeof(BYTE), NULL);

            // #2. 获取线程上下文
            ctx.ContextFlags = CONTEXT_CONTROL;
            GetThreadContext(g_cpdi.hThread, &ctx);

            // #3. 获取WriteFile()的param2、3值 
            //   函数参数存在于进程的栈
            //   param 2 : ESP + 0x8
            //   param 3 : ESP + 0xC
            ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x8), 
                              &dwAddrOfBuffer, sizeof(DWORD), NULL);
            ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0xC), 
                              &dwNumOfBytesToWrite, sizeof(DWORD), NULL);

            // #4.分配临时缓冲区
            lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite+1);
            memset(lpBuffer, 0, dwNumOfBytesToWrite+1);

            // #5.复制WriteFile()缓冲区到临时缓冲区
            ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer, 
                              lpBuffer, dwNumOfBytesToWrite, NULL);
            printf("\n### original string ###\n%s\n", lpBuffer);

            // #6. 将小写字母转换为大写字母
            for( i = 0; i < dwNumOfBytesToWrite; i++ )
            {
                if( 0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A )
                    lpBuffer[i] -= 0x20;
            }

            printf("\n### converted string ###\n%s\n", lpBuffer);

            // #7. 将变换后的缓冲区复制到WriteFile()缓冲区
            WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer, 
                               lpBuffer, dwNumOfBytesToWrite, NULL);

            // #8. 释放临时缓冲区
            free(lpBuffer);

            // #9. 将线程上下文的EIP更改为WriteFile()首地址
            //   (当前为WriteFile+1位置,INT3命令之后)
            ctx.Eip = (DWORD)g_pfWriteFile;
            SetThreadContext(g_cpdi.hThread, &ctx);

            // #10. 运行被调试进程
            ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
            Sleep(0);//释放当前线程的剩余时间片,即调用Sleep(0)函数后,CPU会立即执行其他线程。

            // #11. API Hook
            WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, 
                               &g_chINT3, sizeof(BYTE), NULL);

            return TRUE;
        }
    }
    return FALSE;
}

计算器显示中文数字

DLL注入钩取

只需先将要钩取的API在用户的DLL中重定义,然后再注入目标进程即可。

但是,如果想钩取的API不在目标进程的IAT中,那么就无法使用该技术进行钩取操作。

1,选定目标API

image-20250110172232084

SetWindowTextW()和SetDlgltemTextW()

这两个函数是设置窗口显示字符

SetWindowTextW()内部使用会调用SetDlgltemTextW(),所以总体上只需要钩取SetWindowTextW()就可以

image-20250110174108644

Text=”0.” 这个值是计算器的初始值

image-20250110174157522

输入数字 7

image-20250110174458533

尝试修改这个值 正确显示目标字符

image-20250110174759330

确定使用这个API

2.注入hookiat.dll

hookiat.dll文件中提供了名为MySetWindowTextW()的钩取函数(10001000)。

先用od载入calc.exe,右键选择查杀所有模块间的调用,在所有用到SetWindowTextW()函数的地方下断点

然后做注入

image-20250110182210692

回到OD F9运行 输入后字符变大写了

因为使用的hookiat.dll是韩文原版书里的例子 所以显示的是韩文数字

image-20250110182547985

脱钩:

image-20250110182756972

脱钩后恢复数字:

image-20250110182856416

对hookiat.dll进行分析

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch( fdwReason )
    {
        case DLL_PROCESS_ATTACH : 
            // 保存原始API地址
            g_pOrgFunc = GetProcAddress(GetModuleHandle(L"user32.dll"), "SetWindowTextW");

            // # hook
            // 用hookiat.MySetWindowText()钩取user32.SetWindowTextW()
            hook_iat("user32.dll", g_pOrgFunc, (PROC)MySetWindowTextW);
            break;

        case DLL_PROCESS_DETACH :
            // # unhook
            //  将calc.exe的IAT恢复原值
            hook_iat("user32.dll", (PROC)MySetWindowTextW, g_pOrgFunc);
            break;
    }

    return TRUE;
}

BOOL WINAPI MySetWindowTextW(HWND hWnd, LPWSTR lpString)
{
    wchar_t* pNum = L"零一二三四五六七八九";
    wchar_t temp[2] = {0,};
    int i = 0, nLen = 0, nIndex = 0;

    nLen = wcslen(lpString);
    for(i = 0; i < nLen; i++)
    {
        // 将阿拉伯数字转换为中文数字
        //   lpString是宽字符版本(2个字节)字符串
        if( L'0' <= lpString[i] && lpString[i] <= L'9' )
        {
            temp[0] = lpString[i];
            nIndex = _wtoi(temp);
            lpString[i] = pNum[nIndex];
        }
    }

        // 调用user32!SetWindowTextW() API
        //  (修改lpString缓冲区中的内容)
    return ((PFSETWINDOWTEXTW)g_pOrgFunc)(hWnd, lpString);
}

//复制钩取的函数
BOOL hook_iat(LPCSTR szDllName, PROC pfnOrg, PROC pfnNew)
{
    HMODULE hMod;
    LPCSTR szLibName;
    PIMAGE_IMPORT_DESCRIPTOR pImportDesc; 
    PIMAGE_THUNK_DATA pThunk;
    DWORD dwOldProtect, dwRVA;
    PBYTE pAddr;

    // hMod, pAddr = ImageBase of calc.exe
    //             = VA to MZ signature (IMAGE_DOS_HEADER)
    hMod = GetModuleHandle(NULL);
    pAddr = (PBYTE)hMod;

    // pAddr = VA to PE signature (IMAGE_NT_HEADERS)
    pAddr += *((DWORD*)&pAddr[0x3C]);

    // dwRVA = RVA to IMAGE_IMPORT_DESCRIPTOR Table
    dwRVA = *((DWORD*)&pAddr[0x80]);

    // pImportDesc = VA to IMAGE_IMPORT_DESCRIPTOR Table
    pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)hMod+dwRVA);

    for( ; pImportDesc->Name; pImportDesc++ )
    {
        // szLibName = VA to IMAGE_IMPORT_DESCRIPTOR.Name
        szLibName = (LPCSTR)((DWORD)hMod + pImportDesc->Name);
        if( !_stricmp(szLibName, szDllName) )
        {
            // pThunk = IMAGE_IMPORT_DESCRIPTOR.FirstThunk
            //        = VA to IAT(Import Address Table)
            pThunk = (PIMAGE_THUNK_DATA)((DWORD)hMod + 
                                         pImportDesc->FirstThunk);

            // pThunk->u1.Function = VA to API
            for( ; pThunk->u1.Function; pThunk++ )
            {
                if( pThunk->u1.Function == (DWORD)pfnOrg )
                {
                    // 更改内存属性为 E/R/W
                    VirtualProtect((LPVOID)&pThunk->u1.Function, 
                                   4, 
                                   PAGE_EXECUTE_READWRITE, 
                                   &dwOldProtect);

                    // 修改IAT值(钩取)
                    pThunk->u1.Function = (DWORD)pfnNew;

                    // 恢复内存属性
                    VirtualProtect((LPVOID)&pThunk->u1.Function, 
                                   4, 
                                   dwOldProtect, 
                                   &dwOldProtect);                      

                    return TRUE;
                }
            }
        }
    }

    return FALSE;
}

image-20250113111021751

Call 00CC1090是hookiat.dll的DllMain入口

image-20250113144206726

hook_iat()

上图的蓝色部分代码描述了从PE文件头中查找IMAGE_IMPORT_DESCRIPTION Table的过程。
CALL函数调用返回了ImageBase的地址。
ImageBase地址的值存到EDI中。
[EDI+3C]的值存到EAX中,该值为0F,即IMAGE_NT_HEADER的首地址。
[EDI+EAX+80] = [ 01000000 + 0F + 80 ] = [01000170],该地址即为IMPORT_TABLE的地址。

call 00AF75CA指令用于调用stricmp()函数,通过遍历IID Table比较IID.Name与”user32.dll”字符串,最终查找到user32.dll对应的IID。

如果不相等,就会跳转到00AF10EC地址处,将EBX地址增加14字节,正好等于一个IID结构体的长度。

image-20250113145948679

查找到user32.dll对应的IID后,下面的代码用来在IAT中查找SetWindow API的位置

隐藏进程

本章将讲解通过修改API代码(CodePatch)实现API钩取的技术,还要讲一下有关全局钩取,它能钩取所有进程。此外,还讲解使用上述方法隐藏(Stealth)特定进程的技术。

隐藏进程(stealth process)在代码逆向分析领域中的专业术语为Rootkit,它是指通过修改(hooking)系统内核来隐藏进程、文件、注册表等的一种技术。

image-20250113152303531

技术图表标有下划线的部分表示的就是API代码修改技术。库文件被加载到进程内存后,在其目录映像中直接修改要钩取的API代码本身,这就是所谓的API代码修改技术。

前面我们讲过 IAT钩取技术,如果要钩取的 API不在进程的 IAT中,那么就无法使用该技术。反之,“API代码修改”技术没有这一限制。

API代码修改技术则将API代码的前5个字节修改为JMPXXXXXXXX指令来钩取API。调用执行被钩取的API时,(修改后的)JMPXXXXXXXX指令就会被执行,转而控制hooking函数。

API代码修改就是指直接修改映射到目标进程内存空间的系统 DLL的代码。

进程隐藏用户模式下最常用的是ntdll.ZwQuerySystemInformation()API钩取技术

检测进程的相关API有:

1.CreateToolhelp32Snapshot()& EnumProcess()

2.ZwQuerySystemInformation()

他们都用到了ZwQuerySystemInformation()

练习#1(HideProc.exe,stealth.dll)

HideProc.exe负责将stealth.d文件注人所有运行中的进程。Stealth.dl负责钩取(注入stealth.dll文件的)进程的ntdl1.ZwQuerySystemInformationO) API。接下来我们使用上面2个文件隐藏notepad.exe进程。

HideProc.exe

HideProc.exe程序负责向运行中的所有进程注入/卸载指定DLL文件,它在原有InjectDll.exe程序基础上添加了向所有进程注入DLL的功能,可以认为是InjectDll.exe程序的加强版。

lnjectAIIProcess()

BOOL InjectAllProcess(int nMode, LPCTSTR szDllPath)
{
    DWORD                   dwPID = 0;
    HANDLE                  hSnapShot = INVALID_HANDLE_VALUE;
    PROCESSENTRY32          pe;

    // 获取系统快照
    pe.dwSize = sizeof( PROCESSENTRY32 );
    hSnapShot = CreateToolhelp32Snapshot( TH32CS_SNAPALL, NULL );//获取系统中运行的所有进程的列表

    // 查找进程
    Process32First(hSnapShot, &pe);//将获得的进程信息存放到PROCESSENTRY32结构体变量pe中,进而获取进程的PID
    do
    {
        dwPID = pe.th32ProcessID;

        // 鉴于系统安全性的考虑,对PID小于100的进程不进行注入操作
        if( dwPID < 100 )
            continue;

        if( nMode == INJECTION_MODE )
            InjectDll(dwPID, szDllPath);
        else
            EjectDll(dwPID, szDllPath);
    }
    while( Process32Next(hSnapShot, &pe) );

    CloseHandle(hSnapShot);

    return TRUE;
}

stealth.dll

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    char            szCurProc[MAX_PATH] = {0,};
    char            *p = NULL;

    // #1. 异常处理
    // 若当前进程为HideProc.exe,则终止,不进行钩取操作。
    GetModuleFileNameA(NULL, szCurProc, MAX_PATH);
    p = strrchr(szCurProc, '\\');
    if( (p != NULL) && !_stricmp(p+1, "HideProc.exe") )
        return TRUE;

    switch( fdwReason )
    {
        // #2. API Hooking
        case DLL_PROCESS_ATTACH : 
        hook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, 
                     (PROC)NewZwQuerySystemInformation, g_pOrgBytes);
        break;

        // #3. API Unhooking 
        case DLL_PROCESS_DETACH :
        unhook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, 
                       g_pOrgBytes);
        break;
    }

    return TRUE;
}

存在导出函数SepProcName()

// global variable (in sharing memory)
#pragma comment(linker, "/SECTION:.SHARE,RWS")
#pragma data_seg(".SHARE")
    TCHAR g_szProcName[MAX_PATH] = {0,};
#pragma data_seg()

#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) void SetProcName(LPCTSTR szProcName)
{
    _tcscpy_s(g_szProcName, szProcName);
}
#ifdef __cplusplus
}
#endif

先创建名为“.SHARE”的共享内存节区,然后创缓冲区,最后再由导出函数SetProcName()将要隐藏的进程名称保存到(SetProcName()函数在HideProc.exe中被调用执行)。

hook_ by_code()

BOOL hook_by_code(LPCSTR szDllName, LPCSTR szFuncName, PROC pfnNew, PBYTE pOrgBytes)
{
    FARPROC pfnOrg;
    DWORD dwOldProtect, dwAddress;
    BYTE pBuf[5] = {0xE9, 0, };
    PBYTE pByte;

    // 获取要钩取的API地址
    pfnOrg = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
    pByte = (PBYTE)pfnOrg;

    // 若已经被钩取,返回FALSE
    if( pByte[0] == 0xE9 )
        return FALSE;

    // 为了修改5个字节,先向内存添加“写”属性
    VirtualProtect((LPVOID)pfnOrg, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);

    // 备份原有代码(5字节) 
    memcpy(pOrgBytes, pfnOrg, 5);

    // 计算JMP地址(E9 XXXXXXXX)
    // => XXXX = pfnNew - pfnOrg - 5
    dwAddress = (DWORD)pfnNew - (DWORD)pfnOrg - 5;
    memcpy(&pBuf[1], &dwAddress, 4);

    // “钩子”:修改5个字节(JMP XXXXXXXX)
    memcpy(pfnOrg, pBuf, 5);

    // 恢复内存属性
    VirtualProtect((LPVOID)pfnOrg, 5, dwOldProtect, &dwOldProtect);

    return TRUE;
}

unhook_by_code()

BOOL unhook_by_code(LPCSTR szDllName, LPCSTR szFuncName, PBYTE pOrgBytes)
{
    FARPROC pFunc;
    DWORD dwOldProtect;
    PBYTE pByte;

    // 获取API地址
    pFunc = GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
    pByte = (PBYTE)pFunc;

    // 若已经“脱钩”,則返回FALSE
    if( pByte[0] != 0xE9 )
        return FALSE;

    // 向内存添加”写”属性,为恢复原代码(5个字节)准备
    VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);

    // Unhook
    memcpy(pFunc, pOrgBytes, 5);

    // 恢复内存属性
    VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect);

    return TRUE;
}

NewZwQuerySystemlnformation()

NTSTATUS WINAPI NewZwQuerySystemInformation(
                SYSTEM_INFORMATION_CLASS SystemInformationClass, 
                PVOID SystemInformation, 
                ULONG SystemInformationLength, 
                PULONG ReturnLength)
{
    NTSTATUS status;
    FARPROC pFunc;
    PSYSTEM_PROCESS_INFORMATION pCur, pPrev;
    char szProcName[MAX_PATH] = {0,};

    // 开始前先“脱钩”
    unhook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, g_pOrgBytes);

    // 调用原始API
    pFunc = GetProcAddress(GetModuleHandleA(DEF_NTDLL), 
                           DEF_ZWQUERYSYSTEMINFORMATION);
    status = ((PFZWQUERYSYSTEMINFORMATION)pFunc)
              (SystemInformationClass, SystemInformation, 
              SystemInformationLength, ReturnLength);

    if( status != STATUS_SUCCESS )
        goto __NTQUERYSYSTEMINFORMATION_END;

    // 仅针对SystemProcessInformation操作
    if( SystemInformationClass == SystemProcessInformation )
    {
        // SYSTEM_PROCESS_INFORMATION 类型转换
        // pCur是单向链表的头
        pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;

        while(TRUE)//检PROCESS_INFORMATION结构体链表
        {
            // 比较进程名称
            // g_szProcName 为要隐藏的进程名称
            // (=> 在SetProcName()设置)
            if(pCur->Reserved2[1] != NULL)
            {
                if(!_tcsicmp((PWSTR)pCur->Reserved2[1], g_szProcName))
                {
                    // 从链表中删除隐藏进程的点
                    if(pCur->NextEntryOffset == 0)
                        pPrev->NextEntryOffset = 0;
                    else
                        pPrev->NextEntryOffset += pCur->NextEntryOffset;
                }
                else        
                    pPrev = pCur;
            }

            if(pCur->NextEntryOffset == 0)
                break;

            // 链表的下一项
            pCur = (PSYSTEM_PROCESS_INFORMATION)
                    ((ULONG)pCur + pCur->NextEntryOffset);
        }
    }

__NTQUERYSYSTEMINFORMATION_END:

    // 函数终止前,再次执行API钩取操作,为下次调用做准备
    hook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, 
                 (PROC)NewZwQuerySystemInformation, g_pOrgBytes);

    return status;
}

对NewZwQuerySystemInformation()函数的结构简要说明如下:

“脱钩” ZwQuerySystemInformation()函数;

调用 ZwQuerySystemInformation();

检查SYSTEM_PROCESS_INFORMATION结构体链表,查找要隐藏的进程;

查找到要隐藏的进程后,从链表中移除;

挂钩(hook) ZwQuerySystemInformation()

全局钩取

钩取范围:

①当前运行的所有进程;②将来要运行的所有进程。

Kernel32.CreateProcess() API

由于所有进程都是由父进程(使用CreateProcess())创建的,所以,钩取父进程的CreateProcess() API就可以将stealth.dll文件注入所有子进程。

需要考虑到的:

(1) 钩取CreateProcess() API时,还要分别钩取kernel32.CreateProcessA()、kernel32.CreateProcessW()
这2个API (ASCII版本与Unicode版本)。

(2) CreateProcessA()、CreateProcessW()函数内部又分别调用了CreateProcessInternalA()、
CreateProcessInternelW()函数。常规编程中会大量使用CreateProcess()函数,但是微软的部分软件
产品中会直接调用CreateProcessInternelA/W这2个函数。所以具体实现全局API钩取时,为了准确
起见,还要同时钩取上面2个函数(若可能,尽量钩取低级API)。

(3) 钩取函数(NewCreateProcess)要钩取调用原函数(CreateProcess)而创建的子进程的API。因此,极短时间内,子进程可能在未钩取的状态下运行。

实现上述 效果更好的选择:ntdll.ZwResumeThread() API

Ntdll.ZwResumeThread() API

钩取这个函数,即可在不运行子进程代码的状态下钩取API。

这个函数尚未被公开文档化。所以有很强的不稳定性。

stealth2.dll

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    char            szCurProc[MAX_PATH] = {0,};
    char            *p = NULL;

    // 异常处理使注入不会发生在HideProc2.exe进程
    GetModuleFileNameA(NULL, szCurProc, MAX_PATH);
    p = strrchr(szCurProc, '\\');
    if( (p != NULL) && !_stricmp(p+1, "HideProc2.exe") )
        return TRUE;

    // 改变 privilege
    SetPrivilege(SE_DEBUG_NAME, TRUE);

    switch( fdwReason )
    {
        case DLL_PROCESS_ATTACH : 
            // hook
            hook_by_code("kernel32.dll", "CreateProcessA", 
                         (PROC)NewCreateProcessA, g_pOrgCPA);
            hook_by_code("kernel32.dll", "CreateProcessW", 
                         (PROC)NewCreateProcessW, g_pOrgCPW);
            hook_by_code("ntdll.dll", "ZwQuerySystemInformation", 
                         (PROC)NewZwQuerySystemInformation, g_pOrgZwQSI);
            break;

        case DLL_PROCESS_DETACH :
            // unhook
            unhook_by_code("kernel32.dll", "CreateProcessA", 
                           g_pOrgCPA);
            unhook_by_code("kernel32.dll", "CreateProcessW", 
                           g_pOrgCPW);
            unhook_by_code("ntdll.dll", "ZwQuerySystemInformation", 
                           g_pOrgZwQSI);
            break;
    }

    return TRUE;
}

NewCreateProcessA()

BOOL WINAPI NewCreateProcessA(
    LPCTSTR lpApplicationName,
    LPTSTR lpCommandLine,
    LPSECURITY_ATTRIBUTES lpProcessAttributes,
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    BOOL bInheritHandles,
    DWORD dwCreationFlags,
    LPVOID lpEnvironment,
    LPCTSTR lpCurrentDirectory,
    LPSTARTUPINFO lpStartupInfo,
    LPPROCESS_INFORMATION lpProcessInformation
)
{
    BOOL bRet;
    FARPROC pFunc;

    // unhook
    unhook_by_code("kernel32.dll", "CreateProcessA", g_pOrgCPA);

    // 调用原始API
    pFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateProcessA");
    bRet = ((PFCREATEPROCESSA)pFunc)(lpApplicationName,
                                     lpCommandLine,
                                     lpProcessAttributes,
                                     lpThreadAttributes,
                                     bInheritHandles,
                                     dwCreationFlags,
                                     lpEnvironment,
                                     lpCurrentDirectory,
                                     lpStartupInfo,
                                     lpProcessInformation);

    // 向生成的子进程中注入stealth2.dll
    if( bRet )
        InjectDll2(lpProcessInformation->hProcess, STR_MODULE_NAME);

    // hook
    hook_by_code("kernel32.dll", "CreateProcessA", 
                 (PROC)NewCreateProcessA, g_pOrgCPA);

    return bRet;
}

利用“热补丁”技术钩取API

33.9.1 API代码修改技术的问题

NewCreateProcessA(…)
{
①脱钩
②调用原始API
⑤注入
④挂钩
}

每当在程序内部调用CreateProcessA()API时,NewCreateProcessA()就会被调用执行,不断重复“脱钩” /挂钩。这种反复进行的“脱钩”准钩操作不仅会造成整体性能低下,更 严重的是在多线程环境下还会产生运行时错误,这是由“脱钩” /挂钩操作要对原API的前5个字节进行修改(覆写)引起的。

热补丁技术更稳定。

使用“热补丁”技术时将修改7个字节代码,所以该技术又称为7字节代码修改技术。

image-20250113173138697

通过观察API的起始代码,可以发现,每段API起始代码都相同,而这合起来共7个字节的指令没有任何意义。

热补丁钩取的具体操作:

A.二次跳转
首先将API起始代码之前的5个字节修改为FAR JMP指令(E9 XXXXXXXX),跳转到用户钩取函数处(10001000)。然后将API起始代码的2个字节修改为SHORT JMP指令(EB F9)。该SHORT JMP指令用来跳转到前面的FAR JMP指令处(参考图33-20)。

图33-20 使用“热补丁”技术钩取CreateProcessA() API

调用CreateProcessA() API

在API起始地址(7C80236B)处的JMP SHORT 7C802366 第一次跳转

在地址(7C802366)处JMP 10001000 第二次跳转

B.不需要在钩取函数内部进行“脱钩” /挂钩操作

因为没破坏原函数,还能调用原函数所以不需要在函数内部脱勾

stealth3.dll

BOOL hook_by_hotpatch(LPCSTR szDllName, LPCSTR szFuncName, PROC pfnNew)
{
    FARPROC pFunc;
    DWORD dwOldProtect, dwAddress;
    BYTE pBuf[5] = { 0xE9, 0, };
    BYTE pBuf2[2] = { 0xEB, 0xF9 };
    PBYTE pByte;

    pFunc = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
    pByte = (PBYTE)pFunc;
    if( pByte[0] == 0xEB )
        return FALSE;

    VirtualProtect((LPVOID)((DWORD)pFunc - 5), 7, PAGE_EXECUTE_READWRITE, &dwOldProtect);

    // 1. NOP (0x90)
    dwAddress = (DWORD)pfnNew - (DWORD)pFunc;
    memcpy(&pBuf[1], &dwAddress, 4);
    memcpy((LPVOID)((DWORD)pFunc - 5), pBuf, 5);

    // 2. MOV EDI, EDI (0x8BFF)
    memcpy(pFunc, pBuf2, 2);

    VirtualProtect((LPVOID)((DWORD)pFunc - 5), 7, dwOldProtect, &dwOldProtect);

    return TRUE;
}

BOOL unhook_by_hotpatch(LPCSTR szDllName, LPCSTR szFuncName)
{
    FARPROC pFunc;
    DWORD dwOldProtect;
    PBYTE pByte;
    BYTE pBuf[5] = { 0x90, 0x90, 0x90, 0x90, 0x90 };
    BYTE pBuf2[2] = { 0x8B, 0xFF };

    pFunc = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
    pByte = (PBYTE)pFunc;
    if( pByte[0] != 0xEB )
        return FALSE;

    VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);

    // 1. NOP (0x90)
    memcpy((LPVOID)((DWORD)pFunc - 5), pBuf, 5);

    // 2. MOV EDI, EDI (0x8BFF)
    memcpy(pFunc, pBuf2, 2);

    VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect);

    return TRUE;
}

NewCreateProcessA 不再调用unhook_by_code()与hook_by_code函数,且与已有函数根本的不同在于添加了计算pFunc的语句

BOOL WINAPI NewCreateProcessA(
    LPCTSTR lpApplicationName,
    LPTSTR lpCommandLine,
    LPSECURITY_ATTRIBUTES lpProcessAttributes,
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    BOOL bInheritHandles,
    DWORD dwCreationFlags,
    LPVOID lpEnvironment,
    LPCTSTR lpCurrentDirectory,
    LPSTARTUPINFO lpStartupInfo,
    LPPROCESS_INFORMATION lpProcessInformation
)
{
    BOOL bRet;
    FARPROC pFunc;

    // 调用原始API
    pFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateProcessA");
    pFunc = (FARPROC)((DWORD)pFunc + 2);
    bRet = ((PFCREATEPROCESSA)pFunc)(lpApplicationName,
                                     lpCommandLine,
                                     lpProcessAttributes,
                                     lpThreadAttributes,
                                     bInheritHandles,
                                     dwCreationFlags,
                                     lpEnvironment,
                                     lpCurrentDirectory,
                                     lpStartupInfo,
                                     lpProcessInformation);

    // 向生成的子进程注入stealth3.dll
    if( bRet )
        InjectDll2(lpProcessInformation->hProcess, STR_MODULE_NAME);

    return bRet;
}

要使用热补丁技术需要API有(NOP *5指令+MOV EDI,EDI指令),有些API不是,所以不能用。

高级全局API钩取:IE连接控制

OD使用之查找 API的方法

1.反汇编窗口,右键–查找–当前模块中的名称,就可以列出所有函数了。快捷键Ctrl+N

如果想查看所有引用这个函数的地方,则继续在函数上面右键–查找所有调用引用。

2.bp CreateFileA ,然后F9 跑起来,如果能够断下来。不是很准确,但是在一定条件下,好用。

image-20250114224836595

常规API钩取

使用常规API钩取方法时,每当(要钩取的)目标进程被创建时,都要钩取指定API。图34-9描述了通过DLL注入技术实施常规API钩取操作的情形。

图34-9 常规API钩取

要钩取的目标进程为Test.exe(PID: 2492 )。使用InjDll.exe程序将Hook.dll注入Test.exe进程,然后钩取指定API①。若后面生成了另外一个Test.exe进程(PID: 3796),则必须先向它注入Hook.dll才能(对PID为3796的进程)实现正常的API钩取操作②。也就是说,每当要钩取的目标进程生成时都要手动钩取API。

全局API钩取

图34-10 全局API钩取

InjDll.exe负责将gHook.dll注入Explorer.exe进程(Windows操作系统的基本Shell )。请注意,我们要钩取的进程不是Test.exe,而是启动运行Test.exe的Explorer.exe。

gHook.dll扩展了Hook.dll的功能,它钩取创建子进程的API,每当子进程被创建时,它都会将自身(gHook.dll)注入新创建的进程。所以向Explorer.exe进程(Windows Shell)注入1次gHook.dll后,随后Explorer.exe创建的所有子进程都会自动注入gHook.dll。这就是自动API钩取。

分析钩取哪些API才能使全局API钩取实现。

ntdll!ZwResumeThread() API

创建子进程的API

kernel32!CreateProcess() API

跟踪进入kernel32!CreateProcessW,可以看到在其内部又调用了kernel32!CreateProcessInternelW()

image-20250114181628394

CreateProcessW() API的调用流程如下:

kernel32!CreateProcessW
kernel32!CreateProcessInternalW
ntdll!ZwCreateUserProcess //创建进程(主线程处于挂起状态)
ntdll!ZwResumeThread //主线程被恢复运行(运行进程)

提及的4个API(CreateProcessW、CreateProcessInternalW、ZwCreateUserProcess、ZwResumeThread)中,无论钩取哪个API,都能实现我们的目标—全局API钩取。

redirect.dll

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    char            szCurProc[MAX_PATH] = {0,};
    char            *p = NULL;

    switch( fdwReason )
    {
        case DLL_PROCESS_ATTACH : 
            DebugLog("DllMain() : DLL_PROCESS_ATTACH\n");

            GetModuleFileNameA(NULL, szCurProc, MAX_PATH);
            p = strrchr(szCurProc, '\\');
            if( (p != NULL) && !_stricmp(p+1, "iexplore.exe") )
            {
                DebugLog("DllMain() : current process is [iexplore.exe]\n");

                // 钩取 wininet!InternetConnectW() API 之前预先加载 wininet.dll
                if( NULL == LoadLibrary(L"wininet.dll") )
                {
                    DebugLog("DllMain() : LoadLibrary() failed!!! [%d]\n",
                             GetLastError());
                }
            }

            // hook
            hook_by_code("ntdll.dll", "ZwResumeThread", 
                         (PROC)NewZwResumeThread, g_pZWRT);
            hook_by_code("wininet.dll", "InternetConnectW", 
                         (PROC)NewInternetConnectW, g_pICW);
            break;

        case DLL_PROCESS_DETACH :
            DebugLog("DllMain() : DLL_PROCESS_DETACH\n");

            // unhook
            unhook_by_code("ntdll.dll", "ZwResumeThread", 
                           g_pZWRT);
            unhook_by_code("wininet.dll", "InternetConnectW", 
                           g_pICW);
            break;
    }

    return TRUE;
}

钩取 wininet!InternetConnectW() API 之前预先加载 wininet.dll的原因:

钩取ntdll!ZwResumeThread()API时,需要在相关进程的主线程开始之前拦截控制权,此时,我们要钩取的wininetdll模块可能尚未加载。若模块未加载就钩取其内部API,将导致失败。

NewlnternetConnectW()
wininet!InternetConnectW()的钩取函数为NewInternetConnectW()函数,它负责监视IE的连接地址,IE尝试连接到特定网站时,将其转到我们指定的网站。以下是NewInternetConnectW()函数的代码。

HINTERNET WINAPI NewInternetConnectW
(
    HINTERNET hInternet,
    LPCWSTR lpszServerName,
    INTERNET_PORT nServerPort,
    LPCTSTR lpszUsername,
    LPCTSTR lpszPassword,
    DWORD dwService,
    DWORD dwFlags,
    DWORD_PTR dwContext
)
{
    HINTERNET hInt = NULL;
    FARPROC pFunc = NULL;
    HMODULE hMod = NULL;

    // unhook
    if( !unhook_by_code("wininet.dll", "InternetConnectW", g_pICW) )
    {
        DebugLog("NewInternetConnectW() : unhook_by_code() failed!!!\n");
        return NULL;
    }

    // call original API
    hMod = GetModuleHandle(L"wininet.dll");
    if( hMod == NULL )
    {
        DebugLog("NewInternetConnectW() : GetModuleHandle() failed!!! [%d]\n",
                  GetLastError());
        goto __INTERNETCONNECT_EXIT;
    }

    pFunc = GetProcAddress(hMod, "InternetConnectW");
    if( pFunc == NULL )
    {
        DebugLog("NewInternetConnectW() : GetProcAddress() failed!!! [%d]\n",
                  GetLastError());
        goto __INTERNETCONNECT_EXIT;
    }

    if( !_tcsicmp(lpszServerName, L"www.naver.com") ||
        !_tcsicmp(lpszServerName, L"www.daum.net") ||
        !_tcsicmp(lpszServerName, L"www.nate.com") || 
        !_tcsicmp(lpszServerName, L"www.yahoo.com") )
    {
        DebugLog("[redirect] naver, daum, nate, yahoo => reversecore\n");
        hInt = ((PFINTERNETCONNECTW)pFunc)(hInternet,
                                           L"www.reversecore.com",
                                           nServerPort,
                                           lpszUsername,
                                           lpszPassword,
                                           dwService,
                                           dwFlags,
                                           dwContext);
    }
    else
    {
        DebugLog("[no redirect]\n");
        hInt = ((PFINTERNETCONNECTW)pFunc)(hInternet,
                                           lpszServerName,
                                           nServerPort,
                                           lpszUsername,
                                           lpszPassword,
                                           dwService,
                                           dwFlags,
                                           dwContext);
    }

__INTERNETCONNECT_EXIT:

    // hook
    if( !hook_by_code("wininet.dll", "InternetConnectW", 
                      (PROC)NewInternetConnectW, g_pICW) )
    {
        DebugLog("NewInternetConnectW() : hook_by_code() failed!!!\n");
    }

    return hInt;
}

NewZwResumeThread()函数用来对ntdll!ZwResumeThread() API进行全局钩取

NTSTATUS WINAPI NewZwResumeThread(HANDLE ThreadHandle, PULONG SuspendCount)
{
    NTSTATUS status, statusThread;
    FARPROC pFunc = NULL, pFuncThread = NULL;
    DWORD dwPID = 0;
    static DWORD dwPrevPID = 0;
    THREAD_BASIC_INFORMATION tbi;
    HMODULE hMod = NULL;
    TCHAR szModPath[MAX_PATH] = {0,};

    DebugLog("NewZwResumeThread() : start!!!\n");

    hMod = GetModuleHandle(L"ntdll.dll");
    if( hMod == NULL )
    {
        DebugLog("NewZwResumeThread() : GetModuleHandle() failed!!! [%d]\n",
                  GetLastError());
        return NULL;
    }

    // call ntdll!ZwQueryInformationThread()
    //获取线程句柄所指线程(子进程的线程)所属的子进程的PID
    pFuncThread = GetProcAddress(hMod, "ZwQueryInformationThread");
    if( pFuncThread == NULL )
    {
        DebugLog("NewZwResumeThread() : GetProcAddress() failed!!! [%d]\n",
                  GetLastError());
        return NULL;
    }

    statusThread = ((PFZWQUERYINFORMATIONTHREAD)pFuncThread)
                   (ThreadHandle, 0, &tbi, sizeof(tbi), NULL);
    if( statusThread != STATUS_SUCCESS )
    {
        DebugLog("NewZwResumeThread() : pFuncThread() failed!!! [%d]\n", 
                 GetLastError());
        return NULL;
    }

    dwPID = (DWORD)tbi.ClientId.UniqueProcess;
    if ( (dwPID != GetCurrentProcessId()) && (dwPID != dwPrevPID) )
    {
        DebugLog("NewZwResumeThread() => call InjectDll()\n");

        dwPrevPID = dwPID;

        // change privilege
        if( !SetPrivilege(SE_DEBUG_NAME, TRUE) )
            DebugLog("NewZwResumeThread() : SetPrivilege() failed!!!\n");

        // get injection dll path
        GetModuleFileName(GetModuleHandle(STR_MODULE_NAME), 
                          szModPath, 
                          MAX_PATH);

        if( !InjectDll(dwPID, szModPath) )
            DebugLog("NewZwResumeThread() : InjectDll(%d) failed!!!\n", dwPID);
    }

    // call ntdll!ZwResumeThread()
    if( !unhook_by_code("ntdll.dll", "ZwResumeThread", g_pZWRT) )
    {
        DebugLog("NewZwResumeThread() : unhook_by_code() failed!!!\n");
        return NULL;
    }

    pFunc = GetProcAddress(hMod, "ZwResumeThread");
    if( pFunc == NULL )
    {
        DebugLog("NewZwResumeThread() : GetProcAddress() failed!!! [%d]\n",
                  GetLastError());
        goto __NTRESUMETHREAD_END;
    }

    status = ((PFZWRESUMETHREAD)pFunc)(ThreadHandle, SuspendCount);
    if( status != STATUS_SUCCESS )
    {
        DebugLog("NewZwResumeThread() : pFunc() failed!!! [%d]\n", GetLastError());
        goto __NTRESUMETHREAD_END;
    }

__NTRESUMETHREAD_END:
    //将子进程的主线程恢复运行
    if( !hook_by_code("ntdll.dll", "ZwResumeThread", 
                      (PROC)NewZwResumeThread, g_pZWRT) )
    {
        DebugLog("NewZwResumeThread() : hook_by_code() failed!!!\n");
    }

    DebugLog("NewZwResumeThread() : end!!!\n");

    return status;
}

64位计算

WOW64(Windows On Windows 64 )是一种在64位OS中支持运行现有32位应用程序的机制。

img

64位应用程序会加载kernel32.dll(64位)与ntdll.dll(64位)。而32位应用程序则会加载kernel32.dll(32位)与ntdll.dll(32位),WOW64会在中间将ntdll.dll(32位)的请求(API调用)重定向到ntdll.dll(64位)。

64位Windows中的注册表分为32位注册表项与64位注册表项

img

32位进程请求访问HKLM\SOFTWARE下的键时,WOW64会将其重定向到32位的HKLM\SOFTWARE\Wow6432Node下的键。

x64处理器

x64系统中另一个重要的不同是函数调用约定。32位系统中使用的函数调用约定包括cdecl、stdcall、fastcall等几种,但64位系统中它们统一为一种变形的fastcall。

前四个参数通过寄存器传递,依次使用 RCXRDXR8R9 寄存器。

从第五个参数开始,剩余的参数从右到左依次压入调用栈。

对于浮点和向量参数,使用 XMM0XMM3 寄存器传递,类似于整数参数使用 RCXR9 的方式。

函数返回 时传递参数过程中所用的栈由调用者清理。

64位调试

调试WOW64Test_x64.exe

运行WinDbg64调试器,File–>Open Executable

系统断点

image-20250115114859532

获取EP地址

显示进程PE文件头的命令:!dh WOW64Test_x64

image-20250115115015994

EP地址(RVA)为142C,使用g命令转到该地址处:g WOW64Test_x64 + 142C

image-20250115130933734

显示更多行:u

<

address>L

image-20250115131226024

跟踪位于00000001’40001439地址处的JMP指令,增加指令显示的条数后[u eip L60],查看所有Startup代码

image-20250115141611416

运行(g)调试器,调试器在GetCommandLineW()API处暂停

在GetCommandLineW()下断点

image-20250115134221186

查看栈中存储的返回地址

image-20250115140723222

返回地址是00000001`40001381,跟踪转到该地址处

ASLR

ASLR (Address Space Layout Randomization,地址空间布局随机化)是一种针对缓冲区溢岀的安全保护技术。

借助ASLR技术,PE文件每次加载到内存的起始地址都会随机变化,并且每次运行程序时相应进程的栈以及堆的起始地址也会随机改变。

ASLR.exe文件比ASLR_no.exe文件多岀1个名为“.reloc”的节区,该节区仅在应用了ASLR技术的文件中才会出现。

img

IMAGE_FILE_HEADER\Characteristics

img

拥有.reloc节区的ASLR.exe文件来说,IMAGE_FILE_HEADER 的 Characteristics 属性字段中并不存在 IMAGE _FILE_RELOCS_STRIPPED(l)标志

多岀 1 个.reloc节区,所以Number of Sections值增1

IMAGE_OPTIONAL_HEADER\DLL Characteristics

img

ASLR.exe文件的IMAGE_OPTIONAL_HEADER\DLL Characteristics 中设有 IMAGE_DLL\CHARACTERISTICS_DYNAMIC_BASE(40)标志。

删除ASLR功能

IMAGE_OPTIONAL_HEADER\DLLCharacteristics中设IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE(40)标志,删除它即可删除ASLR功能。

image-20250115153010354

内存的起始地址没有变化说明已经成功了

image-20250115153156569

内核6中的会话

会话指的是登录后的用户环境

以Windows操作系统为例,“切换用户”可以创建本地用户会话, “远程桌面连接”可以创建远程用户会话。

显示当前运行进程所属的会话

image-20250115160952998

Windows 7中共有3个会话(0、1、2),而Windows XP中只有2个会话(0、1)。无 论Windows XP还是Windows 7,系统进程与服务进程都在ID为0的会话(系统会话)中运行。二 者差别在于,第一个登录的用户的会话ID是不同的。Windows XP中,第一个登录系统的用户的会话ID为0;而Windows 7中,第一个登录系统的用户的会话ID为1,非系统会话。

会话隔离机制

从Windows内核版本6开始,第一个登录系统的用户会话ID被设为1,使之与系统会话(ID: 0)区分。分离系统会话与用户会话就取消了它们之间的相互作用,采用这种机制虽然可能引起向下兼容的问题,但能够大大增强系统安全性。微软把这种机制称为会话0隔离机制(Session 0 Isolation)。

内核6中的DLL注入

原有的DLL注入技术是通过调用CreateRemoteThread() API进行的,在Windows XP、2000中能够准确完成DLL注入操作。但Windows 7中该方法不太奏效,准确地说就是,在Windows 7中使用CreateRemoteThread() API无法完成对服务(Service)进程的DLL注入操作。

TLS回调函数

TLS(Thread Local Storage,线程局部存储)回调函数(Callback Function)常用于反调试。

TLS回调函数的调用运行要先于EP代码的执行,该特征使它可以作为一种反调试技术使用。

TLS是各线程的独立的数据存储空间。使用TLS技术可在线程内部独立使用或修改进程的全局数据或静态数据,就像对待自身的局部变量一样。

IMAGE_DATA_DIRECTORY[9]

若在编程中启用了TLS功能,PE头文件中就会设置TLS表(TLS Table)项目,如下图所示(IMAGE_NT_HEADERS-IMAGE_OPTIONAL_HEADER-IMAGE_DATA_DIRECTORY[9])。

img

IMAGE TLS DIRECTORY

typedef struct _IMAGE_TLS_DIRECTORY64 {
    ULONGLONG   StartAddressOfRawData;
    ULONGLONG   EndAddressOfRawData;
    ULONGLONG   AddressOfIndex;         // PDWORD
    ULONGLONG   AddressOfCallBacks;     // PIMAGE_TLS_CALLBACK *;
    DWORD   SizeOfZeroFill;
    DWORD   Characteristics;
} IMAGE_TLS_DIRECTORY64;
typedef IMAGE_TLS_DIRECTORY64 * PIMAGE_TLS_DIRECTORY64;

typedef struct _IMAGE_TLS_DIRECTORY32 {
    DWORD   StartAddressOfRawData;
    DWORD   EndAddressOfRawData;
    DWORD   AddressOfIndex;             // PDWORD
    DWORD   AddressOfCallBacks;         // PIMAGE_TLS_CALLBACK *
    DWORD   SizeOfZeroFill;
    DWORD   Characteristics;
} IMAGE_TLS_DIRECTORY32;
typedef IMAGE_TLS_DIRECTORY32 * PIMAGE_TLS_DIRECTORY32;

#ifdef _WIN64
typedef IMAGE_THUNK_DATA64              IMAGE_THUNK_DATA;
typedef PIMAGE_THUNK_DATA64             PIMAGE_THUNK_DATA;
typedef IMAGE_TLS_DIRECTORY64           IMAGE_TLS_DIRECTORY;
typedef PIMAGE_TLS_DIRECTORY64          PIMAGE_TLS_DIRECTORY;
#else
typedef IMAGE_THUNK_DATA32              IMAGE_THUNK_DATA;
typedef PIMAGE_THUNK_DATA32             PIMAGE_THUNK_DATA;
typedef IMAGE_TLS_DIRECTORY32           IMAGE_TLS_DIRECTORY;
typedef PIMAGE_TLS_DIRECTORY32          PIMAGE_TLS_DIRECTORY;
#endif

所谓TLS回调函数是指,每当创建/终止进程的线程时会自动调用执行的函数。

各函数调用顺序:

DLL_PROCESS_ATTACH:

进程的主线程调用main()函数前,已经注册的TLS回调函数(TLS_CALLBACK1、TLS_CALLBACK2)会先被调用执行。

DLL_THREAD_ATTACH:

创建用户线程(ThreadProc)前,TLS回调函数会被再次调用执行,此时Reason=2(DLL_THREAD_ATTACH)。

DLL_THREAD_DETACH:

ThreadProc()线程函数执行完毕后,Reason=3(DLL_THREAD_DETACH),TLS回调函数被调用执行。

DLL_PROCESS_DETACH:

main()函数(主线程)终止

此时Reason=0(DLL_PROCESS_DETACH), TLS回调函数最后一次被调用执行。

调试TLS回调函数需要注意

若直接使用调试器打开带有TLS回调函数的程序,则无法调试TLS回调函数,因为TLS回调函数在EP代码之前就被调用执行了。

想要调试TLS需要设置:

img

TEB

TEB指线程环境块,该结构体包含进程中运行线程的各种信息,进程中的每个线程都对应一个TEB结构体。

FS段寄存器

SDT(Segment Descriptor Table,段描述符表)

FS段寄存器用来指示当前线程的TEB结构体。

img

实际上,FS寄存器并非直接指向TEB结构体的地址,它持有SDT的索引,而该索引持有实际TEB地址。

  • FS:[0x18]=TEB起始地址

  • FS:[0x30]=PEB起始地址

  • FS:[0]=SEH起始地址

    PEB

PEB是存放进程信息的结构体。每个进程都有自己的 PEB,在进程创建时由操作系统分配。它存储在用户模式下,通常位于进程的地址空间中,为进程的正常运行提供了必要的信息和环境。

SEH

SEH是Windows操作系统默认的异常处理机制。

SEH是Windows操作系统提供的异常处理机制,在程序源代码中使用__try、__except、__finally关键字来具体实现。

正常运行时的异常处理方法

进程运行过程中若发生异常,OS会委托进程处理。若进程代码中存在具体的异常处理(如SEH异常处理器)代码,则能顺利处理相关异常,程序继续运行。但如果进程内部没有具体实现SEH,那么相关异常就无法处理,OS就会启动默认的异常处理机制,终止进程运行。

图48-6 Windows 7的默认异常处理机制

调试运行时的异常处理方法

调试运行中发生异常时,处理方法与上面有些不同。若被调试进程内部发生异常,OS会首先把异常拋给调试进程处理。

调试器几乎拥有被调试者的所有权限,它不仅可以运行、终止被调试者,还拥有被调试进程的虚拟内存、寄存器的读写权限。

被调试者内部发生的所有异常(错误)都由调试器处理。所以调试过程中发生的所有异常(错误)都要先交由调试器管理(被调试者的SEH依据优先顺序推给调试器)。

遇到异常时经常采用的几种处理方法如下所示。

(1)直接修改异常:代码、寄存器、内存

被调试者发生异常时,调试器会在发生异常的代码处暂停,此时可以通过调试器直接修改有问题的代码、内存、寄存器等,排除异常后,调试器继续运行程序。

(2) 将异常抛给被调试者处理
如果被调试者内部存在SEH(异常处理函数)能够处理异常,那么异常通知会发送给被调试者,由被调试者自行处理。

(3) OS默认的异常处理机制
若调试器与被调试者都无法处理(或故意不处理)当前发生的异常,则OS的默认异常处理机制会处理它,终止被调试进程,同时结束调试。

SEH以链的形式存在。第一个异常处理器中若未处理相关异常,它就会被传递到下一个异常处理器,直到得到处理。

IA-32指令

image-20250115181344285

image-20250115182059038

图49-2中,1行代码就是1条指令。(A)区域中是16进制表示的IA-32指令,(B)区域中是与之对应的反汇编代码,(C)区域是指令在内存(或文件)中的实际形式。

立即数(Immediate)也是一个可选项,操作码的操作数为常量时,该常量就被称为立即数。

image-20250115182632264

立即数的长度为1、2、4字节。

静态反调试技术

51.2 PEB
利用PEB结构体信息可以判断当前进程是否处于被调试状态

51.2.1 BeingDebugged(+0x2)
进程处于调试状态时,PEB.BeingDebugged成员(+0x2)的值被设置为1(TRUE);进程在非调试状态下运行时,其值被设置为0(FALSE)。
IsDebuggerPresent()
IsDebuggerPresent()API获取PEB.BeingDebugged的值来判断进程是否处于被调试状态。

51.2.2 Ldr(+0xC)

PEB.Ldr成员是一个指向PEBLDRDATA结构体的指针,而PEBLDRDATA结构体恰好是在堆内存区域中创建的,所以扫描该区域即可轻松查找是否存在0xFEEEFEEE区域。

在进程调试的时,未使用的堆内存区域全部填充着0xFEEEFEEE,这证明正在调试进程。

image-20250116102306755

51.2.3 Process Heap(+0x18)
PEB.ProcessHeap成员(+0x18)是指向HEAP结构体的指针。

进程处于被调试状态时,Flags(+0xC)与Force Flags成员(+0x10)被设置为特定值。
GetProcessHeap()
PEB.ProcessHeap成员(+0x18)既可以从PEB结构体直接获取,也可以通过GetProcessHeap()API获取。GetProcessHeap()API的代码基本类似于IsDebuggerPresent(),按照TEB→PEB→PEB.ProcessHeap顺序依次访问。
Flags(+0xC)& Force Flags(+0x10)

进程正常运行(非调试运行)时,Heap.Flags成员(+0xC)的值为0x2,Heap.ForceFlags成员(+0x10)的值为0x0。进程处于被调试状态时,这些值也会随之改变。

image-20250116103316603

51.2.4 NtGlobalFlag(+0x68)
调试进程时,PEB.NtGlobalFlag成员(+0x68)的值会被设置为0x70

51.3 NtQueryInformationProcess()
通过NtQueryInformationProcess()API可以获取各种与进程调试相关的信息。

与调试器探测有关的成员为ProcessDebugPort(0x7)、ProcessDebugObject-Handle(0x1E)、ProcessDebugFlags(0x1F)。

51.3.1 ProcessDebugPort(0x7)

进程处于调试状态时,系统就会为它分配1个调试端口(DebugPort)。ProcessInformationClass参数的值设置为ProcessDebugPor(0x7)时,调用NtQueryInformationProcess()函数就能获取调试端口。若进程处于非调试状态,则变量dwDebugPort的值设置为0;若进程处于调试状态,则变量dwDebugPort的值设置为0xFFFFFFFF。

CheckRemoteDebuggerPresent()

CheckRemoteDebuggerPresent()API与IsDebuggerPresent()API类似,用来检测进程是否处于调试状态。CheckRemoteDebuggerPresent()函数不仅可以用来检测当前进程,还可以用来检测其他进程是否处于被调试状态。其调用了NtOueryInformationProcess(ProcessDebugPort)API。

51.3.2 ProcessDebugObjectHandle(0x1E)
调试进程时会生成调试对象(Debug Object)。当参数值为ProcessDebugObiectHandle(0x1E)时,调用函数后就能获取调试对象句柄。进程处于调试状态时,调试对象句柄的值就存在;若进程处于非调试状态,则调试对象句柄值为NULL。

51.3.3 ProcessDebugFlags(0x1F)
检测Debug Flags(调试标志)的值也可以判断进程是否处于被调试状态。函数的第二个参数设置为ProcessDebugFlags(0x1F)时,调用函数后通过第三个参数即可获取调试标志的值:若为0,则进程处于被调试状态;若为1,则进程处于非调试状态。

练习StaAD_PEB.exe

运行

image-20250116131716353

IsDebuggerPresent()位置:

image-20250116133408278

image-20250116131556246

PEB.Ldr位置

image-20250116140355435

PEB.ProccessHeap

PEB.ProccessHeap.Flags

位置

image-20250116145358530

51.5 NtQueryObject()
系统中的某个调试器调试进程时,会创建1个调试对象类型的内核对象。检测该对象是否存在即可判断是否有进程正在被调试。ntdll!NtQueryObiectO)API用来获取系统各种内核对象的信息

51.6 ZwSetInformationThread()
下面介绍强制分离(Detach)被调试者和调试器的技术。利用ZwSetInformationThreadO API,被调试者可将自身从调试器中分离出来。

51.7 TLS 回调函数
TLS回调函数是反调试技术中常用的函数,像前面介绍的技术一样,如果不明白其工作原理使用时就会束手无策。
其实,我们并不能将TLS回调本身看作一种反调试技术,但是由于回调函数会先于EP代码执行,所以反调试技术中经常使用它。在TLS回调函数内部使用IsDebuggerPresent()等函数判断调试存在不存在再继续执行。

动态反调试技术

52.2 异常
异常(Exception)常用于反调试技术。正常运行的进程发生异常时,在SEH机制的作用下OS会接收异常,然后调用进程中注册的SEH处理。但是,若进程(被调试者)在调试运行中发生异常,调试器就会接收处理。利用该特征可判断进程是正常运行还是调试运行,然后根据不同结果执行不同操作。

52.2.1 SEH

image-20250116151028340

image-20250116151203873

image-20250116151219046

52.2.2SetUnhandledExceptionFilter()
进程中发生异常时,若SEH未处理或注册的SEH根本不存在,会发生什么呢?此时会调用执行系统的kernel32!UnhandledExceptionFilterO)API。该函数内部会运行系统的最后一个异常处理器,系统最后的异常处理器通常会弹出错误消息框,然后终止进程运行。

52.3 Timing Check
在调试器中逐行跟踪程序代码比程序正常运行(非调试运行)耗费的时间要多出很多。TimingCheck技术通过计算运行时间的差异来判断进程是否处于被调试状态。

52.4陷阱标志
陷阱标志指EFLAGS寄存器的第九个(Index8)比特位。

52.5 0xCC 探测
程序调试过程中,我们一般会设置许多软件断点。断点对应的x86指令为“0xCC”。若能检测到该指令,即可判断程序是否处于调试状态。基于这一想法的反调试技术称为“0xCC探测”技术。

*不值得一看 没看过的避雷

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注