关于弹计算器的shellcode学习

关于弹计算器的shellcode学习

本文字数:7123  阅读时长:15分钟

访客数:加载中... | 阅读量:加载中...

这部分主要对 pop calc.exe 的 shellcode 进行分析,因为Agent 内存马 (二) 篇幅过长,因此分开讲。

其中部分思路在关于调用 JNI_GetCreatedJavaVMs 的 shellcode 浅析内已经仔细讲过了,因此不再赘述。

当我们拿到一个 shellocde,我们自然会将其写入文件中,然后用 IDA 分析其汇编。但是单纯的汇编不一定能够让我们直观快速的理解其思路和功能,因此调试是必不可少的。

执行下述代码会得到 agentshellcode 文件,将其放入 IDA 编译,并且对数据段按 C,然后选择 analyze 就可以反汇编了。

java
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,其中注释为我对代码的分析,通过注释能够快速的理解整个汇编代码的功能。

java
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:00000000000000C5j
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:0000000000000033j
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:000000000000008Cj
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:0000000000000082j
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_66j
seg000:00000000000000BE                 pop     rax                     /* 出栈为 rax, 此时 rax 指向函数导出表 */
seg000:00000000000000BF
seg000:00000000000000BF loc_BF:                                 ; CODE XREF: seg000:0000000000000056j
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:0000000000000005p
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:00000000000000F7j
seg000:0000000000000103                                         ; seg000:00000000000000FCj
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。

java
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

然后选择 16 进制的开头,把我们 shellocde 的 16 进制数据复制进去。

java
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 里动态调试的结果。

同样的仍然进行拆分分析。

java
seg000:0000000000000000                 cld                             /* 清除方向标志位 */
seg000:0000000000000001                 and     rsp, 0FFFFFFFFFFFFFFF0h /* 栈对齐 */
seg000:0000000000000005                 call    loc_CA                  /* 调用 loc_CA */

这部分仅进行了基础的处理,清除方向标志位,进行了栈对齐,然后就跳转到了 loc_CA 函数进行下一步处理。

java
seg000:00000000000000CA loc_CA:                                 ; CODE XREF: seg000:0000000000000005p
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

java
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:00000000000000C5j
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 结构体,想要搜索可以去这个网站,注意可能需要魔法。

java
//0x10 bytes (sizeof)
struct _UNICODE_STRING
{
    USHORT Length;                                                          //0x0
    USHORT MaximumLength;                                                   //0x2
    WCHAR* Buffer;                                                          //0x8
};
java
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:0000000000000033j
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 值的计算。

java
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 入栈,然后开始我们熟悉的导出表获取。这段代码通过偏移和获取虚拟地址,最后我们入栈了导出表地址,并且得到了函数导出名称表的真实地址和以名字导出的函数的个数。

java
seg000:0000000000000066 loc_66:                                 ; CODE XREF: seg000:000000000000008Cj
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:0000000000000082j
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。

java
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 的真实地址进行函数执行。

java
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。

java
seg000:0000000000000103 loc_103:                                ; CODE XREF: seg000:00000000000000F7j
seg000:0000000000000103                                         ; seg000:00000000000000FCj
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。很明显这就是为了执行退出线程,这是用来解决代码执行完后退出线程的处理。

java
seg000:00000000000000BE loc_BE:                                 ; CODE XREF: seg000:loc_66j
seg000:00000000000000BE                 pop     rax                     /* 出栈为 rax, 此时 rax 指向函数导出表 */
seg000:00000000000000BF
seg000:00000000000000BF loc_BF:                                 ; CODE XREF: seg000:0000000000000056j
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 位下的,但是也能在一些疑难上有所帮助。

参考链接

  1. 分析 MSF Windows/exec Shellcode