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
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