PE文件操作-简单的加壳实现
来源:互联网 发布:淘宝助理5.0旧版本 编辑:程序博客网 时间:2024/05/17 02:22
处于一些原因,需要对PE文件进行加壳。所谓加壳就是有某种编码算法对原始文件数据进行编码,并使原始文件内容成为数据部分,而嵌进文件的解密代码成为主体。在loader加载加壳文件后,会将控制权交给解码程序,解码程序在完成解码后,再把控制权交给原始代码。
这个加壳程序是学习之用,所以只实现了最简单的可逆变换-异或运算。对文件操作用了内存映射文件,所以直接操作内存即可。
先说一下思路:对原始PE文件编码,一般是保护数据部分,而文件头则不需要进行编码,所以保留文件头。至于原始文件的节表我们也会保留,而我们会在原始PE文件末尾追加几个节以供解码(见PE文件操作-在末尾添加节)。
- PACKRES节:备份的原始PE的资源数据。这个节是原始文件中唯一需要备份的节,因为图标的显示以及一些程序配置都在这个节中,涉及到程序的加载参数,所以需要保留。
- PACKCODE节:解码代码,负责解码所有节,填充IAT和跳回OEP。
- PACKDATA节:解码参数,其中保存了映像信息,原始节表。由于在填充IAT过程中用到了LoadLibrary和GetProcAddress,所以这个节还包含了我们壳程序的导入表和IAT。
最终文件结构就像下面这样:
原始数据
PACKRES
RES1
RES2
…….
PACKCODE
CODE
PACKDATA
PACK_CONFIG
IAT
INT
首先是备份资源节,这个操作最简单,在PE末尾后添加一个节,把原始资源节的数据拷贝至其中,然后遍历 资目录修正其中数据目录项的DataOffset,这个修正过程和重定位表的修正差不多,大致流程:
PIMAGE_SECTION_HEADER res_sec; insert_section_at_eof(ctx, "PACKRES", opt_hdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].Size, IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_CNT_INITIALIZED_DATA, &res_sec); DWORD old_res = opt_hdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress; //拷贝数据 memcpy((PUCHAR)ctx->map_ptr + res_sec->PointerToRawData, (PUCHAR)ctx->map_ptr + rva_to_fo(ctx, (opt_hdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress)), opt_hdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].Size); //将资源目录定位到新的节 opt_hdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress = res_sec->VirtualAddress; //修正dataoffset adjust_resource_rva(ctx, res_sec->VirtualAddress - old_res);
这一步的效果如下图:
这一步备份完后,就可以对原始数据编码了,遍历所有节进行编码,这里要小心一下,因为有些奇怪的程序VirtualSize是比SizeOfRawData大的,不要想当然的觉得VirtualSize肯定小:
for (int i = 0;i < get_section_count(ctx)-1;i++) { PIMAGE_SECTION_HEADER sec_hdr; get_section_entry_by_index(ctx, i, &sec_hdr); for (int j = 0;j < sec_hdr->SizeOfRawData;j++) { *((PUCHAR)ctx->map_ptr + sec_hdr->PointerToRawData + j) ^= 0x1; } }
这一步后,PE文件便无法使用了,只能看到其资源数据,而双击是无法打开的。
然后开始构造供解码程序使用的配置信息,这里准备了6个数据,
OriginalImportTable用来让解码程序找到原始导入目录,
OriginalIATDirectory找到原始IAT目录,
NumberOfSections记录了需要解码的节数量,
ImageBase记录了加载机制,用于和RVA相加,
AddressOfPoint用于到最后跳回到OEP,
后面则是原始节列表。
PUCHAR config_buf = (PUCHAR)malloc(opt_hdr->FileAlignment); PPACK_CONFIG pack_config = (PPACK_CONFIG)config_buf; DWORD config_size = 0; memset(pack_config, 0, opt_hdr->FileAlignment); pack_config->OrginialImportDirectory = opt_hdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; pack_config->OriginalIATDirectory = opt_hdr->DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].VirtualAddress; pack_config->NumberOfSections = file_hdr->NumberOfSections; pack_config->ImageBase = opt_hdr->ImageBase; pack_config->AddressOfPoint = opt_hdr->AddressOfEntryPoint; config_size = 20; for (int i = 0;i < get_section_count(ctx)-1;i++) { PIMAGE_SECTION_HEADER sec_hdr; get_section_entry_by_index(ctx, i, &sec_hdr); //修正属性,不然只读属性无法解码 sec_hdr->Characteristics |= (IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE); memcpy(&pack_config->Sections[i], sec_hdr, sizeof(IMAGE_SECTION_HEADER)); config_size += 40; } config_size += 40;
在原始节列表之后则是我们自己用的IAT,而在这个节偏移512字节处则是INT,这里是考虑到IAT往往占用空间不大,而INT需要占用较大空间,而且这里只用到了两个API。
下面就开始追加代码节和配置节,这里的解码代码是一段汇编代码,用nasm编译而成,最后将bin文件拷贝进代码节:
//打开解码代码文件 HANDLE inject = CreateFile(L"G:\\Develop\\NASM\\pe_inject\\inject", GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); //代码节 PIMAGE_SECTION_HEADER code_sec; DWORD ret; insert_section_at_eof(ctx, "PACKCODE", 512, IMAGE_SCN_CNT_CODE|IMAGE_SCN_MEM_READ|IMAGE_SCN_MEM_WRITE|IMAGE_SCN_MEM_EXECUTE, &code_sec); PUCHAR code_image = ((PUCHAR)ctx->map_ptr + code_sec->PointerToRawData); ReadFile(inject, code_image, opt_hdr->FileAlignment, &ret, NULL); //配置节 PIMAGE_SECTION_HEADER data_sec; insert_section_at_eof(ctx, "PACKDATA", 1024, IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ, &data_sec); PUCHAR data_image = ((PUCHAR)ctx->map_ptr + data_sec->PointerToRawData); memcpy(data_image, config_buf, opt_hdr->FileAlignment);
添加后如下:
生成了这两个节后,就需要在文件的配置节中构造自己用的导入表了,构造时按照导入表的格式正确的配置RVA就可以了,这样loader就能正确的填充IAT了。
PPE_IMPORT_NODE_32 import_node; create_import_node(handler, "kernel32.dll", 0, "GetProcAddress\0LoadLibraryA\0"); DWORD int_fo = data_sec->PointerToRawData + 512; DWORD iat_fo = data_sec->PointerToRawData + config_size; //传进的两个地址分别为INT基址和IAT基址 construct_import_directory(handler, data_image + 512, data_image + config_size, ctx);
最后需要将几个地址硬编码进解压代码,主要是配置信息VA,GetProcAddress和LoadLibrary的IAT项的VA,这儿为了便捷用了这个不太理想的处理方式,但我看有些加壳软件用了对齐的方法,在代码节的开头放配置信息,而代码放在配置信息之后,这样代码通过向下对齐就可以得到配置信息的VA,有兴趣的可以试试。
然后来看解码代码,我不太会汇编,所以解码代码这一块就比较丑了,解码程序使用nasm编译的。
首先加载并保存在数据节中的配置信息:
%define OrginialImportDirectory [ebp-4h]%define OriginalIATDirectory [ebp-8h]%define ImageBase [ebp-ch]%define ModuleBase [ebp-10h]%define AddressOfPoint [ebp-14h]push ebpmov ebp,espsub esp,14hmov eax,11111111h;保存一些局部变量mov edx,[eax+pack_config.OrginialImportDirectory]mov OrginialImportDirectory,edxmov edx,[eax+pack_config.OriginalIATDirectory]mov OriginalIATDirectory,edxmov edx,[eax+pack_config.ImageBase]mov ImageBase,edxmov edx,[eax+pack_config.AddressOfPoint]mov AddressOfPoint,edx
然后遍历我们在配置信息中的节表去解码原始PE数据:
mov ecx,[eax+pack_config.NumberOfSections]mov esi,0lea eax,[eax+SIZE_OF_CONFIG_HEADER] ;节表开始地址jmp loop_sectionload_section_header:mov ebx,[eax+IMAGE_SECTION_HEADER.VirtualAddress]add ebx,ImageBasepush esi ;保存旧计数值并加载新计数值push ecxmov ecx,[eax+IMAGE_SECTION_HEADER.SizeOfRawData]mov esi,0jmp unpack_section_loop unpack_section: ;这儿便是进行解码 mov dl,[ebx+esi] xor dl,1 mov [ebx+esi],dl inc esi unpack_section_loop: cmp ecx,esi jne unpack_section pop ecx pop esi sub ecx,1 inc esi add eax,SIZE_OF_SECTION_HEADERloop_section:test ecx,ecxjnz load_section_header
然后用配置信息中的原始导入表来配置IAT表:
;sub esp,10hmov ebx,[esp+10h] ;OrginialImportDirectoryadd ebx,[esp+8h]mov ecx,0jmp fill_import_loopfill_import: ;得到INT和IAT mov esi,[ebx+ecx] ;OriginalFirstTrunk add esi,[esp+8h] mov edi,[ebx+ecx+16] ;FirstTrunk add edi,[esp+8h] jmp fill_iat_loop fill_iat: ;计算导入名偏移 mov eax,[esi] add eax,[esp+8h] add eax,2 ;获取地址 push ecx push eax mov eax,ModuleBase push eax call [ds:22222222h] ;这是GetProcAddress,在加壳程序中会将其硬编码为IAT的VA pop ecx ;填充IAT mov [edi],eax ;向后偏移4字节 add esi,4 add edi,4 fill_iat_loop: ;不是0继续,是0恢复导入目录表 mov edx,[esi] cmp edx,0 jnz fill_iat add ecx,20fill_import_loop:cmp dword [ebx+ecx+IMAGE_IMPORT_DESCRIPTOR.OriginalFirstTrunk],0jz ready_exit ;是0则退出循环mov esi,[ebx+ecx+IMAGE_IMPORT_DESCRIPTOR.Name]add esi,ImageBasepush esicall [ds:11111111h] ;这是LoadLibrary,在加壳程序中会将其预编码为IAT的VAmov ModuleBase,eaxjmp fill_import
最后会把控制权转交OEP。
mov eax,AddressOfPointadd eax,ImageBasepush eaxret
这样我们就完成了一个简单的加壳程序,这个程序被OD加载后就会有如下提示:
点否载入后
这儿就是我们的汇编代码,此时OEP处
运行至ret时的OEP处
最终转入OEP执行
这个程序只是完成了最简单的加壳工作,经测试发现,如果PE文件中有LoadConfig这个目录表程序就会崩溃,不知道是不是SHE的原因。如果有EXPORT,IMPORT,RESOURCE,DEBUG,IAT这几项中的一项或多项,则可以正常工作,因为我们暂时没有考虑重定向,所以ASLR肯定没法载入。其他的几个目录项太过罕见所以没有测试。
- PE文件操作-简单的加壳实现
- 读取PE文件头的简单实现
- 检测PE文件加壳信息用的特征码
- 感染PE文件的一个简单实例
- 感染PE文件的一个简单实例
- (未完待续)Windows PE 文件加壳学习笔记
- Windows PE 文件加壳学习笔记(续1)
- 如何用程序判定一个PE文件是否加壳
- pe文件简单分析
- 简单感染PE文件
- 简单感染PE文件
- PE文件操作类
- C语言文件操作 简单的文件读写加外部调用
- PE文件之旅(C语言描述) 第三篇 -- 加壳与脱壳的战场 输出表
- PE文件操作-动态加载
- 文件基础操作-PE结构
- PE文件详解-----PE文件的简介
- IDF实验室之简单的PE文件逆向
- leetcode 5 Longest Palindromic Substring--最长回文字符串
- 菜鸟积累 ps一些基本用法
- swift中使用@noescape的正确姿势
- Idea添加本地git仓库
- groovy配置
- PE文件操作-简单的加壳实现
- weibo videokit视频流学习笔记
- 单片机时钟周期、机器周期、指令周期的区别
- 遍历一遍找出链表倒数第K个节点
- Reorder List的总结
- HDU Danganronpa
- JamVM移植问题2
- AppBarLayout,CoordinatorLayout常用属性
- Android UI(ActionBar+Toolbar)详解