读内存?读的什么内存?
果冻的阿狸
编辑于 2021年10月08日 01:31

火绒剑监控记录

这里逐步说明如何配合Cheat Engine和IDA Pro等工具详细分析火绒剑的监控记录

注:火绒剑的部分进程监控功能,如:SYS_enumproc(枚举进程)/PROC_readvm(跨进程读内存)/PROC_writevm(跨进程写内存)等或更多项目,在64位系统或高于Windows10(1709,build 10.0.16299)版本的系统下是不起效的监控不到任何记录,在32位的Windows7到Windows10(1709 build 10.0.16299)才可正常监控到记录,鉴于现在大部分人是64位系统或版本较新,如果复现测试请注意自己的操作系统版本。

PROC_open

PROC_open 动作信息

PROC_open监控记录说明进程调用了OpenProcess函数打开了某个进程,一般来说Windows下进程间操作前第一步是需要打开目标进程的,微软文档中关于此函数的介绍如下:

https://docs.microsoft.com/zh-cn/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess

然后我们再看参数

target_pid是被打开的目标进程的ID,这里为284

access为打开进程所请求的访问权限,这里为0x00000410,代表所请求权限为PROCESS_VM_READ | PROCESS_QUERY_INFORMATION,即读取进程内存和查询进程信息权限

PROC_open 调用栈信息

然后我们看调用栈信息,其中第1帧记录,KernelBase.dll+0xeca0,我们可以用CE定位到这个代码的位置看一下

CE定位反编译 KernelBase.dll+0xeca0

这里可以看出来它确实是调用了OpenProcess这个函数

然后我们可以看到,在PROC_open动作后有5个连续的PROC_readvm,这里我们分别称为PROC_readvm 1 ~ 5,然后一个一个看。

PROC_readvm - 1

PROC_readvm - 1 动作信息

PROC_readvm记录是说明调用了ReadProcessMemory这个跨进程读内存的函数,微软文档中关于这个函数的说明如下:

https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-readprocessmemory

然后我们看具体参数

target_pid:284,表示读取的目标进程ID为284

base:0x7F759008 表示读取的地址为0x7F759008 

bytes_read:0x00000004 表示要读取的长度为4字节

datalen:0x00000004 表示读取实读取到的长度为4字节

data: 这里是读取到的数据内容

00000000: 00 00 85 00  

PROC_readvm - 1 调用栈信息

然后我们看一下调用栈,其中第4帧的记录,KernelBase.dll+0x75980的代码地址,我们用CE定位一下反编译查看。

我们看到这是一个名为GetModuleBaseNameA的系统函数,说明这个读内存是由GetModuleBaseNameA这个系统函数发起的,这个函数的作用是获取模块的名字,比如进程的名字,微软文档上对这个函数的说明如下

https://docs.microsoft.com/en-us/windows/desktop/api/Psapi/nf-psapi-getmodulebasenamea

PROC_readvm - 2

前面已经介绍过PROC_readvm记录的参数,这里直接复制出来

target_pid:284

base:0x7F75900C

bytes_read:0x00000004

datalen:0x00000004

data:

00000000: 40 94 03 77                                     ; @..w            =

PROC_readvm -2 调用栈

调用栈里可以看到,第4真同样是KernelBase.dll+0x75980,也就是说这个读取操作也是由GetModuleBaseNameA函数发起的。

PROC_readvm - 3

前面已经介绍过PROC_readvm记录的参数,这里直接复制出来

target_pid:284

base:0x77039454

bytes_read:0x00000004

datalen:0x00000004

data:

00000000: E8 14 AB 00                                     ; ....            

PROC_readvm - 3调用栈

调用栈里再次看到,第4真同样是KernelBase.dll+0x75980,也就是说这个读取操作也是由GetModuleBaseNameA函数发起的。

PROC_readvm - 4

前面已经介绍过PROC_readvm记录的参数,这里直接复制出来,这里可以看到,前面都是4字节的数据,这里是长度比之前长,是十六进制的0x48个字节,十进制就表示就是72个字节

target_pid:284

base:0x00AB14E0

bytes_read:0x00000048

datalen:0x00000048

data:

00000000: 00 14 AB 00 4C 94 03 77 08 14 AB 00 54 94 03 77 ; ....L..w....T..w

00000010: 00 00 00 00 A0 F5 94 00 00 00 85 00 E6 4E 85 00 ; .............N..

00000020: 00 20 01 00 44 00 46 00 38 12 AB 00 1C 00 1E 00 ; . ..D.F.8.......

00000030: 60 12 AB 00 CC 22 00 00 FF FF 00 00 E4 1A AB 00 ; `...."..........

00000040: 10 54 03 77 74 6B 08 53                         ; .T.wtk.S        

PROC_readvm - 4调用栈

调用栈里再次看到,第4真同样是KernelBase.dll+0x75980,也就是说这个读取操作也是由GetModuleBaseNameA函数发起的。

PROC_readvm - 5

这里可以看到参数又不一样了,但是很明显的一点,这里读到的数据是进程的名字

target_pid:284

base:0x00AB1260

bytes_read:0x0000001E

datalen:0x0000001E

data:

00000000: 74 00 61 00 73 00 6B 00 68 00 6F 00 73 00 74 00 ; t.a.s.k.h.o.s.t.

00000010: 65 00 78 00 2E 00 65 00 78 00 65 00 00 00       ; e.x...e.x.e...  

PROC_readvm - 5 调用栈

调用栈里再次看到,第4真同样是KernelBase.dll+0x75980,也就是说这个读取操作也是由GetModuleBaseNameA函数发起的。

PROC_readvm 总结

通过这5条记录我们可以看到,这5此读内存操作都是由GetModuleBaseNameA这个获取进程名字的函数发起,而QQ调用这个函数的目的是为了获取进程的名字,那么为什么GetModuleBaseNameA这个函数会读那么多次内存呢?这样我们就需要使用IDA反编译一下这个系统函数看他是如何工作的

首先我们需要在系统目录里找到这个函数所在的模块,KernelBase.dll,然后把它拖到IDA进行分析

KernelBase.dll

然后我们在KernelBase.dll这个模块的导出函数里找到GetModuleBaseNameA这个函数,然后定位到其代码位置

KernelBase.dll导出函数

然后我们在IDA里定位到GetModuleBaseNameA的函数位置后按F5将汇编代码转换为伪代码方便分析

GetModuleBaseNameA

这里可以看到GetModuleBaseNameA是调用了K32GetModuleBaseNameW完成真是操作后转换编码字节序返回的,那我们需要继续查看K32GetModuleBaseNameW的代码

K32GetModuleBaseNameW

这里可以看到K32GetModuleBaseNameW中第一步是调用FindModule这个函数找到指定模块后从目标进程中读取到了进程的名字,说明这对应的是PROC_readvm-5那次读取内存的操作,前面的4次应该是在FindModule这个函数里,我们继续查看

FindModule

这里可以看出FindModule这个函数的工作方式

1.NtQueryInformationProcess这个函数获取到进程环境信息块(PEB)结构的地址,即0x7F75900。

2.调用ReadProcessMemory读取ImageBaseAddress,对应PROC_readvm -1,读取地址为PEB地址0x7F759000+0x8=0x7F759008

3.调用ReadProcessMemory读取Ldr地址,对应PROC_readvm-2,读取地址为PEB地址0x7F759000+0x0C=0x7F75900C,读到的Ldr地址为0x77039440

4.调用ReadProcessMemory读取InMemoryOrderModuleList,对应PROC_readvm-3,读取地址为上一步的0x77039440+0x14=0x77039454,读到的InMemoryOrderModuleList地址为0x00AB14E8

5.调用NtReadVirtualMemory读取Ldr_DATA_TABLE_ENTRY,对应PROC_readvm-4,读取地址为上一步的地址0x00AB14E8-0x8=0x00AB14E0,读到的数据为

00000000: 00 14 AB 00 4C 94 03 77 08 14 AB 00 54 94 03 77 ; ....L..w....T..w

00000010: 00 00 00 00 A0 F5 94 00 00 00 85 00 E6 4E 85 00 ; .............N..

00000020: 00 20 01 00 44 00 46 00 38 12 AB 00 1C 00 1E 00 ; . ..D.F.8.......

00000030: 60 12 AB 00 CC 22 00 00 FF FF 00 00 E4 1A AB 00 ; `...."..........

00000040: 10 54 03 77 74 6B 08 53                         ; .T.wtk.S        

从数据结构的0x30偏移位置,我们可以看到模块名字的地址为0x00AB1260,这对应了PROC_readvm-5,也就是K32GetModuleBaseNameW中最后那次读取内存的操作,读取到了模块的名字。

总结

这里从头到尾分析了一遍这几个内存操作的具体内容,以及具体操作来源,这样才能解释清楚为什么会有那么多次PROC_readvm,这是因为GetModuleBaseName这个获取进程名字的函数Windows的系统内部实现原理就是需要多次读取内存才能获取到模块的名字,主要是Windows系统是一个很复杂的东西,它内部有许多数据结构,而进程相关的数据结构的设计导致了了它如果用GetModuleBaseName这个函数获取模块名称就需要很多次内存读取操作才能完成,而QQ调用GetModuleBaseName这个函数的目的只是为了获取进程的名字而已,产生这么多内存操作的原因是Windows的函数就是这样做的。

那有人就会说,获取进程名字也可以不产生那么多读取操作啊,比如调用CreateToolhelp32Snapshot创建进程快照的函数,这个函数也可以达到获取进程名称的目的,并且不会产生内存读的操作,那是因为Windows对这个函数的实现原理是调用NtQuerySystemInformation这个函数,它是将获取进程信息的工作交给了系统内核完成,内核会将进程列表直接返回给调用者,而GetModuleBaseName这个函数的操作是在应用层完成读取的操作,所以说会产生那么多读取操作,这其实是Windows对类似功能的不同实现方式而已,这几种函数达到的目的是一样的,只是实现方式有区别,具体使用哪个其实不是很重要的事情。

操作系统是个非常复杂的东西,内存里的数据也是多如牛毛,windows api的量非常大,有很多系统函数的背后实现原理就是读写其他进程的内存,这里只是解释了几个系统函数背后的实现原理,就已经快5000字了,而且要再仔细解释还能继续说,如果要把每个程序的每个读取操作全部解释清楚,这太难了,所以说如果有疑问希望还是自己多看书学习,其实很多读内存操作,大都是些正常的操作,如果有疑问可以自己多分析,可以参考我的分析方式。

如果文章中存在错误欢迎指出,会及时改正,本人只是个纯粹的技术人员,客观的技术讨论我很喜欢,但是如果要跟我用胡搅蛮缠不讲理的方式说话,对不起,搞技术的人是很纯粹的客观理性,谢谢。