上一课我们做出了最简单的一个Loader,这次我们就要做一个最复杂的patch了,不单代码比上次多多了,还要混合汇编~~~
“模版”的数据在这咱就不多说了,数据依然是那些个数据,节区依然是那个.sdata,要看的到上一课看哈
在这里我们要实现的patch,也就是文件补丁----给PE文件附加上执行代码
在这个“模版”patch中,我实现了4种补丁类型,分别是:
#define ADD_LAST_SECTION 1 //添加代码到最后一个区段
#define ADD_NEW_SECTION 2 //添加代码到一个新建的区段
#define ADD_TO_HEADER 3 //添加代码到PE头部
//除了以上3种外还有一种是寻找已存在区段空闲处并插入代码,这种插入PE头的修改更简单,只是比较容易失败,这里就不实现了
//这里再加一种字节补丁,针对一些简单的程序
#define BYTE_PATCH 4
由于PE文件对齐后存在的间隙给我们的补丁代码带来了生存空间,我们的目标就是想法设法让我们的补丁代码进入目标程序,让目标程序一运行就自动给我们补丁,我们暂且不管补丁代码如何补丁,现在的目标就是把代码加入PE文件并让他很好的运行起来,所以一开始并不需要补丁的实现,我们可以把补丁编写为弹出一个对话框来作为测试
现在假设存在以下2个变量
Appendcode_Start; //附加代码段起始处
Appendcode_End; //附加代码段结束处
于是我们的补丁代码起始地址为&Appencode_Start,大小为(&Appencode_End-&Appencode_Start)
有了这2个变量,我们就可以专注于如何添加代码到PE中了
首先我们实现第一种类型的代码添加---添加到最后一个区段
对于了解PE的朋友们都会知道,PE中的数据是分段存储的,而在节表中记录了这些区段的信息,而最后一个区段比较特殊,如果我们构造的好的话在能够加载的情况下应该可以无限制的添加代码,别的先不说,先来个添加代码后的整体结构大家应该就会比较清楚了(下面的东西修改了几次都对不齐,不知道怎么整,就这样将就吧= = )
//_________________
//| |
//| PE头 | 第一部分
//| |
//|________________|
//|_______节表______|
//| |
//|_______对齐______|
//| |
//| |
//. .
//. 各区段 . 第二部分
//. .
//| |
//|________________|
//.___对齐后间隙______.
//! !
//!____附加代码______! 第三部分
//!____最终对齐______!
被补丁后的整个PE文件结构如上,其中第一第二部分是整个文件原始情况
加上第三部分后就是我们的目标文件了至于具体的如何加上去,用代码说话吧!
HANDLE hFile ;//文件句柄
HANDLE hMap; //映射文件句柄
//////////////////////////////添加代码到最后一个区块/////////////////////////////////////
BOOL AddToLastSection(PBYTE lpMemory, DWORD dwFileSize)
{
PIMAGE_NT_HEADERS lpNtHeaders;
PIMAGE_SECTION_HEADER lpSectionHeader;
PIMAGE_SECTION_HEADER lpLastSectionHeader;
DWORD dwNewFileSize; //最终文件大小
DWORD dwFileAlignSize; //原文件对齐后大小
DWORD dwLastSectionAlignSize; //最后区段内存对齐后大小
DWORD dwPatchSize; //补丁大小
DWORD dwFileAlign; //文件对齐粒度
DWORD dwSectionAlign; //内存对齐粒度
//DWORD dwLastSectionSize;
//DWORD dwPatchStart; //指定补丁要复制到的文件偏移起始
PBYTE lpNewFile; //最终文件缓存
DWORD dwSectionNum;
lpNtHeaders = (PIMAGE_NT_HEADERS)( lpMemory + ((PIMAGE_DOS_HEADER)lpMemory)->e_lfanew );
lpSectionHeader = (PIMAGE_SECTION_HEADER)(lpNtHeaders + 1);
dwSectionNum = lpNtHeaders->FileHeader.NumberOfSections ;
lpLastSectionHeader = lpSectionHeader + dwSectionNum - 1;
dwFileAlign = lpNtHeaders->OptionalHeader.FileAlignment;
dwSectionAlign = lpNtHeaders->OptionalHeader.SectionAlignment;
dwFileAlignSize = Align(dwFileSize, dwFileAlign); //求原文件对齐大小
dwPatchSize = ((DWORD)&Appendcode_End ) - ( (DWORD)&Appendcode_Start ); //获得补丁代码大小
dwNewFileSize = Align(dwFileAlignSize + dwPatchSize, dwFileAlign); //获得最终文件对齐后大小
dwLastSectionAlignSize = Align(lpLastSectionHeader->Misc.VirtualSize + dwPatchSize, dwSectionAlign); //获得内存中最后区段大小
lpNewFile = (PBYTE)VirtualAlloc (NULL, dwNewFileSize, MEM_COMMIT, PAGE_READWRITE);
if ( !lpNewFile ) //分配内存失败
return FALSE;
//复制原文件数据
memset(lpNewFile, 0, dwNewFileSize);
memcpy(lpNewFile, lpMemory, dwFileSize);
//复制完毕,关闭映射文件和句柄 (不关闭后面就无法创建新文件,本来想在AddCode关闭,结果试了抛异常什么的还是没用,只能hMap设置成全局变量然后在这关了,实在不雅观啊‘_’)
UnmapViewOfFile(lpMemory);
CloseHandle(hMap);
CloseHandle(hFile);
//复制补丁代码前先转储补丁数据
PBYTE pBuffer = (PBYTE)(&Patch_Data); //指向附加代码补丁数据处
//(*(DWORD*)pBuffer) = dwPatchNum;
memcpy(pBuffer + 4, dwPatchAddress, 16*sizeof(DWORD) );
pBuffer += 4 + 16*sizeof(DWORD);
memcpy(pBuffer, byOldData, 16);
memcpy(pBuffer+16, byNewData, 16);
//复制补丁代码
memcpy(lpNewFile + dwFileAlignSize, &Appendcode_Start, dwPatchSize);
//修正PE头数据
PIMAGE_NT_HEADERS lpNewNtHeaders;
PIMAGE_SECTION_HEADER lpNewSectionHeader;
PIMAGE_SECTION_HEADER lpNewLastSection;
DWORD* lpNewEntry; //指向新入口处
DWORD OldEntry;
lpNewNtHeaders = (PIMAGE_NT_HEADERS)( lpNewFile + ((PIMAGE_DOS_HEADER)lpNewFile)->e_lfanew );
lpNewSectionHeader = (PIMAGE_SECTION_HEADER)(lpNewNtHeaders + 1);
lpNewLastSection = lpNewSectionHeader + dwSectionNum - 1;
//给最后区段添加读写执行属性
lpNewLastSection->Characteristics |= 0xC0000020;
//修正最后一个区段的偏移量
lpNewLastSection->SizeOfRawData = dwNewFileSize - lpNewLastSection->PointerToRawData;
lpNewLastSection->Misc.VirtualSize = Align( GetValidSize(lpNewFile, lpNewLastSection), dwSectionAlign);//Align(lpNewLastSection->Misc.VirtualSize + dwPatchSize, dwSectionAlign) ;
//修正镜像大小
lpNewNtHeaders->OptionalHeader.SizeOfImage = Align(lpNewLastSection->VirtualAddress + lpNewLastSection->Misc.VirtualSize, dwSectionAlign);
//修正入口地址
OldEntry = lpNewNtHeaders->OptionalHeader.AddressOfEntryPoint;
lpNewNtHeaders->OptionalHeader.AddressOfEntryPoint = OffsetToRVA( (IMAGE_DOS_HEADER *)lpNewFile, dwFileAlignSize) ;
//修正补丁代码跳回OEP的参数
lpNewEntry = (DWORD*)(lpNewFile + dwFileAlignSize + dwPatchSize - 5);
*lpNewEntry = OldEntry - (lpNewNtHeaders->OptionalHeader.AddressOfEntryPoint + dwPatchSize - 1);
//补丁完毕,写回文件
HANDLE hNewFile;
DWORD dwRead;
if (INVALID_HANDLE_VALUE == ( hNewFile = CreateFile (szFileName, GENERIC_READ | GENERIC_WRITE , FILE_SHARE_READ , NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_ARCHIVE, NULL) ) )
return FALSE;
WriteFile(hNewFile, lpNewFile, dwNewFileSize, &dwRead, NULL);
CloseHandle(hNewFile);
return TRUE;
}
//这里再附上调用添加代码函数的主要函数,保证完整性
BOOL AddCode( )
{
DWORD dwFileSize; //文件大小
PBYTE lpMemory; //内存映射指针
if (INVALID_HANDLE_VALUE != ( hFile = CreateFile (szFileName, GENERIC_READ , FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL) ) )
{
dwFileSize = GetFileSize (hFile, NULL);
if (dwFileSize)
{
hMap = CreateFileMapping (hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if (hMap)
{
lpMemory = (BYTE *)MapViewOfFile (hMap, FILE_MAP_READ, 0, 0, 0);
if (lpMemory)
{
//使用指定方法打补丁
switch(dwTypeOfPatch)
{
case ADD_LAST_SECTION:
if (!AddToLastSection(lpMemory, dwFileSize) )
return FALSE;
break;
case ADD_NEW_SECTION:
if (!AddToNewSection(lpMemory, dwFileSize) )
return FALSE;
break;
case ADD_TO_HEADER:
if (!AddToHeaderSection(lpMemory, dwFileSize) )
return FALSE;
break;
case BYTE_PATCH:
if (!BytePatch(lpMemory, dwFileSize))
return FALSE;
break;
}
return TRUE;
}
else
MessageBox (GetActiveWindow() , TEXT("目标文件打开失败"), NULL, MB_OK);
}
else
MessageBox ( GetActiveWindow() , TEXT("目标文件打开失败"), NULL, MB_OK);
}
}
else
MessageBox (GetActiveWindow() , TEXT("目标文件打开失败"), NULL, MB_OK);
return FALSE;
}
上面的注释应该写的比较清楚了,这样我们就完成了一个添加代码的方法,第二种方法是给PE增加一个新区段,这种方法需要PE头至少有容纳一个节表(20个字节)的空闲容量,不过由于PE头也可以增加大小(最大0x1000字节),所以这种方法基本也能成功,只是增大了PE头的话就需要调整所有节表的文件偏移,要麻烦一些具体实现就不贴出了,可以去看附件的代码
到这里,整个patch的添加代码部分就实现了,下面就是具体编写实现补丁的代码,这个代码用汇编编写,涉及到自定位,动态加载等技术,在贴出代码前有些东西要提下,由于用mov eax,[esp]的方法获得kernel32.dll内部地址然后向前搜索kernel32.dll基地址的方法虽然稳定,但有局限性,比如用这种方法只能给目标文件实现一次补丁,如果继续添加补丁之前的补丁的mov eax,[esp]就会无效,等于说这种方法只能给目标文件添加一次补丁代码,而且代码量相对来说比较大。
所有我这里用的是从系统PEB结构获得基地址,仅需要以下几行代码即可
assume fs:nothing
mov eax,fs:[30h]
mov eax,[eax+0ch]
mov esi,[eax+1ch]
lodsd
mov eax,[eax+8]
只需要上面几行代码就可以获得kernel32基地址,具体涉及到TEB,PEB,PEB_LDR_DATA结构,这种方法在XP上能很好的工作。但是事实上用这种方法在WIN7下获得的是kernelbase.dll的基址而不是kernel32。还有一种,通过SEH框架最后一个异常处理地址,这个根据WINPE权威指南上说是kernel32.dll中,所以可以从此入手。而经测试后在WIN7中这个地址也换了,已经是位于ntdll.dll中的地址。
嗯,看似无路可走,其实PEB的方法还是可行的,在WIN7下查询kernelbase.dll导出表,发现其中虽然没有了LoadLibrary函数,但是有LoadLibraryEx函数,GetProcAddress函数也有,有这2个函数就够了,我们用PEB的动态加载就能够实行,注意是LoadLibraryEx函数哦!比LoadLibrary多了2个参数,调用时需要注意!
还有一个问题就是如何将前面的C代码和汇编代码结合在一起编译,首先就要导出某个变量的符号供其他代码使用,这个问题存在的原因主要是链接时外部符号无法识别,究其根本就是调用约定和名称修饰,具体可以参考C/C++名称修饰,加密解密的第16章也有讲到这个混合编译时的设置等,可以自行查找
讲了这么多,下面就贴出代码落~~
include c:\masm32\include\windows.inc
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 导出变量供补丁工具使用
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;导出的变量
PUBLIC Appendcode_Start ;附加代码起始处
PUBLIC Appendcode_End ;附加代码结束处
PUBLIC Patch_Data ;补丁数据处
.code
Appendcode_Start LABEL DWORD
jmp _NewEntry
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;重要的函数名,为兼容WIN7 kernelbase.dll,使用LoadLibraryExA函数
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
szLoadLibraryExA db 'LoadLibraryExA',0
szGetProcAddress db 'GetProcAddress',0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;SEH错误Handler,用于在错误中回复并跳转到安全位置
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_SEHHandler proc _lpExceptionRecord,_lpSEH,_lpContext,_lpDispatchertext
pushad
mov esi,_lpExceptionRecord
assume esi:ptr EXCEPTIONRECORD
mov edi,_lpContext
assume edi:ptr CONTEXT
mov eax,_lpSEH
push [eax+0ch]
pop [edi].regEbp
push [eax+08]
pop [edi].regEip
push eax
pop [edi].regEsp
assume edi:nothing,esi:nothing
popad
mov eax,ExceptionContinueExecution
ret
_SEHHandler endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;用PEB获取基址的方法,WIN7中获得的实际是kernelbase.dll的基地址
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_GetKernel32Base proc
local @dwRet
pushad
assume fs:nothing
mov eax,fs:[30h] ;获取PEB所在地址
mov eax,[eax+0ch] ;获取PEB_LDR_DATA 结构指针
mov esi,[eax+1ch] ;获取InInitializationOrderModuleList 链表头
;第一个LDR_MODULE节点InInitializationOrderModuleList成员的指针
lodsd ;获取双向链表当前节点后继的指针
mov eax,[eax+8] ;获取kernel32.dll的基地址(WIN7中是kernelbase.dll基址)
mov @dwRet,eax
popad
mov eax,@dwRet
ret
_GetKernel32Base endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;查找导出表获取指定API地址
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_GetApi proc _hModule,_lpszApi
local @dwReturn,@dwSize
pushad
call @F
@@:
pop ebx
sub ebx,@B
assume fs:nothing
push ebp
push [ebx+offset error]
push [ebx+offset _SEHHandler]
push fs:[0]
mov fs:[0],esp
mov edi,_lpszApi
mov ecx,-1
xor eax,eax
cld
repnz scasb
sub edi,_lpszApi
mov @dwSize,edi
mov esi,_hModule
add esi,[esi+3ch]
assume esi:ptr IMAGE_NT_HEADERS
mov esi,[esi].OptionalHeader.DataDirectory.VirtualAddress
add esi,_hModule
assume esi:ptr IMAGE_EXPORT_DIRECTORY
mov ebx,[esi].AddressOfNames
add ebx,_hModule
xor edx,edx
.while edx < [esi].NumberOfNames
push esi
mov edi,[ebx]
add edi,_hModule
mov esi,_lpszApi
mov ecx,@dwSize
cld
repz cmpsb ;搜索指定API的字符串
.if !ecx
pop esi
jmp @F ;成功
.endif
next:
pop esi
inc edx
add ebx,4
.endw
jmp error
@@:
sub ebx,[esi].AddressOfNames
sub ebx,_hModule ;获得偏移
shr ebx,1 ;由于索引数组是WORD数组,所以右移一位
add ebx,[esi].AddressOfNameOrdinals
add ebx,_hModule
movzx eax,word ptr [ebx]
shl eax,2 ;将取得的索引左移2位获得字节偏移
add eax,[esi].AddressOfFunctions
add eax,_hModule
mov eax,[eax] ;取得目标API地址
add eax,_hModule
mov @dwReturn,eax
error:
pop fs:[0]
add esp,0ch
assume esi:nothing
popad
mov eax,@dwReturn
ret
_GetApi endp
;补丁所需要的函数和全局变量
szCreateThread db 'CreateThread',0
szGetTickCount db 'GetTickCount',0
szVirtualProtect db 'VirtualProtect',0
lpGetTickCount dd 0
StartCount dd 0
;以下变量需要补丁程序修正
Patch_Data LABEL DWORD
;dwTypeOfPatch dd 0 ;指示补丁类型
dwPatchNum dd 0 ;补丁数量
dwPatchAddress dd 16 dup(0) ;补丁地址
byOldData db 16 dup(0) ;补丁处旧数据和新数据
byNewData db 16 dup(0)
;这个线程是实现补丁的部分,循环检测补丁地址的数据并补丁,还加入了一个5分钟的超时检测
_Thread proc _lpVirtualProtect
local @lpGetTickCount,@temp,@StartCount,@num
pushad
call @F
@@:
pop ebx
sub ebx,@B
mov edx,dword ptr [ebx+offset lpGetTickCount]
mov @lpGetTickCount,edx
mov edx,dword ptr [ebx+offset StartCount]
mov @StartCount,edx
mov ecx,dword ptr [ebx+offset dwPatchNum]
mov @num,ecx
.while TRUE
call @lpGetTickCount
sub eax,@StartCount
cmp eax,493e0h ;大于五分钟则超时退出线程
jg _exit
;开始检测补丁地址
lea esi,dword ptr [ebx+offset dwPatchAddress] ;指向补丁地址
lea edi,dword ptr [ebx+offset byOldData] ;补丁处旧数据
lea edx,dword ptr [ebx+offset byNewData] ;补丁处新数据
;检测所有补丁处字节
mov ecx,dword ptr [ebx+offset dwPatchNum]
_peek:
push ecx
mov ecx,dword ptr [esi]
xor eax,eax
mov al,byte ptr [ecx] ;取补丁处数据
cmp al,byte ptr [edi] ;补丁处是否解码
jne _mismatch
;更改页面为读写执行,以确保补丁地址处拥有读写执行权限
pushad
lea eax,@temp
push eax
push 40h
push 100h
push ecx
call _lpVirtualProtect
popad
mov al,byte ptr [edx] ;进行补丁
mov byte ptr [ecx],al
dec @num
_mismatch:
inc edi
inc edx
add esi,4
pop ecx
cmp @num,0
je _exit
loop _peek
.endw
_exit:
popad
ret
_Thread endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;补丁功能部分
;_dwKernelBase: kernel32.dll基址
;_lpGetProcAddress: GetProcAddress地址
;_lpLoadLibraryExA LoadLibraryExA地址
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;这里的作用是获得一些必须的函数,然后创建补丁线程
_Patch proc _dwKernelBase,_lpGetProcAddress,_lpLoadLibraryExA
local @lpVirtualProtect
local @temp
pushad
lea edx,dword ptr [ebx+offset szVirtualProtect]
push edx
push _dwKernelBase
call _lpGetProcAddress
cmp eax,0
je _exit
mov @lpVirtualProtect,eax
lea edx,@temp
push edx
push 40h
push 1000h
lea edx,dword ptr [ebx+offset lpGetTickCount]
push edx
call @lpVirtualProtect ;确保这个附加代码处全局变量位置可写
lea edx,dword ptr [ebx+offset szGetTickCount]
push edx
push _dwKernelBase
call _lpGetProcAddress
.if eax
mov dword ptr [ebx+offset lpGetTickCount],eax
call eax
mov dword ptr [ebx+offset StartCount],eax
lea edx,dword ptr [ebx+offset szCreateThread]
push edx
push _dwKernelBase
call _lpGetProcAddress
.if eax
lea edx,@temp
push edx
push 0
push @lpVirtualProtect ;线程参数为VirtualProtect函数的地址
lea edx,dword ptr [ebx+offset _Thread]
push edx
push 0
push 0
call eax ;创建监测线程进行补丁
.endif
.endif
_exit:
popad
ret
_Patch endp
;从导入表获得GetProcAddress函数和LoadLibraryExA函数地址
_start proc
local @dwKernel32Base
local @lpGetProcAddress,@lpLoadLibraryExA
pushad
call _GetKernel32Base
.if eax
mov @dwKernel32Base,eax
lea edx,dword ptr [ebx+offset szGetProcAddress]
push edx
push eax
call _GetApi
mov @lpGetProcAddress,eax
.endif
.if @lpGetProcAddress
lea edx,dword ptr [ebx+offset szLoadLibraryExA]
push edx
push @dwKernel32Base
call @lpGetProcAddress
.if eax
mov @lpLoadLibraryExA,eax
push eax
push @lpGetProcAddress
push @dwKernel32Base
call _Patch
.endif
.endif
popad
xor eax,eax
ret
_start endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;PE文件新入口
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_NewEntry:
call @F
@@:
pop ebx
sub ebx,@B
call _start
;ret
jmpToStart db 0E9h,0F0h,0FFh,0ffh,0ffh ;需要补丁程序修正,放在这个位置只要在&Appencode-5处赋值就可以修正了,比较方便
ret
Appendcode_End LABEL DWORD
end
经过一系列代码运行测试后最终才使用上面的补丁代码,然后再在C代码中对汇编代码中需要修正的数据进行修正,测试运行,其实由于是文件补丁,我们还得考虑重复补丁的情况~
所以就在patch中加了个CRC32验证,验证失败就认为可能损坏或已经补丁,详细的可以下载附件。做完这些工作后,我们的patch"模版"就完工了,虽然也不是最终版本,最终的修正都放到最后一篇讲,因为我也是在最后工具写完测试才发现的问题!
第二篇出了,第一篇都没什么人反应啊(沮丧中...)
,第三篇尽量早点发吧
下面传上patch的代码,整个工程的在第一篇附件中有
Patch.rar