工具】 OllyDbg , LordPE , WinHex,RadASM
【平台】 WinXP SP2
【名称】 快速关机.exe
【名称】个人练手,高手勿进
某软件中有一“快速关机”功能,其关机速度之快,几乎与按电源开关一样,拿来分析一下:
00401630 >push ecx ;保存数值
00401631 >lea eax, dword ptr [esp]
00401635 >push esi ;保存数值
00401636 >push eax ;参数四,指向下一个 SEH 记录的指针
00401637 >push 0 ;参数三
00401639 >push 1 ;参数二
0040163B >push 13 ;参数一,这四个参数最终传递给了ntdll.RtlAdjustPrivilege
0040163D >mov dword ptr [esp+14], 0
00401645 >call 0040131C ;见下,实际是要执行ntdll.RtlAdjustPrivilege函数,修改权限
0040164A >mov esi, dword ptr [<&MSVBVM60.__vbaSetSystemError>] ; MSVBVM60.__vbaSetSystemError
00401650 >call esi ; <&MSVBVM60.__vbaSetSystemError>
00401652 >push 0 ;ntdll.NtShutdownSystem的参数,0为快速关机,1为快速重起
00401654 >call 00401368 ;见下,实际是要执行ntdll.NtShutdownSystem函数,关机
00401659 >call esi ; <&MSVBVM60.__vbaSetSystemError>
0040165B >pop esi
0040165C >pop ecx
0040165D >retn
0040131C >mov eax, dword ptr [4022D4]
00401321 >or eax, eax
00401323 >je short 00401327
00401325 >jmp eax
00401327 >push 00401304
0040132C >mov eax, <jmp.&MSVBVM60.DllFunctionCall>
00401331 >call eax ;取得ntdll.RtlAdjustPrivilege的地址
00401333 >jmp eax ;eax=7C949E8C (执行ntdll.RtlAdjustPrivilege)
00401368 >mov eax, dword ptr [4022E0]
0040136D >or eax, eax
0040136F >je short 00401373
00401371 >jmp eax
00401373 >push 00401350
00401378 >mov eax, <jmp.&MSVBVM60.DllFunctionCall>
0040137D >call eax ;取得ntdll.NtShutdownSystem的地址
0040137F >jmp eax ;eax=7C92E7E6 (执行ntdll.NtShutdownSystem)进去就关机了
如果跟进ntdll.RtlAdjustPrivilege,发现它最后有个 ret 10,此函数有四个DWORD类型的参数(0x10==16==4×4);ntdll.NtShutdownSystem有一个DWORD类型的参数
由上可知,它就用了两个函数(在ntdll.dll中):RtlAdjustPrivilege、NtShutdownSystem。整个关机功能如果用 VC++6.0来写就是这样:
//
int XX;
HINSTANCE hdll=LoadLibrary("ntdll.dll");//没有ntdll.h和ntdll.lib只好这么做了
typedef void(*AdjustPrivilege)(int,int,int,void*);//由 ret 10 知此函数有四个参数
AdjustPrivilege RtlAdjustPrivilege=(AdjustPrivilege)GetProcAddress(hdll,"RtlAdjustPrivilege");
RtlAdjustPrivilege(0x13,1,0,&XX);//照抄来的数据,不知其意
typedef void(*ShutdownSystem)(int);//由 ret 4 知此函数有一个参数
ShutdownSystem NtShutdownSystem=(ShutdownSystem)GetProcAddress(hdll,"NtShutdownSystem");
NtShutdownSystem(0);//0为快速关机,1为快速重起
/
用asm编写则如下:
;-------------------------------------------------------------------------------------
.586
.model flat,stdcall
option casemap:none
include kernel32.inc
includelib kernel32.lib
;-------------------------------------------------------------------------------------
.code
start:
;反正执行到本程序最后一句就关机了,所以就不用保存 esi edi 了
mov esi,@F ;没有使用 .data 段,就这样来取字串"ntdll.dll"
invoke LoadLibrary,esi
add esi,0Ah ;字串"NtShutdownSystem"
push esi
push eax
add esi,11h ;字串"RtlAdjustPrivilege"
push esi
push eax
mov edi,GetProcAddress
call edi
push esp
push 0h
push 1h
push 13h
call eax ;RtlAdjustPrivilege
call edi ;GetProcAddress
push 0h ;0为快速关机,1为快速重起
call eax ;进去就关机了,所以后面就不用 ret 了,也不用FreeLibrary了
@@:
db "ntdll.dll",0
db "NtShutdownSystem",0
db "RtlAdjustPrivilege",0
comment *----------------------------------------------------------------------------
如果有ntdll.inc和ntdll.lib,这样就可以了:
invoke RtlAdjustPrivilege,13h,1h,0h,esp
invoke NtShutdownSystem,0
*------------------------------------------------------------------------------------
end start
;------------------------------------------------------------------------------------
闲来无事,为了熟悉PE文件格式,把上面的asm编译后,又手工修改了一通,做成了一个很小的快速关机程序。先回忆一下导入表:
导入表结构组成:
IMAGE_IMPORT_DESCRIPTOR STRUCT
union
Characteristics dd ?
OriginalFirstThunk dd ?
ends
TimeDateStamp dd ?
ForwarderChain dd ?
Name1 dd ?
FirstThunk dd ?
IMAGE_IMPORT_DESCRIPTOR ENDS
结构第一项是一个union子结构。事实上,这个union子结构只是给 OriginalFirstThunk 增添了个别名,您也可以称其为"Characteristics"。 该成员项含有指向一个 IMAGE_THUNK_DATA 结构数组的RVA。
IMAGE_THUNK_DATA是一个dword类型的集合,指向一个 IMAGE_IMPORT_BY_NAME 结构的指针。注意 IMAGE_THUNK_DATA 包含了指向一个 IMAGE_IMPORT_BY_NAME 结构的指针: 而不是结构本身。
有几个 IMAGE_IMPORT_BY_NAME 结构,我们收集起这些结构的RVA (IMAGE_THUNK_DATAs)组成一个数组,并以0结尾,然后再将数组的RVA放入 OriginalFirstThunk。
此 IMAGE_IMPORT_BY_NAME 结构存有一个引入函数的相关信息。再来研究 IMAGE_IMPORT_BY_NAME 结构到底是什么样子的呢:
IMAGE_IMPORT_BY_NAME STRUCT
Hint dw ?
Name1 db ?
IMAGE_IMPORT_BY_NAME ENDS
Hint 指示本函数在其所驻留DLL的引出表中的索引号。该域被PE装载器用来在DLL的引出表里快速查询函数。该值不是必须的,一些连接器将此值设为0。
Name1 含有引入函数的函数名。函数名是一个ASCIIZ字符串。注意这里虽然将Name1的大小定义成字节,其实它是可变尺寸域,只不过我们没有更好方法来表示结构中的可变尺寸域。The structure is provided so that you can refer to the data structure with descriptive names.
TimeDateStamp 和 ForwarderChain 一般不用,可全填00.
Name1 含有指向DLL名字的RVA,即指向DLL名字的指针,也是一个ASCIIZ字符串。
FirstThunk 与 OriginalFirstThunk 非常相似,它也包含指向一个 IMAGE_THUNK_DATA 结构数组的RVA(当然这是另外一个IMAGE_THUNK_DATA 结构数组)。
好了现在动手,第一种改法:
一、用OD载入“快速关机.exe”,做如下修改:
00401000 push esp
00401001 push 0
00401003 push 1
00401005 push 13
00401007 call dword ptr [401031]; ntdll.RtlAdjustPrivilege
0040100D push 0
0040100F call dword ptr [40102D]; ntdll.ZwShutdownSystem
00401015 ; 之后为结构 'IMAGE_IMPORT_DESCRIPTOR'
二、这样改了之后程序就不能运行了,因为没有函数 RtlAdjustPrivilege 和 ZwShutdownSystem 的导入信息。现在手工写一个导入表,函数 RtlAdjustPrivilege 和 ZwShutdownSystem 在 ntdll.dll 中的信息用 LordPE 获得:
顺序号 RVA 函数名
---------- ---------- ----------------------
0x0192 0x00029E8C "RtlAdjustPrivilege"
0x0154 0x0000E7E6 "NtShutdownSystem"
这几项中好像只有函数名最重要,顺序号两字节可随便填,RVA在这里不用。
在OD中(也可用WinHex来编辑)选中 00401015 这行,然后数据窗口中跟随选择,在数据窗口中依次输入以下数据:
-----------0--1--2--3--4--5--6--7--8--9--A--B--C--D--E--F
00401010 ---------------3D 10 00 00 00 00 00 00 00 00 00 -@.=.........
00401020 00 72 10 00 00 2D 10 00 00 00 00 00 00 FF FF FF .r..-......
00401030 FF EE EE EE EE 00 00 00 00 00 00 00 00 49 10 00 铑铑........I.
00401040 00 5C 10 00 00 00 00 00 00 54 01 4E 74 53 68 75 ./......TNtShu
00401050 74 64 6F 77 6E 53 79 73 74 65 6D 00 92 01 52 74 tdownSystem.?Rt
00401060 6C 41 64 6A 75 73 74 50 72 69 76 69 6C 65 67 65 lAdjustPrivilege
00401070 00 00 6E 74 64 6C 6C 2E 64 6C 6C 00 00 00 00 00 ..ntdll.dll.....
040103D处开始为IAT表,共4×3字节,指明要导入函数的信息首字节偏移地址。
0401015处开始为导入表开始,103D->040103D处为1049;1049->0401049处为154(函数NtShutdownSystem的顺序号),其后紧跟函数名。0401021处为1072,1072->0401072处为DLL名。
0401025处为102D,102D->040102D处本来有8字节全为0,我发现这8字节好像没用,就用来存放导入函数的地址(见上面的call),不知会不会出问题,反正我这里调试通过了。令人奇怪的是若040102D处8字节全为0,用OD载入后发现导入函数的地址无法写入040102D处,你看到040102D处仍是8字节全0,程序无法运行。在040102D处填入任意非0值都能成功,我这里填入了FFFFFFFF,用OD载入后看到已被改写成两个函数的调用地址。
输入完后复制到可执行文件,保存。
三、用LordPE修改PE头信息;删除多余区段,只保留一个".text"区段。
区段数目: 0x0001
程序执行入口点地址: 0x00001000
代码段基址: 0x00001000
数据段基址: 0x00001088
映像基址: 0x00400000
内存区段对齐单位: 0x00001000
文件区段对齐单位: 0x00000200
映像大小: 0x0000107F
文件头大小: 0x00000200
数据目录 (16) RVA 大小
------------- ---------- ----------
导出表 0x00000000 0x00000000
导入表 0x00001015 0x00000028 (".text")第一处修改
资源 0x00000000 0x00000000
例外表 0x00000000 0x00000000
安全证书 0x00000000 0x00000000
重定位表 0x00000000 0x00000000
调试 0x00000000 0x00000000
版权号 0x00000000 0x00000000
全局指针 0x00000000 0x00000000
TLS 表 0x00000000 0x00000000
加载构造表 0x00000000 0x00000000
绑定导入表 0x00000000 0x00000000
IAT 0x0000103D 0x0000000C (".text")第二处修改
延迟导入表 0x00000000 0x00000000
COM 0x00000000 0x00000000
保留 0x00000000 0x00000000
修改好后保存,可再用LordPE的“重建 PE”整理一下,文件大小应该只有639字节。
第二种改法:
上面是用函数名导入函数,函数的顺序号可有可无甚至可错。下面我不用函数名而用顺序号来导入函数。
IMAGE_THUNK_DATA集合就不再是指向 IMAGE_IMPORT_BY_NAME 结构的指针了,直接填入函数顺序号信息。那么,PE装载器怎么知道IMAGE_THUNK_DATA集合中是顺序号而不是指针呢?
区别是:IMAGE_THUNK_DATA 值的低位字指示函数顺序号,而最高二进位 (MSB)设为1。例如函数ntdll.RtlAdjustPrivilege,顺序号是192h,那么它的 IMAGE_THUNK_DATA 值就是80000192h,写入文件或内存时按低字节在前高字节在后的原则就是:92 01 00 80
下面是完整的导入表数据:
-----------0--1--2--3--4--5--6--7--8--9--A--B--C--D--E--F
00401010 -- -- -- -- 00 31 10 00 00 FF 00 00 00 00 00 00 @.1..骁抾尀
00401020 00 3D 10 00 00 19 10 00 00 00 00 00 00 00 00 00 |=...........
00401030 00 54 01 00 80 92 01 00 80 00 00 00 00 6E 74 64 .T.€?.€....ntd
00401040 6C 6C 2E 64 6C 6C ll.dll
不管用什么工具,修改以上内容后保存,再用 LordPE 改一下:
数据目录 (16) RVA 大小
------------- ---------- ----------
导入表 0x00001015 0x00000014 (".text")
IAT 0x00001031 0x0000000C (".text")
其它的同上。
因为没用函数名,最后文件大小只有582字节了(见附件)。
=====
.386
.model flat, stdcall
option casemap:none
include windows.inc
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data
ShutDown dd ?
RtlAdjustPrivilege db 'RtlAdjustPrivilege',0
BaseNtdll dd ?
ZwShutdownSystem db 'ZwShutdownSystem',0
lby db 'LoadLibraryA',0
gpa db 'GetProcAddress',0
apiaddr dd ?
apiaddr1 dd ?
apiaddr2 dd ?
apiaddr3 dd ?
baseKernel dd ?
szntdll db 'ntdll.dll',0
.code
GetApiAddress proc uses ecx ebx edx esi edi hModule:DWORD, szApiName:DWORD
LOCAL dwReturn: DWORD
LOCAL dwApiLength: DWORD
mov dwReturn, 0
;计算 API 字符串的长度(带尾部的0)
mov esi, szApiName
mov edx, esi
Continue_Searching_Null:
cmp byte ptr [esi], 0 ; 是否为 Null-terminated char ?
jz We_Got_The_Length ; Yeah, we got it. :)
inc esi ; No, continue searching.
jmp Continue_Searching_Null ; searching.......
We_Got_The_Length:
inc esi ; 呵呵, 别忘了还有最后一个“0”的长度。
sub esi, edx ; esi = API Name size
mov dwApiLength, esi ; dwApiLength = API Name size
;从 PE 文件头的数据目录获取输出表的地址
mov esi, hModule
add esi, [esi + 3ch]
assume esi: ptr IMAGE_NT_HEADERS
mov esi, [esi].OptionalHeader.DataDirectory.VirtualAddress
add esi, hModule
assume esi:ptr IMAGE_EXPORT_DIRECTORY ; esi 指向 Kernel32.dll 的输出表
;遍历 AddressOfNames 指向的数组匹配名字:
mov ebx, [esi].AddressOfNames
add ebx, hModule ; 别忘了加上基地址,AddressOfNames 是 RVA
xor edx, edx ; edx = "函数计数",初始化为0
.repeat
push esi ; 保存esi,后面会用到。
mov edi, [ebx] ; edi = 输出表中的当前函数名字
add edi, hModule ; 别忘了加上基地址
mov esi, szApiName ; 函数名字的首地址
mov ecx, dwApiLength ; 函数名字的长度
cld ; 设置方向标志
repz cmpsb ; 开始查找,我们先去喝杯咖啡吧 :)
.if ZERO? ; 找到啦?
pop esi ; 恢复 esi
jmp _Find_Index ; 查找该函数的地址索引
.endif
pop esi ; 恢复 esi
add ebx, 4 ; 下一个函数(每个函数的地址占用4个字节)
inc edx ; 增加函数计数
.until edx >= [esi].NumberOfNames
jmp _Exit ; faint,没找到,凄然退出……
;函数名称索引 -> 序号索引 -> 地址索引
;公式:
;API’s Address = ( API’s Ordinal * 4 ) + AddressOfFunctions’ VA + Kernel32 imagebase
_Find_Index:
sub ebx, [esi].AddressOfNames ; 上面的 repz cmpsb 那里,如果匹配的话,
; esi 就指向了下一个函数的首地址,所以要先减掉它。
sub ebx, hModule ; 减掉基地址,得到 RVA
shr ebx, 1 ; 要除以 2 ,还是因为 repz cmpsb 那行
add ebx, [esi].AddressOfNameOrdinals ; AddressOfNameOrdinals
add ebx, hModule ; 别忘了基地址
movzx eax, word ptr [ebx] ; Now, eax = API’s Ordinal
shl eax, 2 ; 要乘以 4 才得到偏移
add eax,[esi].AddressOfFunctions; + AddressOfFunctions’ VA
add eax, hModule ; + Kernel32 imagebase
;从地址表得到导出函数地址
mov eax, [eax] ; 得到函数的 RVA
add eax, hModule ; 别忘了基地址(说了很多次了,呵呵)
mov dwReturn, eax ; 最终得到的函数的线性地址。(呼……好累啊,终于完成了)
_Exit:
mov eax, dwReturn ; done! :)
ret
GetApiAddress endp
strat:
db 64h,8Bh,40h,30h,8Bh,40h,0Ch,8Bh,70h,0Ch,0ADh,8Bh,00h,8Bh,40h,18h
mov baseKernel,eax
invoke GetApiAddress,baseKernel,addr lby
mov apiaddr2,eax
lea eax,dword ptr ds:[szntdll]
push eax
call apiaddr2
mov BaseNtdll,eax
invoke GetApiAddress,baseKernel,offset gpa
mov apiaddr3,eax
;lea eax,dword ptr ds:[RtlAdjustPrivilege]
push offset RtlAdjustPrivilege
push BaseNtdll
call apiaddr3
mov apiaddr,eax
lea eax,dword ptr ds:[ZwShutdownSystem]
push eax
push BaseNtdll
call apiaddr3
mov apiaddr1,eax
lea eax,dword ptr ds:[ShutDown]
push eax
push 0
push 1
push 13h
call apiaddr
push 2
call apiaddr1
end strat