网蝉安全沙龙

公告

NetCicala Security Salon于2008-4-25正式开通
==================================================================
显示标签为“Programmer”的博文。显示所有博文
显示标签为“Programmer”的博文。显示所有博文

[zz]在EXE中创建、加载内核驱动


很早就看过高手写过的文章,<<脚本的故事>>、<>,今天我也来班门弄斧一把。
在 网上见过一些rootkit,通常做法是写驱动,然后将驱动包含在exe中,运行时释放驱动为一个独立文件,然后加载驱动,由驱动完成Rootkit的各 种功能,这样就会生成两个文件。为什么不把它们在一个exe文件中实现呢?今天我们就试验一下,能不能在exe中实现驱动的功能。
其实这个想法来自一个程序。当时想试验一下arp攻击,还以为win32下有相关的系统调用,google了一把,不,baidu了一把,google看 不懂,说win32下发包IpPacket到可以,Arp只能用IpHlpApi提供的SendARP()函数发送(可能孤陋寡闻),而且不能手动构造 arp包,而且SP2下发送TCP SYN是不允许的。晕。借助工具的话就要使用WinPcap,或者自己写驱动。我当时想是否能像Fport那样,直接操作AFD(好像是TDI,还是\ device\tcp udp?),会不会能成功。看了看网上流传的源码,好复杂。WinPcap导出的调用很简单,而且linux下可以平滑过渡。不过,我的初始想法是一个比 较隐蔽的程序,这就要求它有很高的独立性。所以,很自然的落到了exe驱动上来。
第一个想法:在exe中导出DriverEntry,看了看sys文件,好像没有这个导出函数,那么程序入口就是DriverEntry,但是win32 下exe的ImageBase是4MB,驱动好像是0x10000,不知道win32在加载时会不会对程序作重定位。将EXE的入口修改为 DriverEntry…,然后在入口函数中作判断,是用户态还是ring0。好像VC的编译器会在入口加入xxx代码,这样很麻烦,而且相当麻烦。以上 证明这个想法暂时不对。不过突然有个想法,程序里面包含main 和DriverEntry,入口为main,运行后修改入口为DriverEntry,然后加载驱动…,弊端很多…
第二个想法,在exe中执行ring0代码。这是老想法了,网上有无驱执行ring0的源码,我当初也装模作样的分析了一下。具体的实现方法是使用\ Device\PhysicalMemory对象映射物理内存并修改(GDT,IDT,或者hook内核函数),对此有篇文章有精彩的解说(baidu关 键字’/dev/kmem ring0’)。个人觉得hook内核函数比较简单快速,进入ring0后不需要作任何环境修改。如果使用中断门或者调用门,构造就很麻烦,而且好像在哪 见过门的段地址要自己重新构造…。没试验过,仅是猜测。
具体思路:
(1) 使用ZwQuerySystemInformation获取ntoskrnl.exe的基址
(2) 用户态加载ntoskrnl.exe,获取内核api相对于它所在模块基址的偏移,加上内核镜像的基址获得真正的api的地址,计算需要使用的Kernel API的地址。
(3) 将api地址减去0x80000000(NT使用flat内存映射,前512Mb物理内存直接映射到2GB,不知道打开/3GB开关是什么情况,暂不考虑),获得api的物理内存地址
(4) 映射物理内存并写入hook代码
(5) 调用Kernel API对应的NaiveAPI,进入ring0
(6) 系统调用在当前进程的内存空间执行,所以可以使用整个进程空间,直接调用(2)计算的KernelAPI完成任务(注意Irql)
看 到这里,问题似乎解决了。但是,以上方法只能执行ring0代码并不能实现驱动所实现的功能,功能还非常弱,对于一个简单的 MemoryDump/ProcessList还可以,其它很多都受限制。由于代码运行于进程用户内存空间,使用内核栈,如果在别的进程空间调用将 KeBugCheck之类的…。很自然,我们会想到把代码拷贝到内核空间去运行。这的确是解决方法。
第一种方案:由于KernelAPI大部分由 ntoskrnl.exe和HAL.DLL导出,我们可以计算这些导出函数的地址,调用ExAllocatePool()分配空间(设 pKeApiSet),拷贝。然后使用同样的方法来拷贝函数代码到内核,然后将pKeApiSet作为拷贝函数的参数,调用,这样代码就运行在内核空间 了,但是还是只能在当前线程的时间片下运行。如果向其它驱动注册回调函数就麻烦了,因为这些函数都有固定的格式,而且调用时无法取得我们自己的参数。使用 过下面的方法解决了这个问题:
//===========================================
//修改堆栈,对于_fastcall没试过 !!!!!
#define X_RELOC_CODE "\xFF\x34\x24"\
"\xC7\x44\x24\x04\x00\x00\x00\x00"

//若想对齐,此代码后面可以加入 nop nop
//若想跳转,此处加入push addr; ret即可,好像不能直接jmp abs_addr??(VC)
//vc编译使用相对地址
// __asm push dword ptr [esp]
// __asm mov dword [esp+4],lpParam

#define X_RELOC_HEADER_SIZE 11
#define X_RELOC_CODE_SIZE 11

//辅助结构,便于抽象理解,汗
typedef struct
{
char b1[7];
void *lpParam;
}X_RELOC_HEADER;

//=================================================

X_RELOC_HEADER *pCode=( X_RELOC_HEADER *)__KeApiSet.ExAllocatePool(NonPagedPool,UPALIGN(size+ X_RELOC_CODE_SIZE,4096));

Memcpy((char *)pCode, X_RELOC_CODE, X_RELOC_CODE_SIZE);
pCode->lpParam=(void *)&__KeApiSet;
//x_size>=MyCallback函数的大小
memcpy((void *)((ULONG)pCode+11),(void *)MyCallback,x_size);

//这里将pCode注册到其它驱动或者对象的回调函数去,对于新的回调函数添加一个参数(指向__KeApiSet)在最左边。
例如,原来的回调函数为
OriginalCallback(Type1 Param1,Type2 Param2….)
新的回调函数如下
MyCallback(void *lpParam, Type1 Param1,Type2 Param2…)
其实就是在调用函数时修改函数堆栈

------------------------------------------------------------------------

NewCallback à |EIP |OriginalCallbackà |EIP | à |MyParam | |Param1| à |Param1 | |Param2| à |Param2 |

堆栈状态

------------------------------------------------------------------------

CodeAddrà NewCodeLayoutà

|push ebp | |push dword ptr [esp] |

|mov ebp,esp | |movdword [esp+4],xParam |

|sub esp,xxxx | à |push ebp |

| …. | |mov ebp,esp |

代码状态

------------------------------------------------------------------------

还有一种方法是使用EIP定位。Linux下的动态库有一种浮动代码的技术,其中就用到了使用EIP定位,原理如下
Call Nextaddr___ //push eip(=Nextaddr___) + jmp addr
Nextaddr___: //
Pop ebx //ebx=EIP
在刚进入函数时获取EIP值,将参数拷贝到代码前面,eip减去一个数值就可以找到参数。不过这样好象有点麻烦。

后来想起来VC编译器支持一种naked函数,用这个函数__declspec(naked)写就可以了,白白花费了这么长时间。

应用:使用第一种方法(代码拷贝到内核空间,修改堆栈),在进入ring0后,可以注册一个回调函数到IPFilterDriver,实现一个简单的Ip 过滤,美其名曰:“放火墙”。 IoGetDevicePointerIoBuildDeviceIoControlRequestIofCallDriver。细节可以看网上的高 手写的xxxWndows下防火墙。

第三种方法,好像有篇文章说可以使用ZwSetSystemInformation(SysLoadAndCallImage),可以直接加载并运行,而且有种rootkit就使用了这种方法。具体没有看过,可以到网上看看。


本来到这里基本算结束了,但是上面的方法只能实现简单的功能,而且代码有点复杂,对于第二种方法,特别是函数调用,必须使用额外参数来定位。这样还不如单 独写一个sys文件快。这时我考虑到了代码重定位,于是baidu一下,找到了局部变量大哥写的《NT环境下进程的隐藏》,如获珍宝,自己按着方法把代码 重写了一遍,学到不少。其实PE教程说的很明白,就是看不下去…。
对EXE进行重定位方法我就不多说了。具体方法就是在内核空间中分配代内存,拷贝自身镜像到内核,然后对内核空间的镜像作重定位。由于进入ring0后, 在win2k下还可以调用用户态API(printf()还可以使用,在当前进程空间),在xp下调用会导致进程退出,估计对调用前后状态或者地址作了限 制。
有了重定位,就可在函数中使用全局变量,这样对函数的限制大大减小。但还有个问题,就是对kernel API的调用还是使用的显式调用,非常麻烦,对每个函数都要GetProcAddress(),于是我将ntoskrnl.lib和hal.lib加入到 程序中,原本以为这样就可以了,可是编译通过,程序根本运行不起来,运行便出错。不知道是机器上的VC有问题,还是…,估计是加载EXE时,回加载它所使 用的所有库,估计在加载ntoskrnl.exe时出错了。我手动修改PE的导入表,去掉ntoskrnl.exe模块,程序便能正常运行。
卡到 了这里,决定使用先编译,再用工具修改导入表,去掉ntoskrnl.exe的引用。方法就是将ntoskrnl.exe的描述符放到导入表的最后一项, 然后将ntoskrnl.exe的IMAGE_IMPORT_DESCRIPTOR拷贝到程序的其他地方(就像病毒那样,将代码写入到PE的间隙处,VC 默认对齐大小是4096,这样会有很多空隙),然后将这项置零。这样加载时就不会加载ntoskrnl.exe。说起来容易,实际根本不可行,也没试,因 为我查到了VC编译器的延时加载功能(/DelayLoad:dll_name)。
通常在调用dll导出函数时,编译器为所调用的函数生成导入地址表,将函数所在模块生成IMAGE_IMPORT_DESCRIPTOR,win32在加 载程序时会自动加载指定的模块,并确定导入函数的实际地址。DelayLoad将所调用的导出函数生成一个stub,类似如下:
Pid=PsGetCurentProcessId();
Call 40E68E //这里使用了Debug模式/增量编译,所以会有个跳转
//release/static/GZ估计为call dword ptr[xxxxxxxx]
-------------------------------------------------------------------------------
0040E682 Push ecx
Push edx
Push 00429364 //压入IAT地址
Jmp 40E66E
-------------------------------------------------------------------------------
0040E68E jmp dword ptr [00429364]//0040E682
-------------------------------------------------------------------------------
0040E66E Push 00429000 //压入PCImgDelayDescr地址,类似导入表
Call 00401087 //__delayLoadHelper(pImgDelayDesc,ppfnIATEntry)
Pop edx //__ delayLoadHelper计算API真正地址,填入IAT
Pop ecx
Jmp eax //eax=Real Address==[IAT]
-------------------------------------------------------------------------------
由 于PCImgDelayDescr被放置在单独的延时输入描述符目录中,而不是通常的输入表目录,所以win32在加载时,这些延时加载的模块不会被加 载。第一次调用函数时IAT间接指向__delayLoadHelper函数,而__delayLoadHelper根据函数所在模块名和函数名计算函数 地址,然后写会IAT,下次执行直接跳转到真正的函数地址上去。
DelayLoad的这种特性正好解决了当前的问题。方法就是连接ntoskrnl.lib和hal.lib,然后将ntoskrnl.exe和 hal.dll延时加载,自己重写__delayLoadHelper函数来取得KernelAPi的地址,这样问题迎刃而解了。VC6自己的 __delayLoadHelper函数在文件DELAYHLP.CPP中实现,其中加入了对函数Hook的功能,就是导出两个函数指针,在 __delayLoadHelper中先调用指针指向的函数,若为空怎使用默认的LoadLibrary()和GetProcAddress()。这样如 果想hook函数只需要将模块延时加载然后自己实现__pfnDliNotifyHook即可。
问题:
(1) 由于在内核之中,(同一进程空间)不能调用win32API (XP下不行,2K下可以,注意:非GUI API),如果还可能在其它进程空间使用,则根本不能使用UserAPI
(2) 经过重定位的镜像在获取API名称时有问题,镜像基址大于0x80000000,地址的最高位肯定为1,这在PE中与函数的INT冲突,信息会丢失。
解决方法:
(1) 其实使用的API就是LoadLibrary() 和GetProcAddress,重写LoadLibrary和GetProcAddress函数即可。对于LoadLibrary(),只需要调用 ZwQuerySystemInformation获得模块的基址即可;对于GetProcAddress,根据模块镜像的基址找到 IMAGE_NT_HEADERS->IMAGE_EXPORT_DIRECTORY逐个查找即可,注意函数使用序号(ordinal)的查找。
(2) WIN32 PE文件的导入表和延时加载描述符表都有各自的INT和IAT(暂时这样理解),对于函数地址的确定都使用类似的机制。从INT获得函数名称或者函数 ordinal,然后从模块里查找函数地址,填入IAT,只是确定函数地址时间有不同。对于函数名称,,PE使用IMAGE_THUNK_DATA来描 述,它其实是一个DWORD指针,指向一个DWORD数组,对于数组中的每个DWORD,如果最高位为1(& IMAGE_ORDINAL_FLAG32),则此函数按序号引入,否则此DWORD指向一个IMAGE_IMPORT_BY_NAME结构,此结构包含 函数名称字符串的RVA。普通Win32进程运行在0-2GB中,所以这没问题。但我们的镜像被拷贝到0xFExx xxxx ,重定位后IMAGE_THUNK_DATA的地址肯定>0x80000000,所以只能根据经验,全部按名称引入,理由是NT、95的函数序号并 不一致(从书上看的,很简单,xp导出的函数比2K多,如果按序号肯定出问题)。就在这个地方,没有调试器,只有printf,浪费了两天的时间,晕。还 以为是VC自带的代码决不会有问题,重写__delayLoadHelper后解决(__delayLoadHelper对函数Ordinal & IMAGE_ORDINAL_FLAG做了判断…)。

呵呵,终于到主题了,其实前面的要困难些。当代运行在ring0时,它已经能实现多数功能了,但是如果要作个木马,后门之类的,还要解决与用户态通信的问 题(其实并不是这样),所以想到了事件之类的,或者ring0 call ring0,apc…最后还是落到了文件设备上。通过文件设备,任何进程都可以与ring0通信,而且创建驱动的宿主可以安全撤退。如果使用ring0 代码(call gate,hook),则进程不能退出,因为线程的系统调用没有返回,所以即使强行中止也无济于事。如果通过ring0代码创建一个驱动,然后ring0 返回到ring3,进程就可以安全退出。而这时的代码还驻留在内核的未分页区,以回调的方式响应请求。
学过操作系统都知道,VFS的特性,就是所有设备都是用文件来表示,其实质就是分层的函数调用,使用注册机制来处理各种设备之间的差异。实际实现表现为一 个文件对象对应一个设备对象,同时设备对象又对应一个驱动对象,不同的i/o请求通过文件对象分发到不同的设备,进而转发到设备对应的驱动对象,由驱动完 成最终请求。这种分层的设计有何多优点,特别适合扩展和抽象,下面深略3000字。
要创建驱动,无非就是创建一个驱动对象,创建一个设备对象,注册函数到设备对象,然后创建设备链接以使win32可以访问到设备。看win2K 源码(\private\ntos\io\internal.c),驱动加载的基本过程为

一堆注册表操作
取得驱动文件名
MmLoadSystemImage()加载驱动到内存
ObCreateObject()创建驱动对象
对驱动对象的各成员初始化
ObInsertObject()
ObReferenceObjectByHandle() //取得对象指针
NtClose() //关闭ObInsertObject()创建的句柄
驱动名称操作..
status = driverObject->DriverInit( driverObject, ®istryPath->Name );
//调用驱动入口DriverEntry进行驱动初始化
检查驱动对象的合法性(MajorFunction函数,驱动是否创建设备…)
IopBootLog()
MmFreeDriverInitialization()
IopReadyDeviceObjects() //VIP!!!!!

有了上面的过程,基本可以自己手动创建驱动并加载了,看了看搜索结果,发现ntoskrnl.exe导出了IoCreatDriver这个函数,其中实现了IopLoadDriver的大部分功能,事情变得简单起来。
NTKERNELAPI
NTSTATUS
IoCreateDriver (
IN PUNICODE_STRING DriverName, OPTIONAL
IN PDRIVER_INITIALIZE InitializationFunction
);
具体方法就是:用exe中的DriverEntry做为参数,调用IoCreateDriver,即可实现驱动的加载。
当在win2K下运行程序后,用WinObj居然打开失败。晕,再次看IoLoadDeiver,才发现漏掉了一个重要地方,创建驱动时,驱动对象的标志 Flags和所创建设备的标志都有限制,使用IopReadyDeviceObjects()来添加驱动DRVO_INITIALIZED标志和,去掉驱 动创建的所有设备的DO_DEVICE_INITIALIZING标志。IopReadyDeviceObjects()函数没有被 ntoskrnl.exe导出?简单,自己写就行了。曾找不到问题原因时,还重写过IopfCompleteRquest和 IopCompleteRequest,那才叫痛苦呢。
Finally,搞定。为了方便,写了一个库,下次使用时就简单多了

#include “DrvHlpApi.h”
NTSTATUS Driverentry(void *pDriverObject,void *pRegPath)
{
// ….IoCreateDevice()..
Return STATUS_SUCCESS;
}

Int main(int argc.char **argv)
{
x_InitRing0Utils();
x_StartDriver((ULONG)DriverEntry,0,0);
return 0;
}
使用这个方法,完全可以实现驱动的所有功能,同时将它与win32程序结合在一起。在XP SP2和win2k SP4下测试通过(就试过IpFilter那个)。就这么简单。有兴趣的朋友可以mail来取得代码,头文件和库可以在
http://icelord.bokee.com下载到。

参考文章:
1.《NT环境下进程的隐藏》
2.Win2K Source Code
3.PE文件格式详解
4.Ring0Demo.c v1.0 by zzzEVAzzz
5.《开发windows 2K/XP 下的防火墙》

NT内核下的inline hook,附完整的代码和工程文件

原理上很简单的东西,但我想很多新手都苦于找不到完整的示例 代码,下面就是一份完整的代码
inline hook原理大概如下:
修改被HOOK函数A的头5个字节,使其跳转到我们自定义的函数B,函数B的类型与函数A要相同。因为我们是使用JMP直接跳转到函数B,
而不是使用正常的CALL指令。在函数B内,我们可以检查函数参数,然后可以直接返回。也可以再调用函数A的一个副本。这个副本在HOOK动作发生的同时,就保存下来了。
代码非常简单,工程打包供大家学习。高手飘过


/*
* krembo.c, Demonstration of inline hooking (aka. Detouring) within driver/kernel.
* - izik@tty64.org
*/

#include

// Assembly JMP opcode value

#ifndef X86_JMP
#define X86_JMP 0xE9
#endif

// Number of bytes to overwrite

#ifndef JMP_LENGTH
#define JMP_LENGTH sizeof(X86_JMP) + sizeof(int)
#endif

// Size of a page (4k)

#ifndef PAGE_SIZE
#define PAGE_SIZE 0x1000
#endif

// Pointers macros

#define PAGE_MASK(ptr) (ptr & 0xFFFFF000)

#define OFFSET_IN_PAGE(ptr) (ptr - (ptr & 0xFFFFF000))

// RtlRandom pointer prototype

typedef unsigned long (*RTLRANDOM)(
unsigned long *seed
);

// Pointers to duplicated (original) and current versions of RtlRandom

RTLRANDOM pDupRtlRandom;
RTLRANDOM pCurRtlRandom;

/*
* DetourFunction, Detour a function (Install Detour)
* * pFcnAddr, Pointer to the soon-to-be-detoured function
* * iHookAddr, Address of the hook function
* * iPoolType, Pool type of allocated memory
*/

void *DetourFunction(char *pFcnAddr, int iHookAddr, int iPoolType) {
void *pOrigPage;

// Allocate a `PAGE_SIZE` for the original function (duplicate)

pOrigPage = ExAllocatePoolWithTag(iPoolType, PAGE_SIZE, 0xdeadbeef);//分配一页内存,该函数在WIN 2000下可能不存在,XP下没问题

// Is there enough resources?

if (pOrigPage == NULL) {
return NULL;
}

// Duplicate the entire page

RtlCopyMemory(pOrigPage, (char *)PAGE_MASK((int)pFcnAddr), PAGE_SIZE);//拷贝函数内容,把函数所在的一整页都拷贝下来。如果函数不仅仅占据一页呢?

// Calculate the relocation to `iHookAddr`

iHookAddr -= ((int)pFcnAddr + 5);//iHookAddr = iHookAddr - (pFcnAddr +5) iHookAddr是跳转的相对地址

_asm
{
CLI // disable interrupt
MOV EAX, CR0 // move CR0 register into EAX
AND EAX, NOT 10000H // disable WP bit
MOV CR0, EAX // write register back
}

// Overwrite the first `JMP_LENGTH` bytes of pFcnAddr (Detour it)

*(pFcnAddr) = X86_JMP;
*(pFcnAddr+1) = (iHookAddr & 0xFF);
*(pFcnAddr+2) = (iHookAddr >> 8) & 0xFF;
*(pFcnAddr+3) = (iHookAddr >> 16) & 0xFF;
*(pFcnAddr+4) = (iHookAddr >> 24) & 0xFF;

_asm
{
MOV EAX, CR0 // move CR0 register into EAX
OR EAX, 10000H // enable WP bit
MOV CR0, EAX // write register back
STI // enable interrupt
}

// Return pointer to the duplicate function (within the duplicate page)

return (void *)((int)pOrigPage + OFFSET_IN_PAGE((int)pFcnAddr));//返回我们保存的函数副本地址(原件)
}

/*
* MyRtlRandom, RtlRandom hook
* * seed, given seed
*/

unsigned long MyRtlRandom(unsigned long *seed) {//HOOK发生时,原始函数跳转到该函数位置,★该函数的类型与原始函数相同
unsigned long retval;

DbgPrint(("MyRtlRandom invoked "));

retval = pDupRtlRandom(seed); //retval 是被HOOK函数的原件的地址,见DriverEntry(),在该函数内部又调用原始函数的原件(副本)

//DbgPrint(("Returning value = %d, from OldRtlRandom(%d)\n"));

return retval;
}

/*
* RestoreDetouredFunction, Restore a detoured function (Remove detour)
* * pDupFcn, Pointer to the duplicated function
* * pOrigFcn, Pointer to the original function
*/

void RestoreDetouredFunction(char *pOrigFcn, char *pDupFcn) { //移除HOOK
int offset;

_asm
{
CLI // disable interrupt
MOV EAX, CR0 // move CR0 register into EAX
AND EAX, NOT 10000H // disable WP bit
MOV CR0, EAX // write register back
}

// Uninstall the detour, Restore `JMP_LENGHT` bytes from the duplicate function.

for (offset = 0; offset < JMP_LENGTH; offset++) {
pOrigFcn[offset] = pDupFcn[offset];
}

_asm
{
MOV EAX, CR0 // move CR0 register into EAX
OR EAX, 10000H // enable WP bit
MOV CR0, EAX // write register back
STI // enable interrupt
}

// Deallocate the duplicate page

ExFreePoolWithTag((void *)((int)pDupFcn - OFFSET_IN_PAGE((int)pOrigFcn)), 0xdeadbeef);

return ;
}

/*
* DriverUnload, Driver unload point
* * DriverObject, self (Driver)
*/

void DriverUnload(IN PDRIVER_OBJECT DriverObject) {
unsigned long seed;

if (pDupRtlRandom != NULL) { //pDupRtlRandom为我们保存的原始函数原件

DbgPrint(("Removeing detour from RtlRandom\n"));

// Remove detour

RestoreDetouredFunction((char *)pCurRtlRandom, (char *)pDupRtlRandom);

// Do another self-test

seed = 31337;

pCurRtlRandom(&seed);

// No bugcheck? ;-)
}

//DbgPrint(("Krembo unloaded!\n"));

return ;
}

/*
* DriverEntry, Driver Single Entry Point
* * DriverObject, self (Driver)
* * RegistryPath, given RegistryPath
*/

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) {
UNICODE_STRING pFcnName;
unsigned long seed;

//DbgPrint(("Krembo loaded!\n"));

// Register unload routine

DriverObject->DriverUnload = DriverUnload;

// Lookup RtlRandom address

RtlInitUnicodeString(&pFcnName, L"RtlRandom");

pCurRtlRandom = MmGetSystemRoutineAddress(&pFcnName);

//DbgPrint(("Found RtlRandom @ 0x%08x\n", pCurRtlRandom));

// Detour RtlRandom

pDupRtlRandom = DetourFunction((char *)pCurRtlRandom, (int)MyRtlRandom, (int)NonPagedPool);

// Detour status?

if (pDupRtlRandom == NULL) {

// Unable to detour

DbgPrint(("Unable to detour RtlRandom! (Not enough resources?)\n"));

} else {

//DbgPrint(("Duplicate Function @ 0x%08x\n", pDupRtlRandom));

// Detour installed, do a self-test

DbgPrint(("Detour installed, going into a self-test ...\n"));
////DbgPrint(("Detour installed, going into a self-test ...\n"));

// Dummy seed for the self-test

seed = 31337;

pCurRtlRandom(&seed);

}

return STATUS_SUCCESS;
}

晕,不知道怎么添加附件。firefox 功能就是不如IE。不过代码都在这里了,COPY下然后编译就OK了

伪造返回地址绕过CallStack检测

伪造返回地址绕过CallStack检测以及检测伪造返回地址的实践笔记

Author:[CISRG]KiSSinGGer
E-mail:kissingger@gmail.com
MSN:kyller_clemens@hotmail.com

题目有点搞......Anti-CallStack Check and Anti-Anti-CallStack Check...(;- -)

发现最近MJ0011的“基于CallStack的Anti-Rootkit HOOK检测思路”和gyzy的“基于栈指纹检测缓冲区溢出的一点思路”两篇文章有异曲同工之妙。
两者都通过检测CallStack中的返回地址来做文章。
最近在初步学习一些AntiRootkit技术,这两个不得不吸引我的眼球。

按照MJ0011大侠的逻辑,从Rootkit Detector的Hook点向上检测CallStack.
但是CallStack里面都是些DWORDs,怎么判断哪儿是参数,哪儿是返回地址呢?
我Goo了两把...普遍是用EBP回溯的方式.
考虑大部分的__stdcall的形式:
mov edi edi
push ebp
mov ebp esp
...
...
我们从dword ptr [EBP]里面可以获得上个call的EBP,dword ptr [EBP+4]里面获得需要检测的返回地址,然后EBP = dword ptr [EBP],继续找下去.找到栈基址为止.
每次得到的返回地址,判断一下它是否在一个合法的模块中.

但是,根据gyzy大侠的<编写绕过卡巴主动防御的Shellcode>一文启示,我们可以知道如下一种方式,可使这样的检测方式失效.

1.在合法的系统模块里(e.g. ntoskrnl.exe),找到一个'C3'(ret Opcode)字节,它的指针是K.
2.使用如下方式的Hook函数

HookedZwXxx(...)
{
//
// 一些参数处理操作
//

jmp __pushrealretaddr
__trickstage:

push Arg[N] ;
push Arg[N-1]
...
push Arg[0]

push K //从一开始的push Arg[N]指令 到这里,我们实际上是自己模拟实现 call 指令的动作
jmp ZwXxx; // 自己模拟实现 call ZwXxx() ;返回地址 K 在合法模块 ntoskrnl.exe 中
//这种模拟技术还可以用于 bypass inline HOOK
__pushrealretaddr:
call __trickstage

realretaddr:

//
// 另一些结果处理操作
//
}

这样,在ZwXxx深处检查调用栈,dword ptr [EBP+4]是一个处于合法模块中的地址K.

//----------------------------------------------------------------------------------------
我写了一个如下的ring3示例程序.

定义如下一些函数:
int __stdcall Call_C(int a, int b)
{
check_callstack();
return a+b;
}

int __stdcall Call_B(int a, int b)
{
return Call_C(a,b);
}

int __stdcall Call_A(int a, int b)
{
return Call_B(a,b);
}

调用次序是A->B->C,其中C里面执行check_callstack()来检测是否有非法的返回地址.

void
__stdcall
check_callstack( void )
{
int saved_ebp;
int retaddr;

printf("Check Call Stack Methord 1:\n");
__asm
{
mov eax, dword ptr [ebp+4]
mov retaddr, eax //得到并保存函数的返回地址,不明白的话请看附图:_stdcall函数调用堆栈
mov eax, dword ptr [ebp]
mov saved_ebp, eax //保存原始的EBP
}
printf("retaddr = 0x%08X\n",retaddr);

while(saved_ebp <> 0) //检查EBP的有效性
{
if(saved_ebp != 0)
{
retaddr = *(int*)(saved_ebp+4); //ebp +4 为函数的返回地址 ,请看附图:_stdcall函数调用堆栈
printf("retaddr = 0x%08X\n",retaddr);
saved_ebp = *(int*)saved_ebp; //(回溯操作)修改saved_ebp为函数调用前的堆栈头,换句话讲,也就是调用该当前函数的“父函数”,它的堆栈头。
//你必须对线程内函数调用时的堆栈操作有清楚的认识
}
}
}

在没有Hook的情况下,我们执行Call_A(1,2),得到正常返回为3.

check_callstack输出:
retaddr = 0x00401008
retaddr = 0x00401030
retaddr = 0x00401050
retaddr = 0x00401126
retaddr = 0x0040149D
retaddr = 0x7C816FD7

我们现在使用一个函数Hooked_Call_B来在Call_A中把Call_B给Hook掉.
Hook掉的Call_B做的只是把的返回值改成4.

__declspec( naked )//这个函数定义前缀,告诉编译器对该函数代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
int Hooked_Call_B(int a, int b)
{
__asm
{
push ebp
mov ebp, esp
jmp __a

__trickstage:

mov eax, b
push eax
mov eax, a
push eax
//为了方便这里使用一个OD得到的硬编码:P
push 0x004011AD //这个地址指向一个'C3'
jmp Call_B

__a:
call __trickstage //★注意,0x004011AD 处的 "C3",即ret 指令,是对应这个CALL的
mov eax, 4 //这里,改返回值,使得1+2的结果为4.
pop ebp
ret 8
}
}

用来改写Call_A的函数,这个函数在2003编译出来的EXE中会导致异常
因为.text段没有写权限.实际测试中我用StudPE改了段属性.在内核态
的话...这个修改代码段段属性问题...应该很简单把...

int __stdcall SetHook( int Hook_Call )
{
int Original_Call = 0;
int hook_pos = (int)Call_A;

//
// 以下丑陋代码是在Call_A中找到"call Call_B"指令的位置
//
__asm
{
__again:
mov eax,hook_pos
xor ecx,ecx
mov cl,byte ptr ds:[eax]
cmp cl,0xE8
je __finish
mov edx,hook_pos
add edx,1
mov hook_pos,edx
jmp __again
}
__finish: //将Call_A()中"call Call_B"指令的位置保存在 hook_pos 中

//
// 用Hook_Call patch掉call后面的地址,这段代码真的很丑陋 -_-
//

Hook_Call = Hook_Call - hook_pos - 5;
__asm
{
mov eax, Hook_Call
mov edi, hook_pos
mov dword ptr [edi+1], eax
}
return hook_pos;
}

我们之后将调用SetHook( Hooked_Call_B )将Call_A中的"call Call_B"改掉.

我们的Hooked_Call_B,在调试器中看到是[0x004010B0,0x004010D2]这段地址.
那么,如果我们根据EBP回溯CallStack的方法有效,在Hooked_Call_B生效以后应该成功的找到一个retaddr属于[0x004010B0,0x004010D2]区间.

遗憾的是,没有...

check_callstack输出:
retaddr = 0x00401008
retaddr = 0x00401030
retaddr = 0x004011AD <--注意这里 retaddr = 0x00401050 retaddr = 0x0040114D retaddr = 0x0040149D retaddr = 0x7C816FD7 我们可以看到,我们正常的返回地址被一个貌似合法的0x004011AD给偷梁换柱了. 于是,我们在这里断定...根据EBP的回溯,被这种方式(叫做Detour Ret? :P)给愚弄了. 另想辙. 我们来OD里面看看实际的堆栈,这是停在Call_C里面的时候. 0012FEA0 0012FEB4 <--当前EBP 0012FEA4 004011AD <--伪造的返回地址,指向C3 0012FEA8 00000001 <- 0012FEAC 00000002 <-两个参数 0012FEB0 004010CC <--真正的返回地址! 0012FEB4 /0012FEC4 0012FEB8 |00401050 0012FEBC |00000001 0012FEC0 |00000002 当Call_C退出时,执行: pop ebp ret 8 此后寄存器状态: ebp = 0012FEB4 esp = 0012FEB0 eip = 004011AD 这时就执行到004011AD了,004011AD处的ret将使得eip = dword ptr [esp],这样就顺利的返回到004010CC了. 呃?这么看来,004010CC这个恶意的返回地址确确实实是存在于CallStack中的.关键就是怎么确定它的. EBP回溯不行,也许ESP回溯...这个具体方式我这个愚人就不知了.MJ0011就是说使用ESP回溯的.这样得考虑经过的每个call的参数个数问题. 这样我就有了一个思路: 对每一个返回地址判断一下,是否指向一个'C3'. 若是,则retaddr = 第一个参数位置 + 参数个数*4 若否,则retaddr = dword ptr [EBP + 4] 改一下check_callstack: void __stdcall check_callstack( void ) { int saved_ebp; int retaddr; //[参数个数]x4,对于内核例程,参数一般是固定的. int stack_fix = 0x8; printf("Check Call Stack Methord 2:\n"); __asm { mov eax, dword ptr [ebp+4] mov retaddr, eax mov eax, dword ptr [ebp] mov saved_ebp, eax } printf("retaddr = 0x%08X\n",retaddr); while(saved_ebp <> 0)
{
if(saved_ebp != 0)
{
retaddr = *(int*)(saved_ebp+4);
printf("retaddr = 0x%08X\n",retaddr);

if(retaddr != 0)
{
if(*(unsigned char*)retaddr == 0xC3)
{
//
// 若返回指令指向一个'C3',我们得检查在参数push之后的返回地址
// Sorry for my 丑陋的表达式 :(

retaddr = *(int*)(saved_ebp+8+stack_fix);
printf("Suspicious retaddr found : 0x%08x\n",retaddr);
}
}
saved_ebp = *(int*)saved_ebp;
}
}
}

我们来运行程序来验证一下:

没Hook的情况:

retaddr = 0x0040100D
retaddr = 0x00401030
retaddr = 0x00401050
retaddr = 0x00401126
retaddr = 0x0040149D
retaddr = 0x7C816FD7

有Hook的情况:

retaddr = 0x0040100D
retaddr = 0x00401030
retaddr = 0x004011AD
Suspecious retaddr found : 0x004010cc
retaddr = 0x00401050
retaddr = 0x0040114D
retaddr = 0x0040149D
retaddr = 0x7C816FD7

比较顺利的找到属于[0x004010B0,0x004010D2]的0x004010cc

那么我们是否可用就此断定,这种堆栈回溯检测有效了?
还不可妄下结论...

如果,伪造的返回地址指向一个"C2 XXXX"? //但是记着,我们自己的HOOK函数内部,不能使用EBP寄存器哦。作者正是使用EBP回溯来检查函数返回地址
比如,我们在Hooked_Call_B里面这么写:
push xxx //这里随便push两个,与ret 8配合平衡堆栈
push xxx
mov eax, b
push eax
mov eax, a
push eax

push K //这个地址指向一个'C2 08 00'(ret 8)
jmp Call_B

那么,我们还得检测返回地址为C2的情况,并取得C2后面的一个WORD,通过这个WORD判断真正的返回地址在Arg[N]栈位置后面的第3个DWORD处.

更进一步,如果,伪造的返回地址K指向一个如下的指令序列:
pop eax
pop ebx
pop ebp
ret 8

我们还得对这个返回地址做一些语义(pop+ret)上的分析,才能确定真正的返回地址...它在Arg[N]栈位置后面的第6个DWORD的处...

还有
如果返回地址里还有对esp的add,sub..这些东西,呵呵,需要做检测工作的就多了去了.

虽然我在实践中实现了一个比较简单的'C3'检测,但我还是觉得这个Callstack回溯,并不是想象中好搞.

我不想和自己下棋了,没完没了......这篇陋文权当抛砖引玉了.
搞来搞去...我发现各Rootkit Coders以及ARK Coders都进入了一种Code Tricks的较量.
想象各种伎俩的RK/ARK代码在内核中堆积...他进我退他退我追他疲我生...
祸邪?福邪?


最后
感谢有人看完冗长的文章以及丑陋的代码
向以下达人及其共享的文档及其共享的精神致敬:

gyzy <编写绕过卡巴主动防御的Shellcode>
gyzy <基于栈指纹检测缓冲区溢出的一点思路>
MJ0011 <基于CallStack的Anti-Rootkit HOOK检测思路>
l0pht 点评"基于栈指纹检测缓冲区溢出的一点思路
Matt Conover (Show me his trick "without put anything extra on the callstack" :0)

附图:
http://i297.photobucket.com/albums/mm234/songjia8876/STDCALL.jpgscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此图已经缩小,点击察看原图。';}" border="0">