反--反转存和非侵入性跟踪器(上)

  1. 摘要
  2..必须的知识
  2.1.自定位代码
  2.2.找回kernel32.dll基址和APIs
  2.3. 内存补丁注入
  2.4 混合钩子的方法
  2.5 接下来呢?
  3.内存管理
  3.1.扩展程序空间
  3.2.用VirtualAlloc 和 VirtualFree管理内存
  3.3.Delphi代码的问题
  3.4.内存管理的结论
  关键词:编码,钩子,反-转存,非入侵,内存管理
  1.  摘要
  现在有许多壳都是把代码分割并重定位到新的缓冲区来阻止转存。这样的缓冲区有时很难修复,从而许多人在这里放弃了。我可以列举几例,运用得就是这种方法,如:Armadillo, ASProtect SKE, krypton等。到目前我们是运用od脚本或外面的工具去修复被分割的代码或被除去的输入地址表。然而我们并不知道他的思想方法,最终也学不到新东西。
  我在这里展示一些思想方法,它已经被运用到解决以上的问题了,但是要理解它是有些困难的。首先你应该熟悉汇编,PE格式和钩子。最好有些基础,因为在这篇文章里会涉及他们中的一些重要部分。
  我想这些技巧还没有运用于RCE,至少我还没见到。无论如何我会涉及到RCE的一些有趣的方面。
  2.  必须的知识
  如果你已知道所有的这方面材料。就可以进入第3部分了,因为我将覆盖自定位代码和向目标进程注入的一些基础知识。
  2.1.自定位代码
  自定位代码是一种可以在任何特定的内存区域中执行的代码。这样的代码不仅用在病毒和shell中,而且一些壳也用到。自定位代码的主要问题是访问数据的能力。因此,病毒作者用“delta”偏移去访问自身的变量。在这一点上我将走的很快,并直接展示代码以及如何在自定位代码中参考变量。
                      call delta
  delta:              pop ebp
                      sub ebp, offset delta
  
                      mov eax, [ebp+kernel32]
  
                      call [ebp+GetModuleHandleA]
  kernel32             dd     ?
  GetModuleHandleA     dd     ?
  可见,自定位代码中熟练操作数据并不困难。这里有一些规则:
  ——ebp经常用作delta, esi, edi和ebx 也可能被用作delta ,因为在调用API时,他们的值并不会改变。我一直用的是ebp,因为esi/edi经常联合应用于数据的拷贝,而ebx被用来当作重要数据的指针(如:在病毒感染期间,可以用它指向任何我们所需的PE文件。)(译者注:call/pop/sub三个指令的组合是经典的解决代码重定位的方法,几乎所有的病毒开始就是这三句)
  以下是由Super/29a 提出,稍后由Benny/29a [1]描述的一个运用delta的一个技巧,可以使你编译的代码更小。
                      call delta
  delta:              pop ebp
                      mov eax, [ebp+kernel32-delta]
                      call [ebp+GetModuleHandleA-delta]
  kernel32            dd   ?
  GetModuleHandleA    dd   ?
  事实上,这是一个非常好的技巧,但是我们现在不太多关心代码的大小.我们可以运用以前的方法,因为它在调试时的可读性强,并且在编写自定位代码时键入的更少。好了,我要告诉你们的关于自定位代码的就是这些了。不要只学习它的定义(你会找到许多关于它的),更重要的是它的原理。
  2.2 重新得到 kernel32.dll 基址和 APIs
  对于自定位代码来说,下一个重要的问题就是在Win32环境下调用APIs。为了能这样做,我们需要定位kernel32.dll的基址。以下是一些我们可以完成它的方法。
  -扫描 SEH
  -由Ratter/29a 提供的 PEB 技巧
  -获得硬编码。(译者注:比如Win2k下一般是77e60000h,WinXP SP1
            是77e40000h,SP2是7c800000h等。但是这么做不具有通用性)
  扫描 SEH---首先我们需要知道描述的SEH(结构化异常处理)的数据结构表
  kd> dt nt!_EXCEPTION_REGISTRATION_RECORD
  +0x000 Next       : 前一个 _EXCEPTION_REGISTRATION_RECORD结构
  +0x004 Handler     : 异常处理回调函数地址
  kd>
  (译者注:这是windbg中的命令,用来获取结构的信息,很实用)
  
  任何SEH链将会赋予一个指向kernel32.dll中某个值的句柄,当然,如果这是最后一个EXCEPTION_REGISTRATION_RECORD,那么_EXCEPTION_REGISTRATION_RECORD 结构的第一个参数名将被置为-1,这样我们就知道何时在kernel32.dll中获取地址了
  
  getkernelbase:
                  pushad
                  xor edx, edx
                  mov esi, dword ptr FS:[edx]
  __seh:         lodsd
                  cmp eax, 0FFFFFFFFh
                  je __kernel
                  mov esi, eax
                  jmp __seh
  __kernel:       mov edi, dword ptr[esi + 4]
                  and edi, 0FFFF0000h
  __spin:         cmp word ptr[edi], 'ZM'
                  jz __test_pe
                  sub edi, 10000h
                  jmp __spin
  __test_pe:      mov ebx, edi
                  add ebx, [ebx.MZ_lfanew]
                  cmp word ptr[ebx],'EP'
                  je __exit_k32
                  sub edi, 10000h
                  jmp __spin
  __exit_k32:     mov [esp.Pushad_eax], edi
                  popad
                  ret
  
  这个代码并不是最优化的,但是它展示了逻辑。扫描SHE直到这里等于-1,简单的获得句柄的地址和搜索MZ与PE符号。一旦我们找到了他们,我们就获了kernel32.dll的地址。
  PEB 方法---这个方法是由Ratter/29a发现和提出的,我会给出例子,但是要想获得更多的解释请回到[2](译者注:应指的是文后参考文献[2],也可以参见http://www.nsfocus.net/index.php?act=magazine&do=view&mid=2002)
  
  mov eax, dword ptr FS:[30h]
  mov eax, dword ptr[eax+0ch]
  mov eax, dword ptr[eax+1ch]
  mov eax, dword ptr[eax]
  mov eax, [eax+8]
  
  首先我们找回PEB的值,接着找到PEB_LDR_DATA,随后我们就进入到InInitializationOrderModuleList,头一个LIST_ENTRY指向ntdll.dll ,再进入下一个表的入口,瞧,kernel32.dll 的基址我们就接收到了。
  
  获得装入时硬编码的值----这个非常简单,并不需要太多的知识,我们用GetModuleHandleA去重新获得kernel32.dll的值并储存到偏移值中。
  例码:
             pushs <"kernel32.dll">
             call GetModuleHandleA
             mov kernel32, eax
  loader:
  ...
  kernel32   dd   ?
  
  重新获取API的地址也很简单,一旦我们获得了kernel32.dll,我们就可以写出我们自己的GetProcAddress,在kerlnel32.dll的输出表中查找我们所需的APIs
  输出表被装载在离PE头偏移78h处,它的结构如下:
  typedef struct _IMAGE_EXPORT_DIRECTORY {
        DWORD Characteristics;
        DWORD TimeDateStamp;
        WORD MajorVersion;
        WORD MinorVersion;
        DWORD Name;
        DWORD Base;
        DWORD NumberOfFunctions;
        DWORD NumberOfNames;
        DWORD AddressOfFunctions; // 指向导出函数地址表的RVA
        DWORD AddressOfNames; // 指向函数名地址表的RVA
        DWORD AddressOfNameOrdinals; // 指向函数名序号表的RVA
  } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
  
  在这个结构中有三个重要的成员:
  DWORD AddressOfFunctions; //指向导出函数地址表的RVA
  DWORD AddressOfNames; //指向函数名地址表的RVA
  DWORD AddressOfNameOrdinals; //指向函数名序号表的RVA
  以下的表格将会表示的更加清晰。
  
  array of name RVA          oridnals           array of functions RVA
  +---------------+     +---------------+     +---------------+
  | RVA1 name |  <----->  | ordinal1 |    <----->| RVA of API1 |
  +---------------+ +   ---------------+      +---------------+
  | RVA2 name |  <----->  | ordinal2 |    <----->| RVA of API2 |
  +---------------+ +-  --------------+      +---------------+
  | RVA3 name |  <----->  | ordinal3 |    <----->| RVA of API3 |
  +---------------+ +-  --------------+       +---------------+
  | RVA4 name |  <----->  | ordinal4 |    <----->| RVA of API4 |
  +---------------+ +--  -------------+       +---------------+
  | RVA5 name |  <----->  | ordinal5 |     <----->| RVA of API5 |
  +---------------+  +--  -------------+       +---------------+
  我们先在AddressOfNames中搜寻我们所要API的名字会得到他的索引,然后再依此索引在AddressOfFunctions中得到oridnal的索引,接着就可以获得本程序中API的RVA(相对虚拟地址),最后加上dll的基址就是我们所需API的地址。就是这么简单(译者注:可以参见罗云彬的书,讲的比较详细)
  为了缩减病毒和后来的shellcodes的大小,并逃脱扫描检测特殊字符串,病毒作者运用hash或crc32校验来寻找APIs。最简单,迄今最快的hashing algo由z0mbie介绍,由rol/xor组成。(译者注:参见29A-4.227)
  __1:   rol eax, 7 ;hash algo (x) by z0mbie
            xor al, byte ptr [edx]
            inc edx
            cmp byte ptr [edx], 0
            jnz __1
  2.3.内存补丁注入
  内存补丁注入事实上是简单的装入,它将注入把我们的自定位代码注入目标进程,这样的注入为我们干一些卑鄙的工作(如:钩子),也许你会奇怪我为什么不用dll注入的方法。简单的说我不喜欢在一个文件夹下有两个源文件,而且作为病毒程序,除了独立偏移外我不喜欢任何其它的事情。记住,自定位代码是非常好的,而dll注入在C程序中很普遍。
  好了,让我们来看看如何把自定位代码注入到目标中。首先我们用CreateProcessA创建一个挂起的进程,接着用VirtualAllocEx和WriteProcessMemory在目标中写我们的自定位代码。接着在目标的入口处储存钩子直到钩子到达。一旦我们成功装好钩子,则自定位代码就承担起钩子的任务,做所有卑鄙的事,储存原始字节并返回目标进程的入口点。
                push offset pinfo
                push offset sinfo
                push 0
                push 0
                push CREATE_SUSPENDED
                push 0
                push 0
                push 0
                push offset progy
                push 0
                callW CreateProcessA
                push PAGE_EXECUTE_READWRITE
                push MEM_COMMIT
                push 2000h
                push 0
                push pinfo.pi_hProcess
                callW VirtualAllocEx ;allocate big enough block
                mov mhandle, eax
                push 0
                push 2
                push offset infinite
                push 401000h
                push pinfo.pi_hProcess
                callW WriteProcessMemory ;store jmp $ at entry point
                push pinfo.pi_hThread
                callW ResumeThread
                mov ctx.context_ContextFlags, CONTEXT_FULL
  __cycle_ep:
                push 100h
                callW Sleep
                push offset ctx
                push pinfo.pi_hThread
                callW GetThreadContext
                cmp ctx.context_eip, 401000h
                jne __cycle_ep
                push pinfo.pi_hThread
                callW SuspendThread
                push 0
                push size_loader ;size of loader
                push offset loader ;loader code
                push mhandle ;allocated mem block
                push pinfo.pi_hProcess
                callW WriteProcessMemory
                push mhandle
                pop ctx.context_eip ;eip == my code
                push offset ctx
                push pinfo.pi_hThread
                callW SetThreadContext;set context
                push pinfo.pi_hThread
                callW ResumeThread ;resume thread 
        
  2.4混合hooking的途径
  我们有两种下钩子的方法,但如果要对付壳的话,就只有一种选择了。下钩子的两种方法分别是IAT hooking和APIs hooking.前者不是我们的选择,因为壳可以自动找到或运用GetProcAddress去找到所有的APIs,所以是不实际的。更好的选择是第二种方法,它由在API入口处或结束处储存钩子组成,所以能控制它的输出。
  让我们看一个k32中的API:
  .text:7C809A81 VirtualAlloc proc near
  .text:7C809A81
  .text:7C809A81            mov edi, edi
  .text:7C809A83     push ebp
  .text:7C809A84     mov ebp, esp
  .text:7C809A86     push [ebp+arg_10] ; flProtect
  .text:7C809A89     push [ebp+flProtect] ; flAllocationType
  .text:7C809A8C     push [ebp+flAllocationType] ; dwSize
  .text:7C809A8F     push [ebp+dwSize] ; lpAddress
  .text:7C809A92     push 0FFFFFFFFh ; hProcess
  .text:7C809A94     call VirtualAllocEx
  .text:7C809A99     pop ebp
  .text:7C809A9A     retn 10h
  .text:7C809A9A VirtualAlloc endp
  .text:7C809A9D     db 90h
  .text:7C809A9E     db 90h
  我们就用这种方法去钩住VirtualAlloc,这样一来它就只能调用我们的代码,并分配和释放内存。不同程序在dll中的输出都最终在k32.dll和ntdll.dll中结束并去调用native APIs。所以,如果我们知道将要hooking的是什么,那么也可以调用随后的API.。看一看VirtualAlloc,它将调用VirtualAllocEx,我们可以模仿这些,但没有必要储存和执行原始指令,这会使钩子太冗长。这也会节约很多时间,因为我们没有用Length Disassemble Engine去决定指令的长度。注意,运用Length Disassemble Engine和储存旧的字节并不难办到,但是对于这篇文章没有必要。当我们在ret/retn处hooking时,我们必须用Length Disassemble Engine去定位ret/retn.我hooking kernel32.dll的方法是运行LDE去寻找ret/rent,并查看是否填充nops。(译者注:此方法译者不很明白,所以比较生硬,建议参看原文和以下原代码)
  让我们看一些代码:
                      ;ebx - where to redirect
                      ;edi - pointer to api
                      ;esi - CMD_RETN - hook api at ret/retn
                      ; - CMD_ENTRY - hook api at entry
  CMD_RETN     equ 1
  CMD_ENTRY   equ 2
  hook_    api: pusha
                      lea ecx, [ebp+dummy]
                      push ecx
                      push PAGE_EXECUTE_READWRITE
             push 1000h
                      push edi
                      call [ebp+VirtualProtect]
                      test esi, CMD_ENTRY
                      jz __hook_at_ret
                      mov ecx, edi
                      mov al, 0e9h
                      stosb
                      add ecx, 5
                      sub ebx, ecx
                      mov [edi], ebx
                      jmp __exit_hook
                      ;most APIs are paded with nop, if no nop, fail!!!
  __hook_at_ret:   push edi
                      call ldex86
                      add edi, eax
                      cmp byte ptr[edi], 0c3h
                      je __check_ret
                      cmp byte ptr[edi], 0c2h
                      jne __hook_at_ret
                      cmp word ptr[edi+3], 9090h
                      jne __exit_hook ;failed
                      jmp __hook_api
  __check_ret:   cmp dword ptr[edi+1], 90909090h
                      jne __exit_hook
  __hook_api:  mov ecx, edi
                      mov al, 0e9h
                      stosb
                      add ecx, 5
                      sub ebx, ecx
                      mov [edi], ebx
  __exit_hook:   popa
                      Retn
  就如同你在上面代码中所见,这就是混合的hooking方法,也是这篇文章在这方面所需要的一切。你将会理解,我什么一旦涉及内存管理和被保护程序区段重定位时就用这种方法。
  
  2.5.下面是什么呢? 
   我将给你展示怎样在ring3上写内存管理以及如何为你的目标写非入侵式跟踪。
  
  3.内存管理
  也许你会问在反-反转存中为什么会用到内存管理。其实非常简单,我会用最简单的方式来讲解的。当你用一些保护性壳(如:aspr,armadillo)时,它会分配许多缓冲区,而原来此处的代码已经被转移了。由于这个事实,有时去转存是根本不可能的,在目标程序被解压释放时,这些大量的缓冲区完全是由它自己去分配和释放的。一个简单的方法是去钩住VirtualAlloc,往后是VirtualAllocEx,甚至是ntdll.dll中的NtAllocateVirtualMemory,就可以返回去程序缓冲区,随后也就容易转存了。像这样的保护会重分配所有的数据在我们可能转存的范围内,这种情况下可以用LordPE的一个插件,尽管是我以前写的,但在这种情况下运用是很有魅力的。为了避免太多内存的消耗和对内存的分配和释放的跟踪,我将用一个相似的概念,在Intel CPU中被用来把虚拟内存转换成物理内存。我也可以用堆链表来组织内存管理,但这需要更多的代码。
  
  3.1 扩张程序空间
  假如说程序的大小是9000h字节,我们不能保证任何节区在重定位一些缓冲区时被重写。在这种况下,为了后来转存没有问题,我们需要在目标程序中分配缓冲区。我们将用内存补丁去增加程序的大小。
  - 用CreateProcessA 创建挂起状态的进程
  - 用ReadProcessMemory读整个程序的内存
  - 用 NtUnmapViewOfSection 释放目标程序中被用的内存(译者注:可参见http://undocumented.ntinternals.net/UserMode/Undocumented%20Functions/NT%20Objects/Section/NtUnmapViewOfSection.html)
  - 用 VirtualAllocEx 在目标程序中相同的基址处分配更多的缓冲区
  - 用WriteProcessMemory写回原来的程序
  
  样品代码:
                      push offset pinfo
                      push offset sinfo
                      push 0
                      push 0
                      push CREATE_SUSPENDED
                      push 0
                      push 0
                      push 0
                      push offset progy
                      push 0
                      callW CreateProcessA
                      push PAGE_READWRITE
                      push MEM_COMMIT
                      push c_size
                      push 0
                      push -1
                      callW VirtualAllocEx
                      mov esi, eax
                      push 0
                      push c_size
                      push esi
                      push c_start
                      push pinfo.pi_hProcess
                      callW ReadProcessMemory
                      push c_start
                      push pinfo.pi_hProcess
                      callW NtUnmapViewOfSection
                      mov eax, c_size
                      add eax, NEW_MEM_RANGE
                      push PAGE_EXECUTE_READWRITE
                      push MEM_COMMIT or MEM_RESERVE
                      push eax
                      push c_start
                      push pinfo.pi_hProcess
                      callW VirtualAllocEx
                      push 0
                      push c_size
                      push esi
                      push eax
                      push pinfo.pi_hProcess
                      callW WriteProcessMemory
  c_start - base of progy
  c_size - size of progy
  NEW_MEM_RANGE - increased size of our target
  
  在这以后我们把自定位代码注入到了目标中,它将hook VirtualAlloc 和VirtualFree并返回内存范围而不是新分配程序范围负责。但是真正的问题是如何写这样的内存管理。(译者注:作者是在程序尾添加区段,也可以在程序各区段之间,如果每个区段之间的空间都不够,还可以把自定位代码分开分别加入其中,CIH就是这种方法)
  
  3.2 用VirtualAlloc and VirtualFree管理内存
  我们是否知道新的缓冲区被定位了呢?答案是肯定的,它就在我们原程序的结尾处。为了描述我们缓冲区每一页的状态,我也将用4字节大小的表来描述每页的状态(我将称它为PTE)我们不得不时刻跟踪被VirtualAlloc分配的任何区域,因为一旦调用了VirtualFree我们可以在这页上做还未使用的记号,使得再次调用VirtualAlloc时可以返回。如果没有这些,我们甚至可能益出缓冲区,并导致页失败.首先用一个内存管理结构来描述被分配缓冲区的开始,范围和类型,与堆相似,但后来我会指出这样做太慢而且会导致内存泄漏。这时我们记得Intel CPU是如何把虚拟内存转变为物理内存的。在PDE/PTE中虚拟地址被用作索引,并包括物理结构和每页的状态,所以为什么不采用这样的技术和在ring 3级上做相同的事情和内存管理呢?它的速度很快,用页索引去访问存有每页状态的数据。
  短暂的思考后,我就得到了我想要的,页的入口:(译者注:作者在ring3级上模仿虚拟内存转变为物理内存的方法非常有效,巧妙,请仔细阅读)
  31                      2  1  0
  +---------------------------+---+---+
  | FIRST PAGE INDEX       | R | P |
  +---------------------------+---+---+
  P-当前的位显示,页是否在用或空闲。
  R-保存位,仅仅在VirtualAlloc以MEM_RESERVE被调用。
  FIRST PAGE INDEX又称为FPI-拥有第一页的索引,用来决定块大小。
  
  缓冲区的格局如下::
  +--------------+----------------------------------+--------------+
  | Progy     | VirtualAlloc/Free buffer      |    PTE   |
  +--------------+----------------------------------+--------------+
  你的PTE大小也许是=(buffer/1000h)*4,在我这里,我分配了1000页并用4页去描述每页的状态。
  为了获得PTE,你应该获得每页的索引,而获得它是非常简单。假如我们的程序开始于400000h,结束于500000h,则程序的结尾就是我们缓冲区的开始,PTE是被定位在我们缓冲区的最后4页
           mov esi, [edi+memstart] ;esi=500000h
           add esi, NEW_MEM_RANGE ;esi+1004000h
           sub esi, 4000h ;structs for memory manager(PTEs)
  memstart       = end of progy
  NEW_MEM_RANGE = 1004000h ;1000*1000 pages + 4000h for PTE
  现在我们简单的来获取索引,假定eax是虚拟地址。
            mov edx, eax
            sub edx, [ebp+memstart] ;-500000h
            shr edx, 0ch ;index into edx
  瞧,用简单的[esi+edx*4]就可以看见是否这页被分配了,被保留或可用。当然,这是我的执行,在你的执行中组织可以是不同的PTE, 应该给PTEs分配足量的空间以来满足我们的要求。基本上我们用4000h来描述缓冲区1000000h字节的状态,难道这不美好?你不得不去喜欢Intel和他们把物理内存转换成虚拟内存的思想。当然,你可以分配更小的缓冲区和更小的PTE,那将完全取决于你。
  现在我将告诉你关于FIRST PAGE INDEX以及它为什么这么重要。我随后将给你展示如何在这样的缓冲区中运用nonintrusive tracers,还是先告诉你FPI.FPI用来查看被分配缓冲区的大小和调用VirtualFree时释放缓冲区的大小。FPI含有第一页的索引,并被置于任何一个PTE描述的某个范围。如果FPI是1 in 3 PTEs ,这就意味着这个缓冲区开始于PTE的INDEX 1 ,并且所有的拥有FPI 1 的页都是相同缓冲区的一部分。这将稍后帮助我们编写nonintrusive tracer代码和释放内存,因为有时VirtualFree 被作为 VirtualFree(page_base, 0, MEM_DECOMMIT)来调用,并且如果不知道缓冲区的大小会导致内存泄露。也许你奇怪我为什么不储存第一个PTE的大小并在接着的PTEs上做记号,而是去储存它们每个的FPI。简单的说,FPI将使我们知道nonintrusive tracer中必须改变的内存缓冲区的大小.如果异常在第三页发生,你仅仅只用在第三页改变保护,但是我们想在调用VirtualAlloc时改变整个范围的保护是,这就更有意义了。
  
  看一段代码,我想你会理解
  allocatememory      proc
                      arg virtualbase
                      arg range
                      arg flags
                      arg memprotection
                      local numofpages:dword
                      local dummy_var:dword
                      local virtualaddress:dword
                      call deltaalloc
  deltaalloc:         pop edi
                      sub edi, offset deltaalloc
                      mov esi, [edi+memstart]
                      add esi, NEW_MEM_RANGE
                      sub esi, 4000h ;structs for memory manager(PTEs)
                      mov eax, range
                      mov edx, eax
                      shr eax, 0ch
                      and edx, 0FFFh
                      test edx, edx
                      jz __mm0
                      inc eax
  __mm0:              mov numofpages, eax
                      cmp virtualbase, 0
                      jne __commitpage ;commit reserved pages???? yep
  ;find free block big enough and commit pages
  ;starting from index 1
                      mov ecx, 1
  __cycle_empty:      test dword ptr[esi+ecx*4], 1 ;committed?
                      jnz __next_pte
                      test dword ptr[esi+ecx*4], 2 ;reserved?
                      jz __check_size
  __next_pte:         inc ecx
                      cmp ecx, 1000h
                      jne __cycle_empty
  __check_size:       mov eax, numofpages
                      add eax, ecx
  __cycle_size:       dec eax
                      test dword ptr[esi+eax*4], 1
                      jnz __next_pte
                      test dword ptr[esi+eax*4], 2
                      jnz __next_pte
                      cmp eax, ecx
                      jne __cycle_size
                      ;at this point we have found PTEs large enough to
                      ;describe needed memory buffer
                      mov eax, numofpages ;ecx is index used to get page
                      add eax, ecx
                      mov edx, ecx
                      shl edx, 2 ;FPI
                      mov ebx, flags ;1 for P or 2 for R
  __add_pages:        dec eax
                      mov dword ptr[esi+eax*4], 0 ;set PTE to 0
                      or dword ptr[esi+eax*4], edx;set FPI
                      or dword ptr[esi+eax*4], ebx;set flags
                      cmp eax, ecx
                      jne __add_pages
  __done:             shl ecx, 0ch
                      add ecx, [edi+memstart]
                      mov virtualaddress, ecx
                      jmp __exitalloc
  __commitpage:       push virtualbase
                      pop virtualaddress
                      mov eax, virtualbase
                      sub eax, [edi+memstart]
                      shr eax, 0ch
                      mov ecx, numofpages
                      mov edx, eax
                      shl edx, 2
  __commit_em:        mov dword ptr[esi+eax*4], 0 ;clear pte
                      or dword ptr[esi+eax*4], 3 ;flags
                      or dword ptr[esi+eax*4], edx ;fpi
                      inc eax
                      loop __commit_em
  __exitalloc:        mov eax, virtualaddress
                      leave
                      retn 10h
                      endp
  注意,如何从PTE的1索引开始。
  ; 从索引1开始
            mov ecx, 1
  这非常重要,空的PTE被我的deallocatememory设置为0,随后被分配和释放的区域将会把FPI置零,在这种情况下,寻找FPI是0的内存范围将返回更大的范围。当然,我们可以检验PTE的R和P位,但是这会扩大代码并且使代码的可读性降低。现在,如果你仔细读这个源代码,你将理解在ring 3级上用PTEs写一个漂亮的内存管理代码是多么容易,VirtualFree写起来也很简单。
  
  deallocatememory:
                   mov esi, [ebp+memstart]
                   add esi, NEW_MEM_RANGE
                   sub esi, 4000h ;pointer to PTE
                   ;freeing using index and FPI to find all pages
                   mov edx, eax
                   sub edx, [ebp+memstart]
                   shr edx, 0ch ;index into eax
                   mov ecx, edx ;FPI into ecx
                   mov eax, edx
                   cmp eax, 1000h
                   jnb __exit_free
  __freemem:       mov edx, [esi+eax*4]
                   shr edx, 2
                   cmp edx, ecx ;FPI...
                   jne __exit_free
                   mov dword ptr[esi+eax*4], 0 ;clear PTE
                   inc eax
                   jmp __freemem
  __exit_free:     retn
  以上就是我要告诉你关于内存管理的一切.
  
  3.3 用Deiphi 编码的问题
  在这种情况下,理解Delphi 是非常重要的。ASProtect SKE的virtual.dll是用Delphi写的,这是一个大的问题。我在玩弄了一会儿ASProtect 和其他Delphi apps后,我的代码都失败了,甚至我用一切正确的方式去模仿。两个小时的跟踪后我就能够确定这样的问题了,这些就在下面。
  - @System@@FreeMem
  - @System@SysFreeMem
  为了让这个引擎工作,程序必须成功返回0,否则Delphi app会退出 。甚至我模仿任何事情,他都无故失败,所以唯一的方法是用以下两句程序去修补aspr virtual.dll或其他Delphi程序。
  ----------
  mov eax, 0
  retn
  接下来的问题是如何定位这两句程序?此时运用签名的方法在我脑海中闪过。立刻调用VirtualAlloc,它将为aspr virtual.dll分配地址,所以可以储存地址,也可以插入非入侵调试,一旦我们下int3h断点时就可以获得。我们也就知道了扫描signatures的最佳时机。
  更简的方法是在virtual.dll的入口处转存和在IDA中扫描这两句程序的地址。那将非常的方便和简单
  .dumped:00496564 ; __fastcall System::__linkproc__ FreeMem(void)
  .dumped:00496564 @System@@FreeMem$qqrv proc near
  .dumped:00496564
  .dumped:00496564     test eax, eax
  .dumped:00496566     jz short locret_496572
  .dumped:00496568     call ds:off_4CE01C
  .dumped:0049656E     or eax, eax
  .dumped:00496570     jnz short loc_496573
  .dumped:00496572
  .dumped:00496572 locret_496572:
  .dumped:00496572     retn
  .dumped:00496573
  .dumped:00496573 loc_496573:
  .dumped:00496573     mov al, 2
  .dumped:00496575     jmp sub_4965CC
  .dumped:00496575 @System@@FreeMem$qqrv endp
  And also:
  .dumped:00497114 ; __fastcall System::SysFreeMem(void *)
  .dumped:00497114 @System@SysFreeMem$qqrpv proc
  .dumped:00497114
  .dumped:00497114     var_4 = dword ptr -4
  .dumped:00497114
  .dumped:00497114     push ebp
  .dumped:00497115     mov ebp, esp
  .dumped:00497117     push ecx
  .dumped:00497118     push ebx
  .dumped:00497119     push esi
  .dumped:0049711A     push edi
  .dumped:0049711B     mov ebx, eax
  .dumped:0049711D     xor eax, eax
  .dumped:0049711F     mov ds:dword_4D042C, eax
  .dumped:00497124     cmp ds:byte_4D0428, 0
  .dumped:0049712B     jnz short loc_49714C
  .dumped:0049712D     call @System@_16436 ; System::_16436
  .dumped:00497132     test al, al
  .dumped:00497134     jnz short loc_49714C
  .dumped:00497136     mov ds:dword_4D042C, 8
  .dumped:00497140     mov [ebp+var_4], 8
  .dumped:00497147     jmp loc_4972AD
  .dumped:0049714C
  
  3.4 内存管理总结
  如果你已理解上面的一切,那么试图反转存将会失败。你可以在任何一个转存范围内得到所有的东西。当然,为了能在LordPE中看见整个程序范围,你必须确定增加了PEB的大小。
                               mov eax, dword ptr fs:[30h]
                               mov eax, [eax+0ch]
                               mov eax, [eax+14h]
                               add dword ptr[eax+18h], NEW_MEM_RANGE
  这就是所有在ring3上关于内存管理的知识 。我希望你获得其中的思想,并且能自己写出在你的目标中的内存管理。祝你好运。
  
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!

                                                       2006年05月18日 2:22:02 

你可能感兴趣的:(c,exception,api,System,Delphi,hook)