利用pe添加节的方法添加代码 实现简单的加壳
大致流程如下
要达到的目的是 添加一个新节 在新节中添加自己的代码 让程序运行时 先运行自己的代码
而自己的代码就是为了解开之前对程序进行的加密执行完后 再继续运行原程序的路线。
详细步骤
第一步
先写程序将 采用文件映射的方法将待修改的exe加载进来 获取所有我们需要的信息
CreateFile CreateFileMapping MapViewOfFile(返回 文件基地址 Pimage)
GetFileSize(用于修改文件大小)
第二步
初始化pe头信息 DOS=PIMAGE_DOS_HEADER(Pimage); 其他初始化略
验证pe的有效性 MZ 和 PE
第三步
开始加节 因为后边要修改eop所在的节的内容
所以先将此节的属性设置为 可读可写 IMAGE_SCN_MEM_READ|IMAGE_SCN_MEM_WRITE
(此处很关键 自己修改的事飞秋 结果修改完后一直不能运行 卡了很长时间 切记)
加节
1
找到最后一节的地址
PIMAGE_SECTION_HEADER lastsec = SECTION+(FILE->NumberOfSections-1);
确定新加节的地址
PIMAGE_SECTION_HEADER newsec = lastsec+1;
节表数目加1
FILE->NumberOfSections++;
验证一下 在节表头最后 到 第一个节内容开始 有没有40个字节
(一般都有 没有考虑不够的情况)
验证方法(主要明白SizeOfHeaders的真实含义)
NEWSIZE 计算的是 (dos + dos stub ) + nt + 新加节后的节表头总大小(纯粹大小 未对齐)
DWORD NEWSIZE = (DOS->e_lfanew)+sizeof(IMAGE_OPTIONAL_HEADER32)+sizeof(IMAGE_SECTION_HEADER)*NumberOfSections;
(不要忘记 dos stub 直接用(sizeof(IMAGE_DOS_HEADER))尽管没有神马影响 但是事实 必须这样计算)
让它跟原SizeOfHeaders比较
SizeOfHeaders是 dos+dos stub+nt+所有节表头总大小(对齐后的) 可以用作第一个节内容的开始位置
2
大小满足之后 对新加节的所有属性进行初始化(UP函数是 用来对齐的 参数 1 大小 2 对齐粒度)
memcpy(newsec->Name,".NewSec",8);
newsec->Misc.VirtualSize=实际大小(size); 节内容的实际大小 SizeOfRawData 用到
newsec->SizeOfRawData=Up(size,pe.OPTION->FileAlignment); 文件中的大小
newsec->VirtualAddress 在内存中的RVA 重要 可以利用上一个节的数据得到
DWORD last =Up((lastsec->SizeOfRawData),pe.OPTION->SectionAlignment)+lastsec->VirtualAddress;
DWORD last =Up((LastSec->Misc.VirtualSize),pe.OPTION->SectionAlignment)+lastsec->VirtualAddress;
两个last 相等 因为 SizeOfRawData 是Misc.VirtualSize按文件对齐得到的
文件对齐粒度512 内存对齐粒度4096 所以肯定相等
不过为了准确 采用后者
其实这个last 也就是 pe文件 载入内存后 经过SectionAlignment对齐后 的 总大小 SizeOfImage
newsec->PointerToRawData=lastsec->PointerToRawData+lastsec->SizeOfRawData; 同理
newsec->Characteristics=IMAGE_SCN_MEM_READ|IMAGE_SCN_MEM_WRITE; 可读可写
其他属性没有什么影响 故均设置为0 到此 初始化完毕
3
需要修改的其他地方的参数
a (必须)文件在磁盘上的总大小 即第一步中的GetFileSize返回的值 再加上一页的大小4096
用于CreateFileMapping创建文件映射对象函数 把大小设置为修改后的大小 即 加上4096后的值
b (必须)文件映射内存后的总大小
OPTION->SizeOfImage+=Up(size,pe.OPTION->SectionAlignment);
c (不必须的)
pe.DATA[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].Size=0;
pe.DATA[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].VirtualAddress=0;
pe.OPTION->SizeOfCode+=Up(size,pe.OPTION->SectionAlignment);
pe.OPTION->SizeOfInitializedData+=Up(size,pe.OPTION->SectionAlignment);
4 至此加节成功
第四步
取反
因为是对 eop所在节的内容取反 所以要找到 eop 所在节
方法 获取eop= OPTION->AddressOfEntryPoint 然后循环遍历各个节表头的起始地址 进行比较
for (int i=0; i<FILE->NumberOfSections; i++)
{
PIMAGE_SECTION_HEADER sec = PIMAGE_SECTION_HEADER(SECTION+i);
if (eop>=sec->VirtualAddress && eop<=(sec->VirtualAddress+sec->SizeOfRawData))
{return sec;}
}
找到所在节后 是要对在磁盘上此节的内容 进行修改 所以获取
PVOID address = sec->PointerToRawData+image; //文件的地址
DWORD lenth = sec->SizeOfRawData; //文件的大小
取反函数
void _stdcall QF(PVOID address,DWORD len)
{
DWORD i=0;
PBYTE buf=(PBYTE)address;
for ( i=0; i<len; i++)
{buf[i]=~buf[i];}
}
函数中有一个关键字 _stdcall 作用是 函数执行完后 自己跳转到之前压栈的地址 用于返回eop使用
好了 取反结束了 此时 磁盘上的 exe文件已经被 破坏 接下来是 解密
第五步
解密 比较复杂
首先说一下思路
解密其实是 exe在执行后 先执行自己的解密算法 在跳转回去 继续自己的程序
所以解密 就是当exe文件执行时 先执行自己的代码 即将已取反加密的节再取反 即可正常运行
记住是当exe文件执行的时候所以 需要获得当 exe文件加载进去后的 内存地址才行(重要)
并不是说将磁盘上的文件再改回来 这样想是错的 再怎么说文件执行了 再对它进行修改 肯定是不行的
获得了待解密的节首地址 和 大小 此时解密函数参数已经解决了 接下来要做的 是让程序运行的时候 先执行自己的代码
也就是先执行自己的这个解密算法函数 一个函数运行的步骤是 先将参数从右到左依次入栈 再将代码入栈 而我们还需要
执行完后 再跳转回原eop入口地址 所以再将原eop入口地址入栈 待函数执行完后 返回到这个地址 怎么返回的? 这就用到
关键字 _stdcall 它的作用是 由被调的函数清除堆栈 它的反汇编是 ret 8 ;两个字节的大小
(在这个程序中不用它也可以 用是最安全 省心的 至于原因 强哥解释了 我没太搞明白)
接下来就开始copy应该copy的内容到新加节的内容中
参数入栈 需要自己写机器语言(因为是执行自己的函数所以自己调) 利用结构体 形成语句 然后再copy到新加节的开始
当执行新加节的内容时 先执行自己做的这个解密函数(参数入栈 原eop入栈 代码入栈 ret 8)
1 获得copy的首地址 即 新加节的文件地址
PBYTE begin=(RVATORAW(newsec->VirtualAddress)+image);
再将 参数入栈的机器码构成结构体 直接copy机器码到新加节中
2 从右到左 copy参数 即先copy 节的长度length
先对之前自己做的结构体进行初始化
MOV_EBX.address=secofeop->SizeOfRawData; //所在节的length
MOV_EAX.address=secofeop->VirtualAddress+pe.OPTION->ImageBase; //所在节的起始地址 va+400000
PUSH_OLD_EOP.address=pe.OPTION->AddressOfEntryPoint+pe.OPTION->ImageBase; //原eop+400000
再copy到新加节的内容中
memcpy(begin,&MOV_EBX,sizeof(mov_eax)); //所在节的length
memcpy(begin+sizeof(mov_eax),&MOV_EAX,sizeof(mov_eax)); //所在节的起始地址 va+400000
memcpy(begin+sizeof(mov_eax)*2,&PUSH_OLD_EOP,sizeof(push_old_eop)); //原eop+400000
DWORD lenofcode=(DWORD)end_qf-(DWORD)start_qf;
//其中 start_qf 和 end_qf 是取反函数的首地址和末地址 用来计算代码的长度 注意最后还得加上末地址后边的几个字节(重要)
//两个数值是自己写好程序后 调试程序时 手动找出来的 不知道怎么动态获得 待大牛看后 指点迷津
//计算一下代码需要的内存长度 再加上至少有返回的指令的长度 此时即为2
//多了也没事 自动填充为 CC
memcpy(begin+sizeof(mov_eax)*2+sizeof(push_old_eop),(PVOID)start_qf,lenofcode+2); //
重要一步 修改eop入口点地址为新加节的rva
OPTION->AddressOfEntryPoint=newsec->VirtualAddress; //修改入口点地址到新加节的rva
此时解密也结束了
第六步
收尾工作
首先了解一点知识 摘在网络
为了提高速度,系统将文件的数据页面进行高速缓存,并且在对文件的映射视图进行操作时不立即更新文件的磁盘映像。如果需要确保你的更新被写入磁盘,可以强制系统将修改过的数据的一部分或全部重新写入磁盘映像中,方法是调用F l u s h Vi e w O f F i l e函数:
BOOL FlushViewOfFile(
PVOID pvAddress,
SIZE_T dwNumberOfBytesToFlush);
第一个参数是包含在内存映射文件中的视图的一个字节的地址。该函数将你在这里传递的地址圆整为一个页面边界值。第二个参数用于指明你想要刷新的字节数。系统将把这个数字向上圆整,使得字节总数是页面的整数。
1 "对文件的映射视图进行操作时不立即更新文件的磁盘映像" 那么何时更新?我程序正常退出前一定会更新吧?如果程序意外结束(比如电脑死机)那是不是就可能无法将更改写入磁盘?
1、在UnmapViewOfFile、CloseHandle和系统回收物理内存的时候写入磁盘。
当进程结束时(包括正常和异常),系统会自动关闭该进程打开的所有Handle,所以会写入磁盘。除非是内核代码异常,导致死机,这时才可能没有写入。
2 是不是只要程序不意外结束,我们就没使用FlushViewOfFile的必要?否则请问在什么情况下有必要使用它?
2、FlushViewOfFile是为了实现程序自己控制写入磁盘而提供的,当你真正遇到这种需求的时候才能体会到它的价值。
本程序实现的代码如下
BOOL success = FlushViewOfFile(Pimage,FileSize); //将写入文件映射缓冲区的所有数据都刷新到磁盘
if (!success)
{return false;}
success = UnmapViewOfFile(Pimage); //在当前应用程序的内存地址空间解除对一个文件映射对象的映射
//lpBaseAddress Long,指定要解除映射的一个文件映射的基准地址。这个地址是早先用MapViewOfFile函数获得的
CloseHandle(hMap);
return true;