这部分主要对 pop calc.exe 的 shellcode 进行分析,因为Agent 内存马 (二) 篇幅过长,因此分开讲。
其中部分思路在关于调用 JNI_GetCreatedJavaVMs 的 shellcode 浅析内已经仔细讲过了,因此不再赘述。
当我们拿到一个 shellocde,我们自然会将其写入文件中,然后用 IDA 分析其汇编。但是单纯的汇编不一定能够让我们直观快速的理解其思路和功能,因此调试是必不可少的。
执行下述代码会得到 agentshellcode 文件,将其放入 IDA 编译,并且对数据段按 C,然后选择 analyze 就可以反汇编了。
package org.example;
import java.io.FileOutputStream;
import java.io.IOException;
public class WriteBytesToFile {
public static void main(String[] args) {
byte[] buf= new byte[] //pop calc.exe
{
(byte) 0xfc, (byte) 0x48, (byte) 0x83, (byte) 0xe4, (byte) 0xf0, (byte) 0xe8, (byte) 0xc0, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0x51, (byte) 0x41, (byte) 0x50, (byte) 0x52, (byte) 0x51,
(byte) 0x56, (byte) 0x48, (byte) 0x31, (byte) 0xd2, (byte) 0x65, (byte) 0x48, (byte) 0x8b, (byte) 0x52,
(byte) 0x60, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x18, (byte) 0x48, (byte) 0x8b, (byte) 0x52,
(byte) 0x20, (byte) 0x48, (byte) 0x8b, (byte) 0x72, (byte) 0x50, (byte) 0x48, (byte) 0x0f, (byte) 0xb7,
(byte) 0x4a, (byte) 0x4a, (byte) 0x4d, (byte) 0x31, (byte) 0xc9, (byte) 0x48, (byte) 0x31, (byte) 0xc0,
(byte) 0xac, (byte) 0x3c, (byte) 0x61, (byte) 0x7c, (byte) 0x02, (byte) 0x2c, (byte) 0x20, (byte) 0x41,
(byte) 0xc1, (byte) 0xc9, (byte) 0x0d, (byte) 0x41, (byte) 0x01, (byte) 0xc1, (byte) 0xe2, (byte) 0xed,
(byte) 0x52, (byte) 0x41, (byte) 0x51, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x20, (byte) 0x8b,
(byte) 0x42, (byte) 0x3c, (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x8b, (byte) 0x80, (byte) 0x88,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x85, (byte) 0xc0, (byte) 0x74, (byte) 0x67,
(byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x50, (byte) 0x8b, (byte) 0x48, (byte) 0x18, (byte) 0x44,
(byte) 0x8b, (byte) 0x40, (byte) 0x20, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0xe3, (byte) 0x56,
(byte) 0x48, (byte) 0xff, (byte) 0xc9, (byte) 0x41, (byte) 0x8b, (byte) 0x34, (byte) 0x88, (byte) 0x48,
(byte) 0x01, (byte) 0xd6, (byte) 0x4d, (byte) 0x31, (byte) 0xc9, (byte) 0x48, (byte) 0x31, (byte) 0xc0,
(byte) 0xac, (byte) 0x41, (byte) 0xc1, (byte) 0xc9, (byte) 0x0d, (byte) 0x41, (byte) 0x01, (byte) 0xc1,
(byte) 0x38, (byte) 0xe0, (byte) 0x75, (byte) 0xf1, (byte) 0x4c, (byte) 0x03, (byte) 0x4c, (byte) 0x24,
(byte) 0x08, (byte) 0x45, (byte) 0x39, (byte) 0xd1, (byte) 0x75, (byte) 0xd8, (byte) 0x58, (byte) 0x44,
(byte) 0x8b, (byte) 0x40, (byte) 0x24, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0x66, (byte) 0x41,
(byte) 0x8b, (byte) 0x0c, (byte) 0x48, (byte) 0x44, (byte) 0x8b, (byte) 0x40, (byte) 0x1c, (byte) 0x49,
(byte) 0x01, (byte) 0xd0, (byte) 0x41, (byte) 0x8b, (byte) 0x04, (byte) 0x88, (byte) 0x48, (byte) 0x01,
(byte) 0xd0, (byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x58, (byte) 0x5e, (byte) 0x59, (byte) 0x5a,
(byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x59, (byte) 0x41, (byte) 0x5a, (byte) 0x48, (byte) 0x83,
(byte) 0xec, (byte) 0x20, (byte) 0x41, (byte) 0x52, (byte) 0xff, (byte) 0xe0, (byte) 0x58, (byte) 0x41,
(byte) 0x59, (byte) 0x5a, (byte) 0x48, (byte) 0x8b, (byte) 0x12, (byte) 0xe9, (byte) 0x57, (byte) 0xff,
(byte) 0xff, (byte) 0xff, (byte) 0x5d, (byte) 0x48, (byte) 0xba, (byte) 0x01, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x8d, (byte) 0x8d,
(byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0xba, (byte) 0x31, (byte) 0x8b,
(byte) 0x6f, (byte) 0x87, (byte) 0xff, (byte) 0xd5, (byte) 0xbb, (byte) 0xf0, (byte) 0xb5, (byte) 0xa2,
(byte) 0x56, (byte) 0x41, (byte) 0xba, (byte) 0xa6, (byte) 0x95, (byte) 0xbd, (byte) 0x9d, (byte) 0xff,
(byte) 0xd5, (byte) 0x48, (byte) 0x83, (byte) 0xc4, (byte) 0x28, (byte) 0x3c, (byte) 0x06, (byte) 0x7c,
(byte) 0x0a, (byte) 0x80, (byte) 0xfb, (byte) 0xe0, (byte) 0x75, (byte) 0x05, (byte) 0xbb, (byte) 0x47,
(byte) 0x13, (byte) 0x72, (byte) 0x6f, (byte) 0x6a, (byte) 0x00, (byte) 0x59, (byte) 0x41, (byte) 0x89,
(byte) 0xda, (byte) 0xff, (byte) 0xd5, (byte) 0x63, (byte) 0x61, (byte) 0x6c, (byte) 0x63, (byte) 0x2e,
(byte) 0x65, (byte) 0x78, (byte) 0x65, (byte) 0x00
};
try (FileOutputStream fos = new FileOutputStream("agentshellcode")) {
fos.write(buf);
System.out.println("写入文件成功!");
} catch (IOException e) {
System.err.println("写入文件失败: " + e.getMessage());
}
}
}这是执行弹出计算器的 shellcode,其中注释为我对代码的分析,通过注释能够快速的理解整个汇编代码的功能。
seg000:0000000000000000 cld /* 清除方向标志位 */
seg000:0000000000000001 and rsp, 0FFFFFFFFFFFFFFF0h /* 栈对齐 */
seg000:0000000000000005 call loc_CA /* 调用 loc_CA */
seg000:000000000000000A push r9 /* 一系列压栈 */
seg000:000000000000000C push r8 /* */
seg000:000000000000000E push rdx
seg000:000000000000000F push rcx
seg000:0000000000000010 push rsi
seg000:0000000000000011 xor rdx, rdx /* rdx 清零 */
seg000:0000000000000014 mov rdx, gs:[rdx+60h] /* 获取 PEB 进程块 */
seg000:0000000000000019 mov rdx, [rdx+18h] /* 获取_PEB_LDR_DATA */
seg000:000000000000001D mov rdx, [rdx+20h] /* 从中获取 InMemoryOrderModuleList 链表 */
seg000:0000000000000021
seg000:0000000000000021 loc_21: ; CODE XREF: seg000:00000000000000C5↓j
seg000:0000000000000021 mov rsi, [rdx+50h] /* 从链表中通过偏移获取指向 BaseDllName 的字符串指针 */
seg000:0000000000000025 movzx rcx, word ptr [rdx+4Ah] /* 从链表中通过偏移获取 BaseDllName 字符串长度 */
seg000:000000000000002A xor r9, r9 /* r9,rax 清零 */
seg000:000000000000002D
seg000:000000000000002D loc_2D:
seg000:000000000000002D xor rax, rax /* */
seg000:0000000000000030 lodsb /* 从 rsi 指向的地址加载一个字节到 al */
seg000:0000000000000031 cmp al, 61h ; 'a' /* 和 a 进行比较 */
seg000:0000000000000033 jl short loc_37 /* 小于 a 则跳转到 loc_37 */
seg000:0000000000000035 sub al, 20h ; ' ' /* 将大小字母转为小写字母 */
seg000:0000000000000037
seg000:0000000000000037 loc_37: ; CODE XREF: seg000:0000000000000033↑j
seg000:0000000000000037 ror r9d, 0Dh /* 将 r9d 寄存器的值循环右移 13 位 */
seg000:000000000000003B add r9d, eax /* r9d 加上 eax */
seg000:000000000000003E loop loc_2D /* 循环跳转执行 loc_2D 标签,每次执行 ecx 自减 1,直到 ecx 寄存器的值为 0。这三行代码实际上实现了哈希值的计算 */
seg000:0000000000000040 push rdx /* 压入链表地址 */
seg000:0000000000000041 push r9 /* 压入计算出来的哈希值 */
seg000:0000000000000043 mov rdx, [rdx+20h] /* 在链表中通过偏移获取当前进程的基址 (链表首个即为本身) */
seg000:0000000000000047 mov eax, [rdx+3Ch] /* 获取 IMAGE_NT_HEADERS 的起始地址,这里获取的是 PE 头的偏移 */
seg000:000000000000004A add rax, rdx /* 相加得到 NT 文件头地址 */
seg000:000000000000004D mov eax, [rax+88h] /* 获取导出表的虚拟地址 */
seg000:0000000000000053 test rax, rax /* 测试 rax 的值是否为 0 */
seg000:0000000000000056 jz short loc_BF /* 如果为零则跳转至 loc_BF (该函数为错误处理函数) */
seg000:0000000000000058 add rax, rdx /* 相加得到导出表地址 */
seg000:000000000000005B push rax /* 压栈留着后面用 */
seg000:000000000000005C mov ecx, [rax+18h] /* 通过偏移获取 以名字导出的函数的个数 */
seg000:000000000000005F mov r8d, [rax+20h] /* 通过偏移获取 导出的函数名称表 */
seg000:0000000000000063 add r8, rdx /* 加上基址得到函数导出名称表的真实地址 */
seg000:0000000000000066
seg000:0000000000000066 loc_66: ; CODE XREF: seg000:000000000000008C↓j
seg000:0000000000000066 jrcxz loc_BE /* 判断 rcx 是否为 0,如果为 0 则跳转至 loc_BE */
seg000:0000000000000068 dec rcx /* rcx 递减 */
seg000:000000000000006B mov esi, [r8+rcx*4] /* 从函数名称导出表获取指向名称的虚拟地址 */
seg000:000000000000006F add rsi, rdx /* 加上基址得到指向名称的真实地址 */
seg000:0000000000000072 xor r9, r9 /* r9 清零 */
seg000:0000000000000075
seg000:0000000000000075 loc_75: ; CODE XREF: seg000:0000000000000082↓j
seg000:0000000000000075 xor rax, rax /* rax 清零 */
seg000:0000000000000078 lodsb /* 从 rsi 指向的地址加载一个字节到 al */
seg000:0000000000000079 ror r9d, 0Dh /* r9d 右移 13 位 */
seg000:000000000000007D add r9d, eax /* r9d 加上 eax */
seg000:0000000000000080 cmp al, ah /* 比较 al 和 ah */
seg000:0000000000000082 jnz short loc_75 /* 如果不相等则跳转至 loc_75 */
seg000:0000000000000084 add r9, [rsp+8] /* r9 加上函数导出表地址 */
seg000:0000000000000089 cmp r9d, r10d /* 比较 r9d 和 r10d (r10 来自 loc_CA 的赋值),本地调试发现寻找的是 WinExec 函数,r10d 是其 hash 值 */
seg000:000000000000008C jnz short loc_66 /* 如果不相等则跳转至 loc_66 重新执行,其实这就是一个汇编写的循环 */
seg000:000000000000008E pop rax /* 出栈 rax */
seg000:000000000000008F mov r8d, [rax+24h] /* 获取函数序号表的虚拟地址 */
seg000:0000000000000093 add r8, rdx /* 加上基址获取函数序号表的真实地址 */
seg000:0000000000000096 mov cx, [r8+rcx*2] /* 从函数序号表获取相应的序号 */
seg000:000000000000009B mov r8d, [rax+1Ch] /* 获取函数地址表的虚拟地址 */
seg000:000000000000009F add r8, rdx /* 加上基址获得函数地址表的真实地址 */
seg000:00000000000000A2 mov eax, [r8+rcx*4] /* 通过序号从函数地址表里找到对应函数的虚拟地址 */
seg000:00000000000000A6 add rax, rdx /* 加上基址获取对应函数的真实地址 */
seg000:00000000000000A9 pop r8 /* 一系列出栈 */
seg000:00000000000000AB pop r8 /* */
seg000:00000000000000AD pop rsi
seg000:00000000000000AE pop rcx
seg000:00000000000000AF pop rdx
seg000:00000000000000B0 pop r8
seg000:00000000000000B2 pop r9
seg000:00000000000000B4 pop r10
seg000:00000000000000B6 sub rsp, 20h /* 分配影子空间 */
seg000:00000000000000BA push r10 /* 压入参数 r10 */
seg000:00000000000000BC jmp rax /* 跳转执行 WinExec */
seg000:00000000000000BE ; ---------------------------------------------------------------------------
seg000:00000000000000BE
seg000:00000000000000BE loc_BE: ; CODE XREF: seg000:loc_66↑j
seg000:00000000000000BE pop rax /* 出栈为 rax, 此时 rax 指向函数导出表 */
seg000:00000000000000BF
seg000:00000000000000BF loc_BF: ; CODE XREF: seg000:0000000000000056↑j
seg000:00000000000000BF pop r9 /* 弹出之前计算的哈希值 */
seg000:00000000000000C1 pop rdx /* 弹出链表地址 */
seg000:00000000000000C2 mov rdx, [rdx] /* 指向链表指针指向下一个链表 */
seg000:00000000000000C5 jmp loc_21 /* 跳转至 loc_21 重新对新指向的链表进行函数查找 */
seg000:00000000000000CA ; ---------------------------------------------------------------------------
seg000:00000000000000CA
seg000:00000000000000CA loc_CA: ; CODE XREF: seg000:0000000000000005↑p
seg000:00000000000000CA pop rbp /* 将进入函数时 RIP 指向的地址出栈赋值给 rbp */
seg000:00000000000000CB mov rdx, 1 /* rdx 赋值为 1 */
seg000:00000000000000D5 lea rcx, [rbp+101h] /* 获取 rbp+101 处加载我们写在末尾的 calc.exe */
seg000:00000000000000DC mov r10d, 876F8B31h /* 为 r10d 赋值 876F8B31 */
seg000:00000000000000E2 call rbp /* 使用 call,回到进入函数时 RIP 指向的位置(本文中为 005) */
seg000:00000000000000E4 mov ebx, 56A2B5F0h /* ebx 赋值 56A2B5F0 (ExitProcess)*/
seg000:00000000000000E9 mov r10d, 9DBD95A6h /* r10d 赋值 9DBD95A6 (GetVersion) */
seg000:00000000000000EF call rbp /* 返回 rbp 指向重新执行之前执行过的步骤 */
seg000:00000000000000F1 add rsp, 28h /* rsp 移动 0x28 rbp 此时指向的为本文中 0x02A 处的代码(注意栈平衡,本文的代码为 IDA 静态分析的代码) */
seg000:00000000000000F5 cmp al, 6 /* */
seg000:00000000000000F7 jl short loc_103 /* 如果 al 小于 6 则跳转至 loc_103 (退出线程) */
seg000:00000000000000F9 cmp bl, 0E0h /* */
seg000:00000000000000FC jnz short loc_103 /* 如果 bl 不等于 0E0 则跳转至 loc_103 (退出线程) */
seg000:00000000000000FE mov ebx, 6F721347h /* ebx 赋值 6F721347 (RtlExitUserThread) */
seg000:0000000000000103
seg000:0000000000000103 loc_103: ; CODE XREF: seg000:00000000000000F7↑j
seg000:0000000000000103 ; seg000:00000000000000FC↑j
seg000:0000000000000103 push 0 /* 压栈 0 */
seg000:0000000000000105 pop rcx /* 出栈给 rcx */
seg000:0000000000000106 mov r10d, ebx /* 将 ebx 赋值给 r10d */
seg000:0000000000000109 call rbp /* 调用 rbp */
/* loc_103 函数的实际作用就是将 rcx 赋值为 0,r10d 的值赋为 ExitProcess 的 hash 值,然后跳转至 rbp 指向的地址再执行一遍代码,不过这次执行的函数变成了 ExitProcess,执行其退出线程 */
/* 下述为 calc.exe 字符串反编译出的代码,实际未执行 */
seg000:000000000000010B movsxd esp, dword ptr [rcx+6Ch]
seg000:000000000000010E movsxd ebp, dword ptr [rsi]
seg000:0000000000000110 db 65h
seg000:0000000000000110 js short near ptr 178h简单的 x64dbg 调试教程
现在如果我们觉得单纯的看汇编代码过于枯燥无聊,想通过调试去进一步了解,这时候我们就要掏出 x64dbg 了。
随便选择一个无用的进程进行附加 (attach)

附加上进程后,随便选择一个看起来干净的代码段右键选择二进制 -> 编辑

因为我也不是很熟悉,因此我的方法可能有点多次一举,因为 shellocde 里面有 0x00,所以复制字符串会导致 shellocde 缺失。
因此我写入代码的方法是先在 ASCII 处填充和 shellocde 等长的 0。
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
然后选择 16 进制的开头,把我们 shellocde 的 16 进制数据复制进去。
FC 48 83 E4 F0 E8 C0 00 00 00 41 51 41 50 52 51
56 48 31 D2 65 48 8B 52 60 48 8B 52 18 48 8B 52
20 48 8B 72 50 48 0F B7 4A 4A 4D 31 C9 48 31 C0
AC 3C 61 7C 02 2C 20 41 C1 C9 0D 41 01 C1 E2 ED
52 41 51 48 8B 52 20 8B 42 3C 48 01 D0 8B 80 88
00 00 00 48 85 C0 74 67 48 01 D0 50 8B 48 18 44
8B 40 20 49 01 D0 E3 56 48 FF C9 41 8B 34 88 48
01 D6 4D 31 C9 48 31 C0 AC 41 C1 C9 0D 41 01 C1
38 E0 75 F1 4C 03 4C 24 08 45 39 D1 75 D8 58 44
8B 40 24 49 01 D0 66 41 8B 0C 48 44 8B 40 1C 49
01 D0 41 8B 04 88 48 01 D0 41 58 41 58 5E 59 5A
41 58 41 59 41 5A 48 83 EC 20 41 52 FF E0 58 41
59 5A 48 8B 12 E9 57 FF FF FF 5D 48 BA 01 00 00
00 00 00 00 00 48 8D 8D 01 01 00 00 41 BA 31 8B
6F 87 FF D5 BB F0 B5 A2 56 41 BA A6 95 BD 9D FF
D5 48 83 C4 28 3C 06 7C 0A 80 FB E0 75 05 BB 47
13 72 6F 6A 00 59 41 89 DA FF D5 63 61 6C 63 2E
65 78 65 00
然后点击确定,可以发现我们插入的 shellcode 出现了。

然后我们选择 shellcode 的开头右键,选择在此设置 RIP。

当看见 RIP 指向我们选中的地方就说明设置成功了。

然后 x64dbg 的调试流程就和普通的代码调试一样,想要一步步走就选步进,想要执行到目标位置就下断点然后选择执行。寄存器参数在右边的框框里,在左下偏中间处可以看到当前指向的寄存器的值。
shellcode 分析
这部分对这个 shellcode 的流程进行分析,和之前分析的 JNI_GetCreatedJavaVMs 调用不同,这个代码多了很多跳转,省去了很多代码,但是让分析变得容易晕头转向。
因为 IDA 给出的代码是静态代码,不方便分析地址,因此我后续参数的地址都是我在 x64dbg 里动态调试的结果。
同样的仍然进行拆分分析。
seg000:0000000000000000 cld /* 清除方向标志位 */
seg000:0000000000000001 and rsp, 0FFFFFFFFFFFFFFF0h /* 栈对齐 */
seg000:0000000000000005 call loc_CA /* 调用 loc_CA */这部分仅进行了基础的处理,清除方向标志位,进行了栈对齐,然后就跳转到了 loc_CA 函数进行下一步处理。
seg000:00000000000000CA loc_CA: ; CODE XREF: seg000:0000000000000005↑p
seg000:00000000000000CA pop rbp /* 将进入函数时 RIP 指向的地址出栈赋值给 rbp */
seg000:00000000000000CB mov rdx, 1 /* rdx 赋值为 1 */
seg000:00000000000000D5 lea rcx, [rbp+101h] /* 获取 rbp+101 处加载我们写在末尾的 calc.exe */
seg000:00000000000000DC mov r10d, 876F8B31h /* 为 r10d 赋值 876F8B31 */
seg000:00000000000000E2 call rbp /* 使用 call,回到进入函数时 RIP 指向的位置(本文中为 005) */这里仅截取了 loc_CA 的一部分,因为这在这个 shellocde 里面,loc_CA 就和 main 函数类似,在不断的调用其他函数,所以按照执行流程逐步分析。
这部分最开始就是 pop rbp,直接把返回地址弹出赋值给了 rbp,那么显而易见的,call loc_CA 下一行所在的地址被赋值给了 rbp。

然后就是给 rdx 赋值为 1,这个参数后续有用。然后从 rbp+101h 处读数据,可能单纯看汇编可能难以理解,但是只要调试就知道它读取的就是我们写在 shellcode 末尾的 calc.exe

然后给 r10d 赋值 876F8B31,这个是 WinExec 的 hash 值,在后续有用。最后 call rbp 回到我们调用 loc_CA 的下一行地址,即之前上图标的 0x00007FFB72E4105D
seg000:000000000000000A push r9 /* 一系列压栈 */
seg000:000000000000000C push r8 /* */
seg000:000000000000000E push rdx
seg000:000000000000000F push rcx /* 此处入栈了要执行的指令 */
seg000:0000000000000010 push rsi
seg000:0000000000000011 xor rdx, rdx /* rdx 清零 */
seg000:0000000000000014 mov rdx, gs:[rdx+60h] /* 获取 PEB 进程块 */
seg000:0000000000000019 mov rdx, [rdx+18h] /* 获取_PEB_LDR_DATA */
seg000:000000000000001D mov rdx, [rdx+20h] /* 从中获取 InMemoryOrderModuleList 链表 */
seg000:0000000000000021
seg000:0000000000000021 loc_21: ; CODE XREF: seg000:00000000000000C5↓j
seg000:0000000000000021 mov rsi, [rdx+50h] /* 从链表中通过偏移获取指向 BaseDllName 的字符串指针 */
seg000:0000000000000025 movzx rcx, word ptr [rdx+4Ah] /* 从链表中通过偏移获取 BaseDllName 字符串长度 */
seg000:000000000000002A xor r9, r9 /* r9,rax 清零 */到了目标地址继续按序执行上述代码,会进行一系列的压栈,这部分保存了原本执行的寄存器的状态,以便于后续恢复。
压栈后对 rdx 进行了清零,然后执行了三行我们很熟悉的代码(在之前的文章讲过),这三行从 PEB 进程块里面取出了 InMemoryOrderModuleList 链表。
其后执行的俩行的代码从 InMemoryOrderModuleList 链表结构体里面通过偏移获取了 BaseDllName 的字符串指针和 BaseDllName 的字符串长度。在_LDR_DATA_TABLE_ENTRY 结构体中 BaseDllName 相对 InMemoryOrderLinks 的偏移为 0x48,而 BaseDllName 为_UNICODE_STRING 结构,字符串最大长度刚刚好为 0x48+0x2=0x4A,而字符串指针刚刚好为 0x48+0x8=0x50。
最后在这段代码的末尾清空了 r9 寄存器,如下是_UNICODE_STRING 结构体,想要搜索可以去这个网站,注意可能需要魔法。
//0x10 bytes (sizeof)
struct _UNICODE_STRING
{
USHORT Length; //0x0
USHORT MaximumLength; //0x2
WCHAR* Buffer; //0x8
};seg000:000000000000002D loc_2D:
seg000:000000000000002D xor rax, rax /* */
seg000:0000000000000030 lodsb /* 从 rsi 指向的地址加载一个字节到 al */
seg000:0000000000000031 cmp al, 61h ; 'a' /* 和 a 进行比较 */
seg000:0000000000000033 jl short loc_37 /* 小于 a 则跳转到 loc_37 */
seg000:0000000000000035 sub al, 20h ; ' ' /* 将小写字母转为大写字母 */
seg000:0000000000000037
seg000:0000000000000037 loc_37: ; CODE XREF: seg000:0000000000000033↑j
seg000:0000000000000037 ror r9d, 0Dh /* 将 r9d 寄存器的值循环右移 13 位 */
seg000:000000000000003B add r9d, eax /* r9d 加上 eax */
seg000:000000000000003E loop loc_2D /* 循环跳转执行 loc_2D 标签,每次执行 ecx 自减 1,直到 ecx 寄存器的值为 0。这三行代码实际上实现了哈希值的计算 */这部分代码通过不断的跳转实现了一个循环,而这部分代码实现的功能就是 hash 值计算。注意 al (AL) 是 eax 的低 8 位。
其先是清空了 rax,然后从 rsi 指向的地址加载一个字节,而 rsi 所指向的正好是取 BaseDllName 字符串所在的地址,因此就是从 BaseDllName 字符串中取字符,然后进行比较,如果小于 a 则直接跳转到 loc_37 如果大于 a 则先减去 0x20 转为大写字母然后执行按序执行到 loc_37 所在的代码。
而 loc_37 所在的三行代码其先是将 r9d 寄存器右移了 13 位,然后拿 r9d 加上 eax,最后通过 loop 循环 ecx 次,而 ecx 刚刚好是刚刚拿到的字符串长度,这样这段代码就完美的实现了 hash 值的计算。
seg000:0000000000000040 push rdx /* 压入链表地址 */
seg000:0000000000000041 push r9 /* 压入计算出来的哈希值 */
seg000:0000000000000043 mov rdx, [rdx+20h] /* 在链表中通过偏移获取当前进程的基址 (链表首个即为本身) */
seg000:0000000000000047 mov eax, [rdx+3Ch] /* 获取 IMAGE_NT_HEADERS 的起始地址,这里获取的是 PE 头的偏移 */
seg000:000000000000004A add rax, rdx /* 相加得到 NT 文件头地址 */
seg000:000000000000004D mov eax, [rax+88h] /* 获取导出表的虚拟地址 */
seg000:0000000000000053 test rax, rax /* 测试 rax 的值是否为 0 */
seg000:0000000000000056 jz short loc_BF /* 如果为零则跳转至 loc_BF (该函数为错误处理函数) */
seg000:0000000000000058 add rax, rdx /* 相加得到导出表地址 */
seg000:000000000000005B push rax /* 压栈留着后面用 */
seg000:000000000000005C mov ecx, [rax+18h] /* 通过偏移获取 以名字导出的函数的个数 */
seg000:000000000000005F mov r8d, [rax+20h] /* 通过偏移获取 导出的函数名称表 */
seg000:0000000000000063 add r8, rdx /* 加上基址得到函数导出名称表的真实地址 */这段代码先将 rdx 即之前拿到的链表地址和存储了刚刚计算的 hash 值的 r9 入栈,然后开始我们熟悉的导出表获取。这段代码通过偏移和获取虚拟地址,最后我们入栈了导出表地址,并且得到了函数导出名称表的真实地址和以名字导出的函数的个数。
seg000:0000000000000066 loc_66: ; CODE XREF: seg000:000000000000008C↓j
seg000:0000000000000066 jrcxz loc_BE /* 判断 rcx 是否为 0,如果为 0 则跳转至 loc_BE */
seg000:0000000000000068 dec rcx /* rcx 递减 */
seg000:000000000000006B mov esi, [r8+rcx*4] /* 从函数名称导出表获取指向名称的虚拟地址 */
seg000:000000000000006F add rsi, rdx /* 加上基址得到指向名称的真实地址 */
seg000:0000000000000072 xor r9, r9 /* r9 清零 */
seg000:0000000000000075
seg000:0000000000000075 loc_75: ; CODE XREF: seg000:0000000000000082↓j
seg000:0000000000000075 xor rax, rax /* rax 清零 */
seg000:0000000000000078 lodsb /* 从 rsi 指向的地址加载一个字节到 al */
seg000:0000000000000079 ror r9d, 0Dh /* r9d 右移 13 位 */
seg000:000000000000007D add r9d, eax /* r9d 加上 eax */
seg000:0000000000000080 cmp al, ah /* 比较 al 和 ah */
seg000:0000000000000082 jnz short loc_75 /* 如果不相等则跳转至 loc_75 */
seg000:0000000000000084 add r9, [rsp+8] /* r9 加上函数导出表地址 */
seg000:0000000000000089 cmp r9d, r10d /* 比较 r9d 和 r10d (r10 来自 loc_CA 的赋值),进行 hash 值比较选择目标函数,第一次是 WinExec*/
seg000:000000000000008C jnz short loc_66 /* 如果不相等则跳转至 loc_66 重新执行,其实这就是一个汇编写的循环 */先是判断 rcx 是否为 0,如果为 0 就说明执行有误,跳转到 loc_BE 进行错误处理。再将 rcx 进行递减,然后执行熟悉的从导出表获取名称的真实地址,然后再逐个字符加载进行 hash 计算,当满足 al 和 ah 相等后,然后加上 rsp+8 所在的数据,而我们倒数第二入栈的正是之前计算出来的 hash 值,俩者相加再和我们在 loc_CA 精心赋值的 r10d 进行比较,如果不相同则跳转到 loc_66 重新进行函数导出表内的下一个函数的判断,如果相同,就说明找到了目标函数,譬如第一次查找的是 WinExec。
seg000:000000000000008E pop rax /* 出栈 rax */
seg000:000000000000008F mov r8d, [rax+24h] /* 获取函数序号表的虚拟地址 */
seg000:0000000000000093 add r8, rdx /* 加上基址获取函数序号表的真实地址 */
seg000:0000000000000096 mov cx, [r8+rcx*2] /* 从函数序号表获取相应的序号 */
seg000:000000000000009B mov r8d, [rax+1Ch] /* 获取函数地址表的虚拟地址 */
seg000:000000000000009F add r8, rdx /* 加上基址获得函数地址表的真实地址 */
seg000:00000000000000A2 mov eax, [r8+rcx*4] /* 通过序号从函数地址表里找到对应函数的虚拟地址 */
seg000:00000000000000A6 add rax, rdx /* 加上基址获取对应函数的真实地址 */
seg000:00000000000000A9 pop r8 /* 一系列出栈 */
seg000:00000000000000AB pop r8 /* */
seg000:00000000000000AD pop rsi
seg000:00000000000000AE pop rcx
seg000:00000000000000AF pop rdx
seg000:00000000000000B0 pop r8
seg000:00000000000000B2 pop r9
seg000:00000000000000B4 pop r10 /* 此处出栈了要执行的指令 */
seg000:00000000000000B6 sub rsp, 20h /* 分配影子空间 */
seg000:00000000000000BA push r10 /* 压入参数 r10 */
seg000:00000000000000BC jmp rax /* 跳转执行 WinExec */这部分先是出栈函数导出表的地址给 rax,然后通过通过偏移进行函数序号表的真实地址的获取,然后通过序号在函数地址表找到对应函数的真实地址。后续进行一系列出栈,恢复寄存器为最开始的状态,在最后,分配影子空间,入栈要执行的指令,调用 WinExec 的真实地址进行函数执行。
seg000:00000000000000E4 mov ebx, 56A2B5F0h /* ebx 赋值 56A2B5F0 (ExitProcess)*/
seg000:00000000000000E9 mov r10d, 9DBD95A6h /* r10d 赋值 9DBD95A6 (GetVersion) */
seg000:00000000000000EF call rbp /* 返回 rbp 指向重新执行之前执行过的步骤 */
seg000:00000000000000F1 add rsp, 28h /* rsp 移动 0x28 rbp 此时指向的为本文中 0x02A 处的代码(注意栈平衡,本文的代码为 IDA 静态分析的代码) */
seg000:00000000000000F5 cmp al, 6 /* */
seg000:00000000000000F7 jl short loc_103 /* 如果 al 小于 6 则跳转至 loc_103 (退出线程) */
seg000:00000000000000F9 cmp bl, 0E0h /* */
seg000:00000000000000FC jnz short loc_103 /* 如果 bl 不等于 0E0 则跳转至 loc_103 (退出线程) */
seg000:00000000000000FE mov ebx, 6F721347h /* ebx 赋值 6F721347 (RtlExitUserThread) */在执行完函数调用后,其又会回到 loc_CA,因为上述执行流程就是完成了一次 call rbp,然后回到 loc_CA 执行后续代码。这里对 ebx 进行了赋值,其也是一个 hash 值不过是 ExitProcess 的,用于退出线程,并且将 r10d 赋值成了一个新的 hash 值,而这个 hash 值正好是 GetVersion。然后调用 call rbp,和之前一样进行函数执行,不过这次执行的是 GetVersion 了。
当执行完 GetVersion 后又继续回到 loc_CA 继续执行代码,这一次先是将 rsp 下移了 0x28,相当于不再执行之前的 push 入栈寄存器参数,然后通过俩个比较尝试跳转到 loc_103,如果都不满足则将 ebx 赋值为 6F721347 (RtlExitUserThread) 再按序执行到 loc_103。
seg000:0000000000000103 loc_103: ; CODE XREF: seg000:00000000000000F7↑j
seg000:0000000000000103 ; seg000:00000000000000FC↑j
seg000:0000000000000103 push 0 /* 压栈 0 */
seg000:0000000000000105 pop rcx /* 出栈给 rcx */
seg000:0000000000000106 mov r10d, ebx /* 将 ebx 赋值给 r10d */
seg000:0000000000000109 call rbp /* 调用 rbp */而 loc_103 执行了什么?为什么都要执行 loc_103 呢?这部分代码很简单,就是变向给 rcx 赋值了 0,然后把要执行的 hash 值换成了 ebx 了,然后调用 rbp。很明显这就是为了执行退出线程,这是用来解决代码执行完后退出线程的处理。
seg000:00000000000000BE loc_BE: ; CODE XREF: seg000:loc_66↑j
seg000:00000000000000BE pop rax /* 出栈为 rax, 此时 rax 指向函数导出表 */
seg000:00000000000000BF
seg000:00000000000000BF loc_BF: ; CODE XREF: seg000:0000000000000056↑j
seg000:00000000000000BF pop r9 /* 弹出之前计算的哈希值 */
seg000:00000000000000C1 pop rdx /* 弹出链表地址 */
seg000:00000000000000C2 mov rdx, [rdx] /* 指向链表指针指向下一个链表 */
seg000:00000000000000C5 jmp loc_21 /* 跳转至 loc_21 重新对新指向的链表进行函数查找 */其实上面还漏了这俩个错误处理函数,其实这俩也很简单,其中 loc_BE 解决的是获取函数导出表个数时个数为 0 时,就直接把之前压入的 rax 弹出,然后按序执行 loc_BF,跳转到 loc_21。
而 loc_BF 就是解决当前函数导出名称表查找不到当前目标函数,通过链表跳转到下一个链表,再回到 loc_21 重新查找新的链表的函数导出名称表是否有目标函数。
小结
这个 shellcode 因为跳来跳去所以有点不方便分析,可能分析的有点不是很清晰,请见谅。
如果觉得不是很明白可以看看这篇文章,虽然分析的是 x32 位下的,但是也能在一些疑难上有所帮助。
参考链接
- 分析 MSF Windows/exec Shellcode
