【工具】 OllyDbg , LordPE , WinHex,RadASM

【平台】 WinXP SP2

【名称】 快速关机.exe

【名称】个人练手,高手勿进


某软件中有一“快速关机”功能,其关机速度之快,几乎与按电源开关一样,拿来分析一下:

00401630   >push    ecx ;保存数值
00401631   >lea     eaxdword 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     esidword 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     eaxdword ptr [4022D4]
00401321   >or      eaxeax
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     eaxdword ptr [4022E0]
0040136D   >or      eaxeax
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字节了(见附件)。

上传的附件 快速关机pediy.rar