现在的位置: 首页 > 综合 > 正文

幽默人讲解R3枚举目标进程加载模块的全过程

2018年04月06日 ⁄ 综合 ⁄ 共 4904字 ⁄ 字号 评论关闭

作 者: jokersky,这位哥们的描述就是个白痴都能看懂代码,佩服他的口才,特转载之。希望大家听完故事后发表一下感想哦。[/

相信观众朋友通过Google或者Baidu连接到这里的时候已经很清楚自己需要什么啦,我也就不在说明枚举目标进程加载模块的用处啦。

【开发环境】Windows6.0.1+Microsoft Visual 9.0

【开发语言】C++

【功能实现方法】通过ZwQueryVirtualMemory枚举进程

【功能实现步骤】


因为ZwQueryVirtualMemory是未文档化的API,我们不可以象使用MessageBox一样来使用它,,但是我们一定要使用它呢?其实也很容易,我们只需要多做几项工作就可以啦,首先我们先去Google或者Baidu搜索得到它的原形:


NTSTATUS 这种类型需要引用winternl.h不然默认工程不会知道他是一个long型的

NTAPI

ZwQueryVirtualMemory

(

IN HANDLE ProcessHandle, 您要枚举的进程句柄

IN PVOID BaseAddress, 你要工作的起始地址,

IN MEMORY_INFORMATION_CLASS MemoryInformationClass,  你要枚举的类型

OUT PVOID MemoryInformation, 你要把枚举得到的数据存放在什么容器里

IN ULONG MemoryInformationLength, 你要告诉此函数它你的容器的尺寸

OUT PULONG ReturnLength OPTIONAL 工作成功后函数告诉你,他到底给了你多少数量

);



现在有了它的原形后,我们要做的就是模仿他的样子,来自己定义一个函数类型,至于为什么要这么做呢? 我们一会再说

#include <winternl.h>

typedef

NTSTATUS

(WINAPI *ZWQUERYVIRTUALMEMORY)

(

IN HANDLE ProcessHandle, 

IN PVOID BaseAddress, 

IN MEMORY_INFORMATION_CLASS MemoryInformationClass, 

OUT PVOID MemoryInformation, 

IN ULONG MemoryInformationLength, 

OUT PULONG ReturnLength OPTIONAL 

); 

别问我为什么要这么定义,我也不知道,这是C++的语法,想知道就只能去看基础书啦。接下来做什么呢?观众朋友们还记得MEMORY_INFORMATION_CLASS这个类型的参数吗?呵呵,sorry它也是未文档化的,你想使用它,一样模仿它自己做一个吧。


typedef enum _MEMORY_INFORMATION_CLASS 

{

MemoryBasicInformation,

MemoryWorkingSetList,

MemorySectionName

}MEMORY_INFORMATION_CLASS;


对于这哥们呢,也没什么好说的,枚举是语言的基础语法,里面的成员呢字面含义很好理解。如果实在不懂呢,你就把他当成一个领导给他的工人下的三种指示好啦,通过参数的形式来传达给ZwQueryVirtualMemory这个工人。让他按照指示来完成工作。基本的准备工作到这里就告一段落啦,真正的故事开始了,构造一个函数来实现我们的目的吧。


VOID EnumProcessForModule ()                               

这是一个最基本的函数,没有返回类型,没有参数,为什么要这样定义呢?因为每个同学想要达到的效果各不相同,为了避免扰乱视听,在您真正理解了他的实现原理后,自己再做自己喜欢的封装吧。或翻译成其他语言

{


按惯例函数头部创建一些需要的局部变量,虽然C++有个优点可以随时随地创建,但是我不喜欢那么做,为了函数的整体环境规划清爽,不然怕城管来砸,


ZWQUERYVIRTUALMEMORY QueryVirtualMemoryFunction=NULL;

DWORD Index=0;

NTSTATUS NtStatus=0;

MEMORY_BASIC_INFORMATION InfoMation;

BYTE Buffer[MAX_PATH*2+4]={0};

PUNICODE_STRING SectionName=NULL;

DWORD Counter;

CString ModuleAddress;

CString ModuleName;


这些就是我们所需要配合我们完成工作的变量啦,用到那个说那个,Let's Begin

第一个局部变量:这就接上文章头部我们没讲完的故事啦,我们自己模仿了ZwQueryVirtualMemory的样子定义了一个我们自己的函数型类型的指针,但是光这样是没用,只有一个空壳,并没有灵魂,他什么也做不了,所以我们要给他赋予灵魂,什么是它的灵魂呢,那就是它真正的函数地址(如果您对汇编有一定了解,那就会感觉很清晰啦),怎么得到它呢?这就需要GetProcAddress这个API来帮忙啦。他就像鬼差一样,去地府地府里把它的灵魂抓回来,让他回魂,呵呵~



QueryVirtualMemoryFunction=(ZWQUERYVIRTUALMEMORY)

::GetProcAddress

(

GetModuleHandle("ntdll.dll"), //ntdll.dll=地府

_T("ZwQueryVirtualMemory") //ZwQueryVirtualMemory=鬼魂名

);


为了尽量做一个严谨的程序员,我们要尽量对每一步工作的有效性作下判断,这是很有好处的


if (QueryVirtualMemoryFunction==NULL)判断它是否有了灵魂,

{

AfxMessageBox(_T("别瞎TM扯淡啦!"));                

如果没有,那啥都别想啦,一切工作都要靠它来完成你不能让一个死人来干活吧



else 呵呵,哥们如果你还没死,那就别歇着啦,帮兄弟点忙吧。。。

{


到底让他干点什么活呢?不知道观众朋友们知道人口普查不?就是挨家挨户的查户口,HOHO,这哥们就是干这种活的,怎么回事呢?我来说明一下,我们用户层的一个进程呢,就好比是一座城市,他所管理的人口呢就是0-0x80000000(虚拟内存)这么多,行啦,那就让他从0开始去查户口吧,每查完0x1000个人呢。让他回来报告一次就行啦,为什么要以0x1000来做标准呢?这就涉及了PE的一种对齐方式啦,不过不用担心,只是枚举进程模块而已,这么简单的活还不用去学习PE格式,等以后真正不学不行的时候再说。



第二个局部变量:Index建立一个循环在0-0x80000000之间,0x1000一次的跑吧


for(Index;Index<0x80000000;Index+=0x1000)

{

第三个局部变量:NtStatus你把它当成锦衣卫好了,他就是监督工人是否正常干活,就是负责接收函数的long型返回值来判断执行的效果

NtStatus=QueryVirtualMemoryFunction

(

(HANDLE)-1, 因为我们是注入的,所以-1就代表当前进程的句柄,HOHO,i love dll

(PULONG)Index,  工作地址哟,第一次从0x00000000开始,第二次从0x00001000MemoryBasicInformation, 记得不?枚举类型,普查嘛,要有目的的查呀,他就代表我们的目的

&InfoMation, 第四个局部变量,你把它理解成移动硬盘好啦,查到的数据放到里面,回来交给我们

sizeof(InfoMation),  你要告诉工人。你给了他一个多大的移动硬盘才行,HOHO

null 最后一个参数我们用不到,无视它,

);


if (NtStatus>=0) 老规矩,判断下>=0就表示成功,这不是我规定的,是MicroSoft规定的

{

呵呵,既然成功了,工人拿着查到的数据回来啦交给我们检查,而什么是我们关心的呢,那就是这次普查的0x1000个人的类型啦,我们就来看下移动硬盘里


if (InfoMation.Type==MEM_IMAGE) 

Type就表示这些人的类型啦,如果是暴民,那我们就要严肃的处理啦,MEM_IMAGE这样的就表示暴民啦,HOHO,开个玩笑,其实它是0x1000这个大小的

一块内存的描述标志,比如这块内存的标志是MEM_IMAGE哪就说明他是一个我们要的模块啦


{


既然找到了我们要的东西,接下来作什么呢,我们要对抓到的暴民进行再过滤以免抓错人:


if ((DWORD)InfoMation.AllocationBase==Index)

{

这么做是为什么,因为你不这样过滤,你会得到几千个模块,为什么呢?因为模块之间互相映射,唉,挺乱的,实话说我解释的不太清楚,如果你真的想知道你要花几个星期的时间去深入学习下PE啦,如果你不在意你就按照我的方法做就行啦,他其实就是过滤掉同名的模块。


NtStatus=QueryVirtualMemoryFunction

[/COLOR]

啊,你又看见他啦,为什么呢?因为它普查出了暴民,你就要派他再去一次对这些暴民作深入的调查,(事实是当我们找到模块后,我们要再次调用它,让他去把这个模块的名称信息弄回来)


(

(HANDLE)-1, 不变

(PULONG)Index, 模块地址啦,

MemorySectionName, 看见了吗,我们换了一个命令让他做不同的工作(内存节名枚举)

Buffer, 

第五个局部变量:BYTE Buffer[MAX_PATH*2+4]换一个牌子的移动硬盘, 我要详细的解释一下为什么要用这种类型的来接收数据,其实呢,MemorySectionName这种类型的枚举,他也像MemoryBasicInformation类型一样有一个对应的结构用来接收数据的,那就是:


typedef struct MEMORY_SECTION_NAME

{

   UNICODE_STRING SectionFileName;

   WCHAR       NameBuffer[ANYSIZE_ARRAY];

} *PMEMORY_SECTION_NAME;


但是为什么不用它呢?因为它更底层一些在R3层用它很麻烦,所以放弃它,定义一个和他同样大小的数组,一样可以接收数据,关键在于这个尺寸问题,因为这个结构是在宽字节环境所以我们来计算一下这个结构到底有多大。首先来看第一个成员UNICODE_STRING他的最大值就是MAX_PATH这

个宏表示260个字节,又因为它是宽字符环境所以就要*2啦,再来看第二个成员WCHAR大家知道这个宏[ANYSIZE_ARRAY]表示2,同样因为UNICODE的关系要*2也就是4,所以这个结构最大不会超过[MAX_PATH*2+4],那么定义这个尺寸的一个数组接收数据就绰绰有余啦,大家知道数组作为参数的时候会自动降级为指针, 怎么样?还好理解吧?继续......


sizeof(Buffer),解释过了。

null

);


if (NtStatus>=0)老规矩,检查,

{


走到这一步我们的工作也就基本完成了,我们得到了2个重要的数据,一个是模块的基址也就是我们做了GetModuleHandle();的工作, 还有就是模块名称啦,这个循环执行结束,进程内所加载的模块就被我们全部枚举出来啦。这里呢。我用2个CString来接收他们,并用一个列表控件来输出.



SectionName=(PUNICODE_STRING)Buffer;我们的移动硬盘要换个驱动,不然系统不认

ModuleName=SectionName->Buffer;

ModuleAddress.Format(_T("%x"),Index);

Counter=ListGoodsFilter.InsertItem(Counter,ModuleAddress);

ListGoodsFilter.SetItemText(Counter,1,ModuleName);

这些都是界面编程和字符串的事,我就不解释啦,

}

}

}

}

}

}

}
【上篇】
【下篇】

抱歉!评论已关闭.