打造 API 函数 -- API 绝密档案系列之四  告别众乡亲

重要更正,在这里感谢 heXer 指出 GetFunctionAddress 的 bug,现已修正。对于我的粗心,在这里向各位看官道歉。如果你已经下载此文,请以修正版为准。

题目:周谕的老母是谁?诸葛亮的老母是谁?张非的老母是谁?
可怜天下父母心,不幸被伊万大叔答中,“季氏、何氏、吴氏”,伊万大叔立马丢回来一个问题:“林妹妹是怎么死的”,线索越剧名曲。
想当年,徐玉兰、王文娟将宝玉和林妹妹演译到何等林漓至尽,徐玉兰一曲“天上掉下个林妹妹....” 更是唱的荡气回肠。
伊万大叔的答案就在里面,可是我想破脑袋也不得其要领。因为我不是什么斯坦,只好问结果,各位看官可座稳了,下面公布伊万大叔给的答案:
林妹妹是摔死的!!!!,何解,天上掉下来不就摔死了,哈哈哈哈,这都是什么东西吗。TMD(Theater Missile Defens)。
废话少说,言归正传。

附件下载:filtinpt.rar

前面三篇介绍了 GetModuleHandle 和 GetProcAddress 这两个函数,第一个函数获得进程启动时加载在内存的基地址,第二个获取函数的调用的地址。我在第二篇中说过:“可以使用其他的方法达到相同的目的,这样就是最高境界了。”现在我们就按这个思路去做,用自己的方法打造这两个函数。

我们先看看PE文件头中的 IMAGE_OPTIONAL_HEADER :

代码:
77E600C8           dw 10Bh              ; OptionalHeader.Magic 77E600C8           db 5                 ; OptionalHeader.MajorLinkerVersion 77E600C8           db 0Ch               ; OptionalHeader.MinorLinkerVersion 77E600C8           dd 59000h            ; OptionalHeader.SizeOfCode 77E600C8           dd 73E00h            ; OptionalHeader.SizeOfInitializedData 77E600C8           dd 0                 ; OptionalHeader.SizeOfUninitializedData 77E600C8           dd 7A40h             ; OptionalHeader.AddressOfEntryPoint 77E600C8           dd 1000h             ; OptionalHeader.BaseOfCode 77E600C8           dd 5A000h            ; OptionalHeader.BaseOfData 77E600C8           dd 77E60000h         ; OptionalHeader.ImageBase 77E600C8           dd 1000h             ; OptionalHeader.SectionAlignment 77E600C8           dd 200h              ; OptionalHeader.FileAlignment ...... ......


我们感兴趣的不是 ImageBase 因为我们担心万一模块没有被装载在这个地址上,基于这点写的代码肯定土崩瓦解。我们感兴趣的是:

代码:
77E600C8           dd 1000h                        ; OptionalHeader.SectionAlignment


除非你自己修改 PE 文件头,不管什么编译器,SectionAlignment 总是等于 0x1000,有了这个认识我们就开始构造我们的GetModuleHandle,当然,我们不会使用这个名字,这样会把编译器给弄傻了,而且我也不喜欢这个名字,我们就使用我在上一篇中提过的名字 GetProcBaseAddress。代码非常简单,就是利用这个 0x1000 的对齐特性,随便找一个在这个模块中的函数调用地址,将这个地址异或 0xFFFF0000 ,也就是对齐到 0x1000,然后往前查找 DOS 文件头的 e_magic 和 PE 文件头的 Signature,按 SectionAlignment 的值 0x1000递减。

代码:
GetProcbaseAddress proc uses ebx esi FunAddr:DWORD     .if !FunAddr         ;如果 FunAddr = 0 随便 Call 一下,pop eax 就是当前的地址         call $ + 5         pop eax     .else         ;如果是某个函数的调用地址,函数调用地址一般有下面的格式:         ; __stdcall MessageBoxA(x,x,x,x)         ; MessageBoxA proc near         ;FF 25 08 04 40 00     jmp     ds:__imp__MessageBoxA         ; MessageBoxA endp         ;其中 FF 25 是 jmp 的操作码,后面的 00400408 是函数调用地址         ;这样就容易理解下面的代码了。         mov eax, FunAddr         mov eax, [eax+2]         mov eax, [eax]     .endif     and eax, 0FFFF0000h     .repeat         .if word ptr [eax] != 'ZM'             xor esi, esi             sub eax, 1000h         .else             mov esi, [eax+IMAGE_DOS_HEADER.e_lfanew]         .endif         add esi, eax     .until dword ptr [esi] == 'EP'     ret GetProcbaseAddress  endp


是不是很简单?但使用的前提是必须有一个该模块函数的调用地址,其实这一点是非常容易实现的。在后面给出完整的代码中可以看到如何处理。
下面我们接着构造 GetFunctionAddress(GetProcAddress),在给出代码前我们首先回忆上一篇中给出的那个 ntdll.dll 的 PE 文件头和输出表的具体结构,详细请看上篇,这里只列出其中的输出表:

代码:
77EB4230 ExportTable      77EB4230           dd 0                                ; Characteristics  77EB4230           dd 41B04DB0h                        ; TimeDateStamp 77EB4230           dw 0                                ; MajorVersion 77EB4230           dw 0                                ; MinorVersion 77EB4230           dd offset NameStrBase               ; Name 77EB4230           dd 1                                ; Base 77EB4230           dd 33Dh                             ; NumberOfFunctions 77EB4230           dd 33Dh                             ; NumberOfNames 77EB4230           dd offset AddressOfFunctionsBase    ; AddressOfFunctions 77EB4230           dd offset AddressOfNamesBase        ; AddressOfNames 77EB4230           dd offset AddressOfNameOrdinalsBase ; AddressOfNameOrdinals


另外我们也仔细研究输出表中 AddressOfNames 所指向的函数名表 NameStrBase。

代码:
...... ...... 77EB8DC4 aRequestdevicew    db 'RequestDeviceWakeup',0 77EB8DD8 aRequestwakeupl    db 'RequestWakeupLatency',0 77EB8DED aResetevent        db 'ResetEvent',0 77EB8DF8 aResetwritewatc    db 'ResetWriteWatch',0 77EB8E08 aResumethread      db 'ResumeThread',0 77EB8E15 aRtlfillmemory     db 'RtlFillMemory',0 77EB8E23 RtlFillMemory      db 'NTDLL.RtlFillMemory',0 77EB8E37 aRtlmovememory     db 'RtlMoveMemory',0 77EB8E45 RtlMoveMemory      db 'NTDLL.RtlMoveMemory',0 77EB8E59 aRtlunwind         db 'RtlUnwind',0 77EB8E63 RtlUnwind          db 'NTDLL.RtlUnwind',0 77EB8E73 aRtlzeromemory     db 'RtlZeroMemory',0 77EB8E81 RtlZeroMemory      db 'NTDLL.RtlZeroMemory',0 77EB8E95 aScrollconsoles    db 'ScrollConsoleScreenBufferA',0 77EB8EB0 aScrollconsol_0    db 'ScrollConsoleScreenBufferW',0 77EB8ECB aSearchpatha       db 'SearchPathA',0 ...... ......


在这张表中,其中有几个函数名出现两次,另一个函数名前多了一个模块指引。我想大概原因是,一方面,老盖不希望用户直接调用许多底层的函数,另一方面也是为了用户使用方便,将常用函数放在一个模块中,前面的模块指引指出该函数所在的模块,这也就是 bug 所在。我们的 GetProcBaseAddress 运行的条件是必须先有一个该模块的函数调用地址,然后由这个地址搜索模块的基地址,以这种模式运行,不能解决直接从模块名获得模块基地址,为此,必须打造另一个具有类似 GetModuleHandle 功能的函数,这就是我们的新函数 GetDllBase ,GetDllBase 的工作方式和 GetProcBaseAddress 完全不同,所起的作用却是相同的,其实在打造了 GetDllBase 后,可以完全舍弃这个 GetProcBaseAddress 的函数,但我还是将他保留下来,我觉得这样比较好,让你可以看到完成同种的功能,可以使用不同的方法。下面是 GetDllBase 实现的代码:

代码:
GetDllBase proc uses ecx edx esi lpProcName:DWORD     ;mov eax, large fs:18h     xor eax, eax     ; masm32 在编译普通exe文件时,不支持 mov eax, fs:[18] 指令     ; 因此写成如下格式,编译后等同 mov eax, fs:[18]     db 64h     mov eax, ds:18h                               ; TEB.Self                                                        mov eax, [eax].TEB.Peb                        ; 取 PEB 表的地址     mov eax, [eax].PEB.Ldr                        ; 取 PEB_LDR_DATA 结构的地址     ; PEB_LDR_DATA.InLoadOrderModuleList.Flink 指向 LDR_DATA_TABLE_ENTRY     add eax, PEB_LDR_DATA.InLoadOrderModuleList.Flink     ; 这个结构形成一个链表     mov ecx, [eax].LDR_DATA_TABLE_ENTRY.InLoadOrderModuleList.Flink Loop_SeachModule:     cmp ecx, eax     jz  Exit     mov edx, ecx     ; 获取下一个 LDR_DATA_TABLE_ENTRY 结构的地址     mov ecx, [ecx].LDR_DATA_TABLE_ENTRY.InLoadOrderModuleList.Flink     ; 比较该内存地址是否为空,如果是,则出错     cmp [edx].LDR_DATA_TABLE_ENTRY.InMemoryOrderModuleList.Flink, 0     jz  Loop_SeachModule     ; 获取模块名的地址     lea esi, [edx].LDR_DATA_TABLE_ENTRY.BaseDllName.UNICODE_STRING.woLength     push eax     ; 验证是否是我们需要的模块     invoke ComparAnsiStrUnicodeStr, lpProcName, esi     cmp eax, 1     pop eax     jnz Loop_SeachModule     ; 如果是,取该模块的基地址     mov eax, [edx].LDR_DATA_TABLE_ENTRY.DllBase     jmp Exit1 Exit:     xor eax, eax Exit1:     ret GetDllBase endp


GetDllBAse 的工作原理是利用 LDR_DATA_TABLE_ENTRY 结构的特性,这个结构如下:

代码:
PEB_LDR_DATA STRUCT                 ; sizeof = 24h     _Length                         DWORD       ?   ; original name Length     Initialized                     BYTE        ?   ; 04h                                     db  3 dup(?)    ; padding     SsHandle                        PVOID       ?   ; 08h     InLoadOrderModuleList           LIST_ENTRY  <>  ; 0Ch     InMemoryOrderModuleList         LIST_ENTRY  <>  ; 14h     InInitializationOrderModuleList LIST_ENTRY  <>  ; 1Ch PEB_LDR_DATA ENDS PPEB_LDR_DATA typedef PTR PEB_LDR_DATA LIST_ENTRY STRUCT     Flink DWORD ?     Blink DWORD ? LIST_ENTRY ENDS LDR_DATA_TABLE_ENTRY_U0 union ; (sizeof=0X8, standard type)     HashLinks                       LIST_ENTRY     <>     SectionPointer                  DWORD          ?       ; offset (FFFFFFFF) LDR_DATA_TABLE_ENTRY_U0 ends LDR_DATA_TABLE_ENTRY_U1 union ; (sizeof=0X4, standard type)     TimeDateStamp                   DWORD          ?     LoadedImports                   DWORD          ?       ; offset (FFFFFFFF) LDR_DATA_TABLE_ENTRY_U1 ends LDR_DATA_TABLE_ENTRY struc ; (sizeof=0X54, standard type)     InLoadOrderModuleList           LIST_ENTRY     <>     InMemoryOrderModuleList         LIST_ENTRY     <>     InInitializationOrderModuleList LIST_ENTRY     <>     DllBase                         DWORD          ?       ; offset (FFFFFFFF)     EntryPoint                      DWORD          ?       ; offset (FFFFFFFF)     SizeOfImage                     DWORD          ?     FullDllName                     UNICODE_STRING <>     BaseDllName                     UNICODE_STRING <>     Flags                           DWORD          ?     LoadCount                       WORD           ?     TlsIndex                        WORD           ?     U0                              LDR_DATA_TABLE_ENTRY_U0 <>     CheckSum                        DWORD          ?     U1                              LDR_DATA_TABLE_ENTRY_U1 <>     EntryPointActivationContext     DWORD          ?       ; offset (FFFFFFFF)     PatchInformation                DWORD          ?       ; offset (FFFFFFFF) LDR_DATA_TABLE_ENTRY ends


LDR_DATA_TABLE_ENTRY 结构的前三个元素都是链表结构,定义成 LIST_ENTRY 结构,通过 LIST_ENTRY.Flink (向前链界) 或 LIST_ENTRY.Blink (向后链接),和下一个 LDR_DATA_TABLE_ENTRY 链接,因此通过这个结构,可以遍列所有加载的模块,我们就是根据这个特性来得到所需模块的基地址。
PEB.Ldr 指向 PEB_LDR_DATA ,PEB_LDR_DATA.InLoadOrderModuleList.Flink 就指向我们关心的 LDR_DATA_TABLE_ENTRY 表。

各位还记得上一篇中那个“象鼻子吸盘”的代码吗,这段代码最后返回的是某个函数的序号,AddrOfNameOrdinalsBase 序号表单位是 WORD ,根据这个序号,在 AddressOfFunctions 相应的位置上就是函数的调用地址了,就这么简单,只要理解了输出表的结构中几个元素的含义,就非常容易打造我们自己的函数:

代码:
GetFunctionAddress proc uses ecx ebx esi edi BaseAddress:DWORD, lpProcName:DWORD     LOCAL Count                 :DWORD     LOCAL NumberOfNames         :DWORD     LOCAL AddressOfNames        :DWORD     LOCAL AddressOfNameOrdinals :DWORD     LOCAL AddressOfFunctions    :DWORD     LOCAL ExportSize            :DWORD     LOCAL ProcAddr              :DWORD     LOCAL ExportAddr            :DWORD     mov ebx, BaseAddress     movzx eax, word ptr [ebx].IMAGE_DOS_HEADER.e_lfanew     add ebx, eax     mov eax, [ebx].IMAGE_NT_HEADERS1.OptionalHeader.DirectoryExport.isize     mov ExportSize, eax     mov ebx, [ebx].IMAGE_NT_HEADERS1.OptionalHeader.DirectoryExport.VirtualAddress     add ebx, BaseAddress     mov ExportAddr, ebx     mov eax, [ebx].IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals     add eax, BaseAddress     mov  AddressOfNameOrdinals, eax     mov eax, [ebx].IMAGE_EXPORT_DIRECTORY.NumberOfNames     mov NumberOfNames, eax     mov eax, [ebx].IMAGE_EXPORT_DIRECTORY.AddressOfNames     add eax, BaseAddress     mov  AddressOfNames, eax     mov eax, [ebx].IMAGE_EXPORT_DIRECTORY.AddressOfFunctions     add eax, BaseAddress     mov AddressOfFunctions, eax     mov eax, NumberOfNames     and Count, 0     lea ecx, [eax-1]              ; ecx = NumberOfNames - 1     test ecx, ecx     jl CheckEnd Loop1:     mov eax, Count     mov edi, lpProcName     lea esi, [eax+ecx]            ; ecx = NumberOfName - 1                                                                 ; esi = Count + ecx     mov eax, AddressOfNames       ; eax = AddressOfNamesBase     sar esi, 1                    ; esi / 2     mov eax, [eax+esi*4]          ; 在AddressOfNames偏移地址表中取函数名的偏移量     add eax, BaseAddress          ; 加上基地址得到函数名的地址 Loop_If_dl_IsNotTerminalChar:     ; 从函数名中取一个字符     mov bl, [edi]     mov dl, bl     cmp bl, [eax]                 ; 和函数名列表中的函数名在相应的位置上进行比较,特别                                   ; 需要注意,当两个字符进行比较时:                                   ;  if bl >= [eax]                                   ;     CF = 0          //这里将CF标志位置 0                                   ;  else               //bl < [eax]                                   ;     CF = 1                                   ;  end                                   ; 这里将 CF(进位标志) 置位,根据函数名排序的条件,即                                   ; 决定了是向前搜索,还是向后搜索:                                   ;   jnz short IfCharNotEqu                                   ; 不影响标志位,这个标志位将在后面的代码中使用     jnz IfCharNoEqu     test dl, dl     jz If_dl_IsTerminalChar     mov bl, [edi+1]               ; 取下一个字符     mov dl, bl     cmp bl, [eax+1]               ; 和下一个函数名字符进行比较,这里和上面的比较相同,                                   ; 通过设置 CF 标志位来决定搜索的方向。     jnz  IfCharNoEqu     inc edi                       ; 调整指针,指向下二个字符     inc edi     inc eax     inc eax     test dl, dl     jnz Loop_If_dl_IsNotTerminalChar If_dl_IsTerminalChar:     xor eax, eax     jmp CheckDirection IfCharNoEqu:                      ; 指令     sbb eax, eax                  ;   sbb eax, eax    //带进位减                                   ; 其结果只有两种可能                                   ;   if CF == 0                                   ;      eax = 0                                   ;      set CF = 0                                   ;   else        //CF = 1                                   ;      eax = 0xFFFFFFFF                                   ;      set CF = 1                                   ;   end;     sbb eax, 0FFFFFFFFh           ; 指令                                   ;   sbb eax, 0FFFFFFFFh    //带进位减                                   ; 其结果也是两种可能                                   ;   if  CF == 0                                   ;      eax = 0                                   ;      set SF = 0                                   ;   else         //CF = 1                                   ;      eax = 0xFFFFFFFF                                   ;      set SF = 1                                   ;   end                                   ; 这里 SF 是符号标志位,即判断结果是正数还是负数。 CheckDirection:                     ; 指令     test eax, eax                 ;   test eax, eax                                   ; 一般我们用来测试某一位是 1 还是 0,但这个操作同样影                                   ; 响符号标志位:                                   ;   if eax >= 0                                   ;     set SF = 0                                   ;   else                                   ;     set SF = 1                                   ;   end     jge ForewardSearch            ; Jump if Greater or Equal (SF=0) BackwardSearch:     lea ecx, [esi-1]     jmp NextSearch ForewardSearch:     jle SearchEnd     lea eax, [esi+1]     mov Count, eax NextSearch:     cmp ecx, Count     jge Loop1     jmp SearchEnd CheckEnd:     mov esi, NumberOfNames SearchEnd:     cmp ecx, Count     jge GetOrdinal     or ax, 0FFFFh     jmp Exit GetOrdinal:     mov eax, AddressOfNameOrdinals     mov ax, [eax+esi*2]     movzx ecx, word ptr ax      ; 序号列表的单位是 WORD 所以 esi*2 即得到序号的偏移                                 ; 地址,加上基地址,即得到函数的序号。     mov eax, AddressOfFunctions ; 函数地址表起始地址     lea eax, [eax+ecx*4]        ; 加上序号乘4,因为函数地址表的单位是 DWORD     mov ecx, [eax]              ; 取函数地址的偏移量     add ecx, BaseAddress        ; 加上基地址即函数的调用地址     mov ProcAddr, ecx     mov edx, ExportSize     add edx, ExportAddr     ; 如果返回的函数调用地址在 ExportAddr 之间,则该函数在外部模块     .if ((ecx > ExportAddr) && (ecx <= edx))         ; 验证是否为有效的字符串         invoke CheckIsStr, ecx         .if eax             ; 如果是,根据模块前部的指引获得该模块的基地址             invoke GetDllBase, eax             ; 调用自己,获得函数调用地址             invoke GetFunctionAddress, eax, lpProcName             mov ProcAddr, eax         .endif     .endif Exit:     mov eax, ProcAddr     ret GetFunctionAddress endp


有了这三个个函数,下面是实战了,我在目录 \masm32\EXAMPLE1\FILTINPT 中随便选了段代码 FILTINPT 来演示我们自己打造的函数,因为代码太长,所以只摘录部分来讲解,需要仔细研究的可以下载。
首先将该程序的所有 API 函数列表:

代码:
    .data     ......     ;API 函数名列表     szCallWindowProcA      db 'CallWindowProcA',0     szCreateWindowExA      db 'CreateWindowExA',0     szDefWindowProcA       db 'DefWindowProcA',0     ......     ......     szGetModuleHandleA     db 'GetModuleHandleA',0                            dd  0     EVEN     ;API 函数名引用表     OffCallWindowProcA     dd  offset szCallWindowProcA     OffCreateWindowExA     dd  offset szCreateWindowExA     OffDefWindowProcA      dd  offset szDefWindowProcA     ......     ......     ;自建输入表     ; Imports from user32     _CallWindowProcA       dd  ?     _CreateWindowExA       dd  ?     _DefWindowProcA        dd  ?     ;注意下面是代码段     .code          ;下面这条宏命令是不让 proc 宏自动生成 ebp 框架的代码     option prologue:none ; 自建输入表引用表 ; __stdcall CallWindowProcA(x,x,x,x,x) MyCallWindowProc    proc lpPrevWndFunc:DWORD,   \                          hWndl:DWORD,           \                          Msg:DWORD,             \                          wParam:DWORD,          \                          lParam:DWORD                 jmp     _CallWindowProcA    ; CallWindowProcA(x,x,x,x,x) MyCallWindowProc    endp ; __stdcall CreateWindowExA(x,x,x,x,x,x,x,x,x,x,x,x) MyCreateWindowEx    proc dwExStyle:DWORD,       \                          lpClassName:DWORD,     \                          lpWindowName:DWORD,    \                          dwStyle:DWORD,x:DWORD, \                          y:DWORD,               \                          nWidth:DWORD,          \                          nHeight:DWORD,         \                          hWndParent:DWORD,      \                          hMenu:DWORD,           \                          hInstance1:DWORD,      \                          lpParam:DWORD                 jmp     _CreateWindowExA    ; CreateWindowExA(x,x,x,x,x,x,x,x,x,x,x,x) MyCreateWindowEx    endp ; __stdcall DefWindowProcA(x,x,x,x) MyDefWindowProc     proc hWnd1:DWORD,Msg:DWORD,wParam:DWORD,lParam:DWORD                 jmp     _DefWindowProcA    ; DefWindowProcA(x,x,x,x) MyDefWindowProc     endp ...... ......     ;恢复 proc 宏自动生成堆栈框架     option prologue:PrologueDef


下面是原来的程序,将程序中所有的 API 调用前面都加了 My 前缀,引用我们自建的 API 表:

代码:
WndProc proc hWin   :DWORD,              uMsg   :DWORD,              wParam :DWORD,              lParam :DWORD     .if uMsg == WM_COMMAND     ;======== menu commands ========         .if wParam == 1000             invoke  MySendMessage,hWin,WM_SYSCOMMAND,SC_CLOSE,NULL         .elseif wParam == 1900             szText TheMsg,"Assembler, Pure & Simple"             invoke  MyMessageBox,hWin,ADDR TheMsg,ADDR szDisplayName,MB_OK         .endif     ;====== end menu commands ====== ...... ......


最后是启动部分,初始化自建的API表:

代码:
start:     jmp S1     ;这里加了一句永远也不会执行的 API 调用,目的是为了获得 user32.dll 的基地址     invoke  MessageBox,0,ADDR TheText,ADDR szDisplayName,MB_OK S1:     mov eax, offset ExitProcess          ;取ExitProcess函数地址     invoke GetProcbaseAddress, eax       ;获取kernel32.dll基地址 ;    invoke GetDllBase, addr szKernel32  ;测试     mov MyImageBase, eax                 ;保存     mov esi, offset OffExitProcess       ;取kernel32.dll API 函数名引用表的首地址     mov edi, offset _ExitProcess         ;取kernel32.dll 自建输入表首地址     .while (dword ptr [esi] != 0)         invoke GetFunctionAddress, MyImageBase, [esi]         stosd                            ;建立kernel32.dll的输入表         lodsd     .endw     mov eax, offset MessageBox           ;取MessageBox函数的地址     invoke GetProcbaseAddress, eax       ;获取user32.dll基地址     mov MyImageBase, eax                 ;保存     mov esi, offset OffCallWindowProcA   ;取user32.dll API 函数名引用表首地址     mov edi, offset _CallWindowProcA     ;取user32.dll 自建输入表首地址     .while (dword ptr [esi] != 0)         invoke GetFunctionAddress, MyImageBase, [esi]         stosd                            ;建立user32.dll的输入表         lodsd     .endw     ;下面是原程序的代码,所有 API 函数都家里 My 前缀     invoke  MyGetModuleHandle, NULL     mov hInstance, eax     invoke  MyGetCommandLine     mov CommandLine, eax     invoke  WinMain,hInstance,NULL,CommandLine,SW_SHOWDEFAULT     ;这里保留是为了取 Kernel32.dll 的基地址     invoke  ExitProcess,eax     ret end start


这里给出的代码和壳实际使用的代码差别并不大,就是少了垃圾代码,当然,函数名子也需要简单的处理一下。这一点上 Themida 的 GetFunctionAddress 写的比较有水平,隐蔽性相当强,垃圾代码也写的比较好,基本做到了真中有假,假中有真,但基本原理和上面的代码相同(比我们这个要简单,只是从头到尾的查表,不过加上垃圾代码有几千行,Themida 最有效的战略手段就是疲劳战,技术上虽然比较先进,但也不是不可突破的,包括那个许多人害怕的驱动,实际也是很容易突破的),Themida 的 GetFunctionAddress 几个入口参数中有一个是函数名的第一个字母,还有一个密码数,当然还有函数的基地址。先用这个首字母快速定位,然后将获取的函数名加密,和参数的密码比较而确定是否是需要的函数,整个过程中不出现函数名字,给出一个字母的目的可能是为了加快搜索速度,不过我认为没有必要,你只要找到这个GetFunctionAddr的地址下断点 Themida 什么时候调用什么函数就一清二楚了,对于其他的函数,Themida 并没有处理。另外在实际使用中也不会将函数列表,用一个取一个,用完就删掉,要用再取。

说明: 代码中给出的 PEB 结构得自于 WinDGB,适用 win2k 系统,对于其他系统,请自行改造。TEB 结构我没有完整的(虽然我自己弄了一个所谓完整的,但可能有许多错误,所以没敢列出),只列出前部,省略了大部分,4F 给出的 TEB 表也只有前部少数是正确的,(我跟踪系统程序时验证过)。
这个系列就暂时告一段落,以后有兴趣再写点什么垃圾来玩玩,希望大家喜欢,多顶一下。

后话:肺腑之言
许多朋友认为自己能用别人提供的方法就算学会破解,或能使用某种语言就认为理解了操作系统,其实这是远远不够的,正所谓知其然,不知其所以然。深刻的理解系统底层的一些方法,可以让你做到知其所以然,将来不管你从事那类开发,都会获益不浅。尤其是通过自己对底层代码的分析,在这个过程中,本身就是最好的学习,尤其是能锻炼你分析代码的能力,理解每一句代码的深层含义。
通往高手的路不是一条平坦的路,一定会遇到许多你根本就想不到的困难,一定要做好充分的思想准备,不要怕挫折,有些问题实在想不通可以放放,也许你在研究别的东西时豁然开朗,触类旁通,那些问题无意中就解决了。
朝思暮想,一招交手,乃门外汉也。不管多好的文章,光看是没有用的,要变成自己的东西,就要实践,不断的写。
在北美你去找一个地产经纪,问买房子的要点是什么,这个经纪会告诉你:
1、Location
2、Location
3、Location
意思是,位置,位置,位置,也就是决定一个房子的好坏最重要的就是房子的位置。
同样,如何才能成为真正的高手:
1、实践
2、实践
3、实践
金大侠在笑傲江湖有这样一段话(不是原话,是意思),那是发生在华山绝顶,风清杨祖师指点令狐冲武功时,随便从地上拣起一根不知是那个魔教长老的大腿骨,象令狐冲一比划,问这个怎么破,令狐冲说这个不是招啊,这就引出一个绝世真理,无招胜有招。无招胜有招并不是说你什么都不学乱来一气,就能胜过任何人了,这样理解就完全错了,这里的无招是建立在无数招数的艰苦锻炼上而达到的,到你真正理解了这些招数之后,返朴归真而达到从有招到无招的一种境界。写程序也一样,当你真正能从有招过度到无招,那时就是无招胜有招了。那就是当年独孤求败大侠的境界了。这非常符合辩证唯物主义的否定之否定规律,当历史螺旋发展到高级阶段时,可以见到初级阶段的雏影。也就是从什么都不会(无招)学到来去无踪(重返无招)。