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
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
以上就是我要告诉你关于内存管理的一切.
The next problem is how to locate those two procedures? Using signatures is the only thing that crosses my mind. second call to VirtualAlloc will allocate buffer for aspr virtual.dll so store that address and insert non instrusive debugger that will gain control once we hit int3h - push/ret to aspr virtual.dll and we know when is good time to start scanning for our signatures. Easier solution would be to dump file at entry of virtual.dll and to scan in IDA for addresses of this two procs. That would be very nice and easy locating w/o single chance to fail Anyway here are two procedures from IDA and dumped virtual.dll (note that these are present in all Delphi apps): 接下来的问题是如何定位这两个步骤?此时运用签名的方法在我脑海中闪过。