2-Windows Shellcode开发(x64 stager)

文章首发于先知社区:https://xz.aliyun.com/news/17961arrow-up-right

作者:一天

随着Windows操作系统的不断演进,x64架构的广泛应用,掌握x64环境下Shellcode的开发技术变得尤为重要。本文将深入剖析Windows x64 Shellcode开发的关键技术点,从基础的弹窗示例到复杂的网络通信模块,结合实战代码示例,为读者呈现一套完整的开发思路与实现方法。

注意:x64编写shellcode,最重要的就是RSP对齐对齐对齐对齐对齐对齐!

一、弹窗shellcode

1.1 环境配置

在本小节会重点介绍 GetProcAddressByHash x64的实现,其实大致的过程是类似x86的,只是涉及到PE和PEB时的偏移量会不同,还有一个不同点就是:我们可以用的通用寄存器非常多,一定要关注调用前后值不变和变的寄存器。

我用的环境是Visual Studio+ml64+link,按道理来说只要你安装了相应平台工具集会自带这些工具套件,不必额外安装,如果想用nasm汇编的另说。

接下来我们创建一个C++控制台应用

创建好后,右键项目->生成依赖项->生成自定义

点击勾选masm,然后确定即可

接下来给项目添加一个shellcode.asm文件即可

无论是debug还是release都需要配置入口点

在release下需要在命令行设置 /SAFESEH:NO

1.2 GetProcAddressByHash

编写纯独立shellcode,最大的难题是如何不依赖导入表获取函数的地址,为了解决这个大问题,不得不又请出我们的两位老朋友——PEB和PE文件结构。想必各位师傅也知道我要干什么了,无非就是

  1. 获取PEB的地址:从gs/fs寄存器中获取PEB的地址

  2. 遍历加载的模块列表:从PEB中访问 Ldr 成员,获取 PEB_LDR_DATA 结构。遍历InMemoryOrderModuleList链表,获取每个模块的LDR_DATA_TABLE_ENTRY。

  3. 查找目标DLL(如kernel32.dll):比较每个模块的BaseDllName与目标DLL名称(不区分大小写)

  4. 解析目标DLL的导出表:从DLL基地址获取PE头,定位导出表。遍历导出表中的函数名称,找到目标函数并计算其地址。

都快形成肌肉记忆了/(ㄒoㄒ)/,具我所知还有另一种方法可以得到ntdll.dll和kernel32.dll的基址,等我研究清楚了再水一篇文章。

代码参考这三个文件

  1. stager_reverse_https.asm:程序的主入口点

  2. block_api.asm:代码通过动态解析哈希值来定位所需的API函数地址

  3. block_reverse_https.asm:该汇编代码实现了一个通过HTTP下载并执行远程代码的Shellcode加载器

接下来进入分析环节

(1)保存前4个参数到栈上,并保存rsi的值

rcx、rdx、r8、r9存储着要调用的WindowsAPI的前四个参数,且接下来各个步骤可能需要用到这四个寄存器,所以我们将其值保存到栈上。rsi的值要不要保存可以根据情况来判断。如果不保存,则main中使用到rsi来保存值可能会有数据丢失的风险。

(2)获取 InMemoryOrderModuleList 模块链表的第一个模块结点

  1. mov rdx,gs:[rdx+60h]:GS存放着TEB的首地址,而TEB偏移0x60的位置则是PEB结构体的指针。

  1. mov rdx,[rdx+18h]:获取ldr指针。在PEB结构体偏移0x18则存放着 PEB_LDR_DATA 结构体的指针

  1. mov rdx,[rdx+20h]:获取第一个模块结点。在 PEB_LDR_DATA 结构体中0x20的位置是存储着 InLoadOrderModuleList 模块链表的指针。第一个模块即进程本身

(3)模块遍历

  1. movzx rcx,word ptr [rdx+48h] :此时的rdx指向InMemoryOrderLinks,而InMemoryOrderLinks与BaseDllName的偏移是0x48,而BaseDllName这个结构体如下图所示。故 movzx rcx,word ptr [rdx+48h] 获取的是是模块的名称的长度

注意: ①在Stephen Fewer的代码中获取长度是用偏移0x4a来获取的,其实两种方式都可以,用MaximumLength,以为着它将字符串末尾的多个 00 也算进去了,这会导致多执行几轮计算hash的步骤,导致hash值与用偏移0x48计算的hash值的不一样。

②我将Stephen Fewer代码中出现r9的地方用r8替换,然后出现r8的地方用r9替换,毕竟造轮子也喜欢搞点特殊。

  1. mov rsi,[rdx+50h]:获取模块名称,分析的方法同上

接下来我们调式一下,来验证是否正确,在 mov rsi,[rdx+50h],下一个断点,可以看到rsi确实存放着模块名称数组的首地址

(4)计算模块hash

看注释,不多讲

  1. 可以将字符串统一为大写,也可以将字符串统一为小写,目的就是大小写不敏感,因为微软在给dll命名时有时会用字母大写,有时会用小写。

  1. ror r8d,0dh :循环右移的位数可以自己设定,不一定要求是13位,只要保证你给的目标hash也是使用相同的手段得到即可

(5)获取导出表

  1. mov rdx, [rdx+20h]:获取模块的基址,此时rdx是指向 InMemoryOrderLinks,距离rdx偏移0x20的位置上是模块的基址

  1. mov eax, dword ptr [rdx+3ch]:获取PE头RVA,从这条指令开始都是涉及PE头的操作。

  1. cmp word ptr [rax+18h],20Bh:PE头偏移0x18的位置是Magic字段,该字段表示PE类型标识(0x20B=PE64,0x10B=PE32)

  1. mov eax, dword ptr [rax+88h]:PE头偏移0x88的位置是DataDirectory数组首地址。DataDirectory[0].VirtualAddress表示导出表的RVA

  1. mov ecx, dword ptr [rax+18h]mov r9d, dword ptr [rax+20h]:按名称导出的函数数量和函数名称字符串地址数组的RVA

我们调式来看一下,在 mov rdx, [rdx+20h] 下一个断点

rax+18h处存储着0x020b,确实是PE64的Magic

因为第一个模块(本进程)是没有编写导出函数的,所以导出表的RVA为0,我们按F11跳出,此时程序停在 mov rdx, [rdx+20h],第二个模块即ntdll.dll,可以看到一共有0x9B8个导出函数。

(6)获取函数名

调试

(7)计算模块 hash + 函数 hash之和

解释一下 add r8,[rsp+8],在这条之前,我们就已经将模块哈希值压入到了栈上,此时的rsp指向的是导出表的地址,rsp+8的位置才是模块哈希值

(8)获取目标函数指针

(9)清栈并调用目标函数

调用顺序:main->GetProcAddressByHash->Windows API

恢复到调用前的栈空间的布局,其中之一的原因是:某些API可能有4个以上的参数,前4个参数存放在特定的寄存器,而后面的参数存放在栈上。恢复之后就需要预留32字节的影子空间,由main函数来清理,当然也可以不清理,要看情况而言。

首先,我们要清除 GetProcAddressByHash 中存放在栈上的值,然后呢恢复之前存放在栈上的rsi、rcx、rdx、r8、r9的值,其中后4个寄存器是用来做Windows API的前四个参数。

其次,为了确保我们调用完目标函数后能返回到main中的下一条指令,我们需要保存原始返回地址,通过 push r10jmp rax 来模拟call指令

最后呢再说一下,按照Windows x64调用约定要求调用者(这里的调用者是GetProcAddressByHash)为前4个参数分配32字节的影子存储区,即使参数通过寄存器传递,具体原因还请师傅们自行查阅资料了。

1.3 main

有几个点需要注意的是: ① and rsp, 0xFFFFFFFFFFFFFFF0 这条指令的核心作用是将 栈指针(RSP)强制对齐到16字节边界,Windows x64调用约定要求调用(call)函数时,RSP在调用前必须对齐到16字节

②如果涉及到栈操作,在调用call指令之前,push指令或者pop指令必须使rsp以0结尾数,不是0结尾就要想办法对齐,比如代码中我用 push 0 来保证rsp按16字节对齐。不对齐会出现下图所示的异常

③还有一个字符串问题,按照Stephen Fewer的代码,他是这样表示字符串的。

我按照他的格式,栈中的字符串如下所示,好像不太符合字符串从左到右依次从低地址往高地址增长。

原因可能是他使用的nasm汇编,而我使用的是masm汇编。正确的表示应该是 mov r14, '23resu'mov r14,0000323372657375h

④按照windows X64调用约定,参数传递的方式有所不同,前四个参数分别使用RCX、RDX、R8和R9从左到右顺序传递,后续的参数就使用栈传递,压栈的顺序是从右到左。如

对应的汇编顺序是

我这里是表示的很不严谨,实际还需要考虑影子空间和对齐,但是传参顺序就是按照上述代码进行的。

1.4 测试

老规矩,用010 editor提取,这一步我是建议按照我的方式进行,因为下面各个stager要下载的stage就是本例中的弹窗shellcode。

然后用 runshc64.exe 加载shellcode

1.5 完整代码

二、stager(wininet版)

详细的过程我就不介绍了,感觉写来写去还是哪些步骤,请各位师傅参考我的另一篇文章搭配食用

2.1 值得关注的点

在这里我提一下值得注意的点:

①Stephen Fewer使用的是lpszAgent非NULL,这个指针指向空字符 "",即无UA

我使用的是lpszAgent = NULL,使用默认的UA,即"Microsoft-WinINet",孰优孰劣我不太清楚:)

②我使用了区别于Stephen Fewer的获取服务器ip地址的方式,我们来看一下他是怎么获取的

我感觉中间的步骤有点多余了(大佬们别喷我/(ㄒoㄒ)/),所以就改成了下面代码,还有server_host最好定义在汇编代码的最后面,这样我们才能制作出模板shellcode,服务器地址固定在Shellcode末尾,可通过直接修改最后的字节替换IP。

③我使用了另一种获得uri的方式,区别于Stephen Fewer。我们都知道call指令的作用是下一条指令的地址到栈上,并跳转到相应位置处的代码去执行后续逻辑。

  1. 我们可以利用这个特性将uri字符串定义在call指令的下面

  2. 然后再让call指令跳转到httpOpenRequestA标签处的指令,然后执行后续的逻辑

  3. 最后弹出存放在栈上的uri字符串地址(1次push和1次pop,刚好相互抵消,不影响rsp)

④在 HttpOpenRequestAdwFlags 参数中添加 INTERNET_FLAG_RELOAD,强制从服务器获取最新内容(而非缓存)。好像我在x86的时候使用的是dwFlags=0,我感觉不太好,缓存影响调式程序。

⑤mian函数中,我们之前都不清零影子空间,但是这里需要清除,不然执行到execute_stage标签的ret指令时rsp不指向缓冲区的地址,也就弹不出缓冲区的地址,当然你也可以用一个寄存器存储缓冲区的地址(推荐使用r12、r13、r14和r15等非易失性寄存器)

⑥在masm汇编中,下面红框中一定要使用rdi,而非edi。

⑦又是tm的对齐!

不知道师傅们有没有关注过红框中的代码,按道理来说push两次,再pop两次,栈上应该就没有缓冲区的地址了啊

为了搞清楚Stephen Fewer在末尾写道 pop rax ; f*cking alignment 的缘由,我又要调式了,我们在 7. 调用VirtualAlloc分配可执行内存call GetProcAddressByHash 下一个断点,此时栈上一切正常。

F10之后再去查看栈上的情况。可以看到此时缓冲区的地址已经存放在栈上,但是我们并没有push缓冲区的地址到栈上,具体原因未知。

程序运行到 mov rdi, rsp ,此时栈的情况如下图所示。很明显第一个 push rbx 是为了对齐用的,你也可以将其换成其他的,如 push rax

2.2 测试

shellcode64.bin是在第一小节 一、弹窗shellcode 制作的弹窗shellcode

首先是exe形式

其次是shellcode形式

2.3 完整代码

三、stager(winhttp版)

详细的过程我就不介绍了,感觉写来写去还是哪些步骤,请各位师傅参考我的另一篇文章搭配食用

3.1 测试

shellcode64.bin是在第一小节 一、弹窗shellcode 制作的弹窗shellcode

换个方法测试,选定shellcode的范围,文件->导出为十六进制

导出类型选择C语言,范围选择所选内容

加载器代码如下

3.2 完整代码

四、stager(ws2_32版)

代码参考:

  1. stager_reverse_tcp_nx.asm:程序的主入口点

  2. block_api.asm:代码通过动态解析哈希值来定位所需的API函数地址

  3. block_recv.asm:通过 recv 接收并执行后续载荷

  4. block_reverse_tcp.asm:实现反向 TCP 连接

在我的另一篇文章 Windows Shellcode开发(x86 stager) 中我说过TCP socket编程有两种玩法,一种是正向连接——已经介绍过了,另一种是反向连接。本文将详细介绍TCP反向连接的实现

4.1 必要的解释

(1)调用WSAStartup函数

  1. sub rsp, 400+8:WSAData结构体大小400字节(400是整除16的),为什么要加8个字节用于对齐呢?可以看到上面的代码,push出现了2次,pop出现了一次,很明显,需要+8来抵销一次push。如果不对齐,执行到 call GetProcAddressByHash 则会报错。

  2. mov r12,0101A8C05C110002h:我们了分析一下这个sockaddr_in结构体如何构造

    • 0101A8C0: C0=192, A8=168, 01=1, 01=1,即ip=192.168.1.1

    • 5C11(大端序):端口4444

    • 0002 :表示AF_INET(IPv4)

  3. push 0101h:在x86 shellcode编写中,我使用的是 0202h,即Winsock 2.2版本。但是经过测试,我在自己的win11系统上也能适用Winsock 1.1版本。低版本Winsock能适配更多windows古早系统,同时windows11、10也能够兼容低版本Winsock。

  4. 在上面的代码中,R12保存sockaddr_in结构指针,一直到 调用connect函数 时才会使用到,为此我修改了 GetProcAddressByHash 部分代码,即保存r12寄存器的值,避免丢失。

(2)调用connect函数

在正向连接中,我们的shellcode需要扮演一个服务器,需要bind+listen+accept这一套组合才能与客户端建立连接。但是在反向连接中,我们的shellcode扮演着一个客户端,只需要connect直接向控制端的监听端口发起连接请求。

(3)清栈

  • 400+8是在WSAStartup初始化分配给WSAData结构体的

  • 执行到此条指令时,一共出现了7次push,2次pop,所以还剩7-2=5,每次栈操作涉及8字节,故要释放 5*8 个字节的栈空间

  • 一共执行了4次windows API函数,每次调用 GetProcAddressByHash 都会产生32字节的影子空间,故要释放4*32字节的栈空间

这是程序开始执行时的rsp指针

add rsp, ((400+8)+(5*8)+(4*32)) 下一个断点,可以看到红框内是我们使用到的栈空间

F11后,栈空间如下,又回到了程序开始时的rsp指针

PS:经过测试,不清理栈空间,程序也能正常执行。

(4)分段接收

这一段代码我对Stephen Fewer的代码进行了大整改,具体原因还是因为必需在原始数据的开头patch 4个字节作为接下来接收的数据长度,我不想实现,所以就大改 :)

下图是Stephen Fewer的代码

4.2 测试

shellcode64.bin是在第一小节 一、弹窗shellcode 制作的弹窗shellcode

python服务器代码

首先是exe形式

其次是shellcode形式

4.3 完整代码

下一步计划:linux shellcode开发或者srdi相关的文章,又或者两个一起出?如果看过我几篇文章的师傅想必会有所察觉,那就是我的文章之间存在联动,或者说我的文章是按照某个主题往下推进,如果对这个主题感兴趣或者觉得我的文章对您有帮助,麻烦点点关注不迷路o.O。

参考资料

[1]: metasploit-framework/external/source/shellcode/windows/x64/src/stager/stager_reverse_https.asm at master · rapid7/metasploit-frameworkarrow-up-right [2]: metasploit-framework/external/source/shellcode/windows/x64/src/block/block_api.asm at master · rapid7/metasploit-frameworkarrow-up-right [3]: metasploit-framework/external/source/shellcode/windows/x64/src/block/block_reverse_https.asm at master · rapid7/metasploit-frameworkarrow-up-right [4]: x64 调用约定 | Microsoft Learnarrow-up-right [5]: https://xz.aliyun.com/news/17827 [6]: metasploit-framework/external/source/shellcode/windows/x64/src/stager/stager_reverse_tcp_nx.asm at master · rapid7/metasploit-frameworkarrow-up-right [7]: metasploit-framework/external/source/shellcode/windows/x64/src/block/block_recv.asm at master · rapid7/metasploit-frameworkarrow-up-right [8]: metasploit-framework/external/source/shellcode/windows/x64/src/block/block_reverse_tcp.asm at master · rapid7/metasploit-frameworkarrow-up-right [9]: 免杀那点事之随手C写一个持久反弹shell(六)arrow-up-right

Last updated