|
计算机与网络安全 今天
一次性付费进群,长期免费索取资料。
回复公众号:微信群 可查看进群流程。
广告
Windows黑客编程技术详解作者:甘迪文
当当
微信公众号:计算机与网络安全
ID:Computer-network
为了隐藏木马进程,把木马的全部功能实现在DLL文件中,然后将DLL文件注入其他进程中,从而达到隐藏木马进程的目的。现在要做的是隐藏进程中的DLL文件,当把DLL文件注入远程进程后,可以将DLL也隐藏掉。操作系统在进程中维护着一个叫作TEB的结构体,这个结构体是线程环境块。本文通过WinDBG调试工具来一步一步地介绍TEB,并通过TEB介绍如何隐藏DLL文件。
1. 启动WinDBG
启动WinDBG工具,如图1所示。
图1 WinDBG启动界面
依次单击菜单栏的“File”→“Symbol File Path”命令,输入符号文件路径。这里直接填入微软提供的符号服务器:“srv*F:\Program Files\symbolcache*http://msdl. microsoft.com /download/symbols”,如图2所示。
图2 设置符号文件路径
设置好符号路径,就可以开始调试了。这里调试的目标就是WinDBG,因为原理是相同的。进行本地调试,依次单击菜单“File”→“Kernel Debug”命令,出现如图3所示的窗口。
图3 Kernel Debugging窗口界面
选择“Local”选项卡,也就是进行本地调试,单击“确定”按钮。这样,就可以用WinDBG开始调试,跟着步骤一步一步做就可以了。
2. 分析步骤
首先获取TEB,也就是线程环境块。在编程的时候,TEB始终保存在寄存器FS中。获取TEB的命令为“!teb”。在WinDBG的命令提示处输入该命令,WinDBG将输出如下内容:
lkd> !tebTEB at 7ffde000 ExceptionList: 00c0e060 StackBase: 00c10000 StackLimit: 00bfb000 SubSystemTib: 00000000 FiberData: 00001e00 ArbitraryUserPointer: 00000000 Self: 7ffde000 EnvironmentPointer: 00000000 ClientId: 00000554 . 00000320 RpcHandle: 00000000 Tls Storage: 00000000 PEB Address: 7ffd5000 LastErrorValue: 0 LastStatusValue: c0000139 Count Owned Locks: 0 HardErrorMode: 0
从上面的输出内容可以看出,TEB地址为7ffde000。
获得TEB以后,通过TEB的地址来解析TEB的数据结构,从而获得PEB,也就是进程环境块,命令为“dt _teb 7ffde000”,WinDBG的输出内容如下:
lkd> dt _teb 7ffde000nt!_TEB +0x000 NtTib : _NT_TIB +0x01c EnvironmentPointer : (null) +0x020 ClientId : _CLIENT_ID +0x028 ActiveRpcHandle : (null) +0x02c ThreadLocalStoragePointer : (null) +0x030 ProcessEnvironmentBlock : 0x7ffd5000 _PEB +0x034 LastErrorValue : 0 +0x038 CountOfOwnedCriticalSections : 0 +0x03c CsrClientThread : (null) +0x040 Win32ThreadInfo : 0xe2bfb130 +0x044 User32Reserved : [26] 0 +0x0ac UserReserved : [5] 0 +0x0c0 WOW32Reserved : (null) +0x0c4 CurrentLocale : 0x804 +0x0c8 FpSoftwareStatusRegister : 0 +0x0cc SystemReserved1 : [54] (null)
上面只是部分输出,该结构体非常长,这里只查看其中的一部分内容,只要找到PEB在TEB中的偏移就可以了。从该命令的输出可以看出,PEB结构体的地址位于TEB结构体偏移0x30的位置,该位置保存的地址是7ffd5000。也就是说,PEB的地址是7ffd5000,通过该地址来解析PEB,并获得LDR。在命令提示符处输入命令“dt nt!_peb 7ffd5000”,输出如下内容:
lkd> dt nt!_peb 7ffd5000 +0x000 InheritedAddressSpace : 0 '' +0x001 ReadImageFileExecOptions : 0 '' +0x002 BeingDebugged : 0 '' +0x003 SpareBool : 0 '' +0x004 Mutant : 0xffffffff +0x008 ImageBaseAddress : 0x01000000 +0x00c Ldr : 0x001a1e90 _PEB_LDR_DATA +0x010 ProcessParameters : 0x00020000 _RTL_USER_PROCESS_PARAMETERS +0x014 SubSystemData : (null) +0x018 ProcessHeap : 0x000a0000 +0x01c FastPebLock : 0x7c9a0600 _RTL_CRITICAL_SECTION +0x020 FastPebLockRoutine : 0x7c921000 +0x024 FastPebUnlockRoutine : 0x7c9210e0 +0x028 EnvironmentUpdateCount : 1 +0x02c KernelCallbackTable : 0x77d12970
从输出结果可以看出,LDR在PEB结构体偏移的0x0C处,该地址保存的地址是001a1e90。通过该地址来解析LDR结构体。在命令提示符处输入命令“dt _peb_ldr_data 001a1e90”,WinDBG输出如下内容:
lkd> dt _peb_ldr_data 001a1e90nt!_PEB_LDR_DATA +0x000 Length : 0x28 +0x004 Initialized : 0x1 '' +0x008 SsHandle : (null) +0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x1a1ec0 - 0x1a3218 ] +0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x1a1ec8 - 0x1a3220 ] +0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x1a1f28 - 0x1a3228 ] +0x024 EntryInProgress : (null)
在这个结构体中,可以看到3个相同的数据结构,也就是在偏移0x0c、0x14、0x24处的3个结构体_LIST_ENTRY。该结构体是个链表,定义如下:
typedef struct _LIST_ENTRY { struct _LIST_ENTRY *Flink; struct _LIST_ENTRY *Blink;} LIST_ENTRY, *PLIST_ENTRY;
上面这个结构体在SDK提供的帮助中是找不到的,需要去WDK的帮助中才可以找到。这3条链表分别保存的是_LDR_DATA_TABLE_ENTRY,也就是LDR_DATA表的入口。
现在来手动遍历第一条链表,输入命令“dd 1a1ec0”:
lkd> dd 1a1ec0 // _list_entry 的地址001a1ec0 001a1f18 001a1e9c 001a1f20 001a1ea4001a1ed0 00000000 00000000 01000000 010582f7001a1ee0 00096000 00580056 00020c64 00160014001a1ef0 00020ca6 00005000 0000ffff 001a2cd4001a1f00 7c99e310 49a5f6a7 00000000 00000000001a1f10 000b000b 000801b7 001a1fc0 001a1ec0001a1f20 001a1fc8 001a1ec8 001a1fd0 001a1eac001a1f30 7c920000 7c932c60 00096000 0208003a
在这么多的输出中,在链表偏移0x18的位置是模块的映射地址,即ImageBase;在链表偏移0x28的位置是模块的路径及名称的地址;在链表偏移0x30的位置是模块名称的地址。1a1ec0偏移0x28的位置中保存的地址是20c64,接下来输入命令“du 20c64”:
lkd> du 20c6400020c64 "F:\WinDDK\7600.16385.0\Debuggers"00020ca4 "\windbg.exe"
可以看到,输出WinDBG的全部路径。再来看一下偏移0x18的地址,该进程的映射基址为01000000。偏移0x30处的地址保存着20ca6,查看该地址,输入命令“du 20ca6”:
lkd> du 20ca600020ca6 "windbg.exe"
的确是模块的名称。既然是链表,就来下一条链表的信息:
lkd> dd 1a1f18 // 1a1f18 中保存的是下一个_list_entry 的地址001a1f18 001a1fc0 001a1ec0 001a1fc8 001a1ec8001a1f28 001a1fd0 001a1eac 7c920000 7c932c60001a1f38 00096000 0208003a 7c9a0028 00140012001a1f48 7c942838 80084004 0000ffff 7c99e2c8001a1f58 7c99e2c8 498ffe8a 00000000 00000000001a1f68 000b000a 000e01b8 003a0043 0057005c001a1f78 004e0049 004f0044 00530057 0073005c001a1f88 00730079 00650074 0033006d 005c0032lkd> dd 1a1fc0 // _list_entry 的地址001a1fc0 001a2068 001a1f18 001a2070 001a1f20001a1fd0 001a21b8 001a1f28 7c800000 7c80b5be001a1fe0 0011d000 00420040 001a1f70 001a0018001a1ff0 001a1f98 80084004 0000ffff 001a2a44001a2000 7c99e2b0 49c4f753 00000000 00000000001a2010 000b000a 000e0157 003a0043 0057005c001a2020 004e0049 004f0044 00530057 0073005c001a2030 00730079 00650074 0033006d 005c0032
按照上面介绍的解析方法,自己进行解析。
lkd> du 1a1f70001a1f70 "C:\WINDOWS\system32\kernel32.dll"001a1fb0 ""
上面介绍的几个结构体在VC6的头文件中是找不到的,不过在网上还是可以查到的。涉及的几个结构体的定义如下:
typedef struct _PEB_LDR_DATA {
BYTE Reserved1[8]; PVOID Reserved2[3];
LIST_ENTRY InMemoryOrderModuleList;}
PEB_LDR_DATA, *PPEB_LDR_DATA;
typedef struct _LDR_DATA_TABLE_ENTRY { PVOID Reserved1[2];
LIST_ENTRY InMemoryOrderLinks;
PVOID Reserved2[2];
PVOID DllBase;
PVOID EntryPoint;
PVOID Reserved3;
UNICODE_STRING FullDllName;
BYTE Reserved4[8];
PVOID Reserved5[3];
union { ULONG CheckSum; PVOID Reserved6; };
ULONG TimeDateStamp;}
LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
从这两个结构体中可以看出有非常多的保留字段,这些都是微软不愿意公开的,或不愿意让用户使用的。不过网上有大量的相关结构体的具体定义,可以自行查找进行阅读。
看完上面的各种结构体,是不是觉得自己都可以实现枚举进程中模块的函数了?下面来写一个。
3. 编写枚举进程中模块的函数
枚举进程中的模块的方法就是通过上面介绍的几个结构体来完成的,其步骤如下:
获得TEB地址→获得PEB地址→得到LDR→获得第二条链表的地址→遍历该链表并输出偏移0x18的值和0x28指向的内容。
只要把上面在WinDBG中找到链表的方法弄明白,就没有太大的问题了。关键的问题是怎么找到TEB。TEB保存在FS中,有了这个提示就很好解决了,代码如下:
void EnumModule(){
DWORD *PEB = NULL, *Ldr = NULL, *Flink = NULL, *p = NULL, *BaseAddress = NULL, *FullDllName = NULL; // 定位
PEB __asm { // fs 位置保存着 TEB // fs:[0x30]位置保存着 PEB mov eax,fs:
[0x30] mov PEB,eax } // 得到 LDR
Ldr = *( ( DWORD ** )( ( unsigned char * )PEB + 0x0c ) );
// 第二条链表 Flink = *( ( DWORD ** )( ( unsigned char * )Ldr + 0x14 ) );
p = Flink;
p = *( ( DWORD ** )p ); while ( Flink != p )
{ BaseAddress = *( ( DWORD ** )( ( unsigned char * )p + 0x10 ) );
FullDllName = *( ( DWORD ** )( ( unsigned char * )p + 0x20 ) );
if ( BaseAddress == 0 )
{ break; } printf("ImageBase = %08x \r\n ModuleFullName = %S \r\n", BaseAddress, (unsigned char *)FullDllName);
p = *( ( DWORD ** )p ); }}
该函数的实现没有太多的技巧,主要在于掌握C语言中指针,还有就是能够掌握以上介绍的几个结构体之间的关系,也就是各结构体之间的数据关系。在main()函数中调用这个函数,输出结果如图4所示。
图4 自实现的枚举模块函数
4. 隐藏指定DLL模块
DLL模块的隐藏是把指定DLL模块在链表中的节点断掉,也就是做一个数据结构中链表的删除动作,只不过不进行删除,只是将其节点脱链即可,如图5所示。
图5 链表节点脱链
如果是枚举模块的话,一般情况下,只要枚举第二条链表就可以了,也就是偏移0x14处的那条。如果要做模块隐藏的话,最好是将3条链表中的指定模块全部脱链。对于脱链的方法,其实也是对3条链表进行遍历,然后将指定的模块脱链就可以了。和上面枚举的方法差别不大,下面给出代码。
void HideModule(char *szModule)
{ DWORD *PEB = NULL, *Ldr = NULL, *Flink = NULL, *p = NULL, *BaseAddress = NULL, *FullDllName = NULL;
__asm { mov eax,fs:[0x30] mov PEB,eax }
HMODULE hMod = GetModuleHandle(szModule);
Ldr = *( ( DWORD ** )( ( unsigned char * )PEB + 0x0c ) );
Flink = *( ( DWORD ** )( ( unsigned char * )Ldr + 0x0c ) );
p = Flink; do { BaseAddress = *( ( DWORD ** )( ( unsigned char * )p + 0x18 ) );
FullDllName = * ( ( DWORD ** )( ( unsigned char * )p + 0x28 ) );
if (BaseAddress == (DWORD *)hMod)
{ **( ( DWORD ** )(p + 1) ) = (DWORD) *( ( DWORD ** )p );
* (*( ( DWORD ** )p ) + 1) = (DWORD) *( ( DWORD ** )(p + 1) );
break; }
p = *( ( DWORD ** )p );
}
while ( Flink != p );
Flink = *( ( DWORD ** )( ( unsigned char * )Ldr + 0x14 ) );
p = Flink;
do { BaseAddress = *( ( DWORD ** )( ( unsigned char * )p + 0x10 ) );
FullDllName = *( ( DWORD ** )( ( unsigned char * )p + 0x20 ) );
if (BaseAddress == (DWORD *)hMod) { **( ( DWORD ** )(p + 1) ) = (DWORD) *( ( DWORD ** )p );
*(* ( ( DWORD ** )p ) + 1) = (DWORD) *( ( DWORD ** )(p + 1) );
break; }
p = *( ( DWORD ** )p ); }
while ( Flink != p );
Flink = * ( ( DWORD * )( ( unsigned char * )Ldr + 0x1c ) );
p = Flink;
do { BaseAddress = * ( ( DWORD ** )( ( unsigned char * )p + 0x8 ) );
FullDllName = * ( ( DWORD ** )( ( unsigned char * )p + 0x18 ) );
if (BaseAddress == (DWORD *)hMod) { **( ( DWORD ** )(p + 1) ) = (DWORD) *( ( DWORD ** )p );
*(* ( ( DWORD ** )p ) + 1) = (DWORD) *( ( DWORD ** )(p + 1) );
break; }
p = *( ( DWORD ** )p );
}
while ( Flink != p );}
在main()函数中调用这个函数,主函数如下:
int main(int argc, char* argv[])
{ HideModule("kernel32.dll");
getchar();
return 0;}
接下来隐藏调用“kernel32.dll”模块。当然,这里的隐藏只能是这个程序运行时隐藏该进程中的“kernel32.dll”模块,对其余进程中的模块并没有影响。在程序的末尾使用getchar(),其用意是希望该进程可以停留住,否则它如果退出,便没有验证“kernel32.dll”模块是否真的被隐藏的机会了。编译连接并运行,然后用工具查看,如图6所示。
图6 查看HideModule中Kernel32.dll模块被隐藏
从图6中可以看出,在HideModule.exe进程中看不到Kernel32.dll的模块名。当然,就算用自己编写的枚举的模块函数也是没用的,因为模块已经不在链表中了。虽然这个程序把进程中“kernel32.dll”隐藏了,但是并没有多大的实际意义。隐藏模块主要是用在被注入的DLL中,也就是一个DLL文件被注入远程线程中后,再调用该函数来隐藏被注入的DLL,为了不被发现而隐藏。
|
|