社区应用 最新帖子 精华区 社区服务 会员列表 统计排行
  • 17阅读
  • 0回复

[分享]【木马分析】vshell木马分析

楼层直达
z3960 
级别: FLY版主
发帖
815171
飞翔币
217046
威望
215802
飞扬币
2782147
信誉值
8



前言


搬运一下自己博客文章,分享一下,其实里面还有很多细节,特别是TEB和PEB通过hash查询函数的方法,我还是有点云里雾里,只知道个大概,而且没成功复现过,有兴趣的大佬可以指点一下要是有理解错的地方也请指出原文地址:https://cz.caozhexxgweb.cn/?id=139

今天来分析一下这个vshll的shellcode加载器,首先我们导出bin,文件本地很小只有1kb,

因为是汇编,所以直接拖入ida查看即可

入口


因为是shellcode,所以代码先保存了寄存器和空间 复制代码 隐藏代码push r.. × 8    保存所有通用寄存器,防止被后续破坏。lea rbp,[rsp-0x298]sub rsp,0x398   在原来栈顶以下再开 0x398 字节的大栈帧。后面所有局部变量/拼字符串都放这里。


sub_45C函数


先说一下,这函数主要是通过函数hash来查找出这个函数的虚拟地址,这里的726774Ch就是kernel32.dll里的LoadLibraryA。

我们进去sub_45C函数,想按F5发现F5大法失败了,说是要设置语言但实际不行,可能出bug了,只能硬着头皮看汇编了


起始部分

复制代码 隐藏代码mov rax, rsp    暂存当前栈顶地址到 RAX,方便直接在红区里写数据。mov [rax+8], rbxmov [rax+10h], rbpmov [rax+18h], rsimov [rax+20h], rdi  把调用的 4 个寄存器存在栈指针下方的红区(未改变RSP他就bu不保存了)。看样子是比平时一个个 push 更快。(其实我也不知道这样的设计的意义是什么)push r14    额外保存 R14(后面要当循环计数器)。sub rsp, 10h    腾出 0x10 字节的局部变量区,IDA 叫 var_18。


起始后的栈指针

[tr=rgb(246, 248, 250)][td]var_18 (16 B)
内容栈指针
存的 rbx/rbp/rsi/rdi← RSP+0x0
← RSP-0x10
保存的 r14← RSP-0x18


取得 PEB 与第一模块(重点)


从刚刚开始我就一直好奇一个事情,就是我们可以看到这个exe的导入表都是空,那么怎么执行外部函数的呢


这里简单带一下TEB和PEB的知识(网上都有)


主要是我不知道mov rax, qword ptr gs:loc_5C+4是什么查了一下才豁然开朗。
  • TEBTEB是每个线程都有,保存 TLS、异常链、自旋计数等纯线程级信息。通过 GS(x64)或 FS(x86)直接寻址。
PEB
  • 每个进程 1 份,被所有线程共享。字段很多,但最常用的是:
  • PEB->Ldr → PEB_LDR_DATA:维护已加载模块链表。
  • PEB->ProcessParameters:命令行、环境变量。
  • 指向主进程堆、TLS 位图、异常处理列表、调试标志

也就是说他这里只要拿到 PEB,不调用任何 WinAPI 就能做到下面的操作:
  1. 枚举出 kernel32.dll、ntdll.dll 等在 内存中的真实基址;
  2. 解析它们的导出表,手动拿到 LoadLibraryA、GetProcAddress 等地址;
  3. 进而做到免IAT、免明文字符串免API调用监控,做到免杀的效果。(但这个木马已经被杀烂了)

==下面借用一下别人的说明==

  • Windows 每个进程的 PEB(Process Environment Block) 中有一个字段 PEB->Ldr。
    Ldr 里有三条双向链表:

  • InLoadOrderModuleList

  • InMemoryOrderModuleList ← 本函数用的

  • InInitializationOrderModuleList

每个节点不是简单结构,而是大号 LDR_DATA_TABLE_ENTRY,里面既有链表指针,也记录 DLL 的各种信息。
重要偏移(Win10 x64 常见布局):[tr=rgb(246, 248, 250)][td]DllBase
字段偏移作用
InMemoryOrderLinks0x20当前链表的 LIST_ENTRY
0x30模块基址(HMODULE)
BaseDllName0x58UNICODE_STRING,DLL 文件名(不含路径)
  • InMemoryOrderModuleList 是循环链表:最后一个节点再指向 Ldr 里的头结点哑元;哑元的 DllBase 字段恒为 0。
  • ==上面借用一下别人的说明==

我们继续看代码 复制代码 隐藏代码; 获取 PEBmov     rax, gs:[0x60]          ; TEB → PEB  (= qword ptr gs:loc_5C+4); 保存调用参数mov     ebp, ecx                ; 把目标哈希存进 EBP,后面循环都要用; 计数寄存器清零xor     r14d, r14d              ; r14d = 0,既代表“false”也当循环计数起点; 走到 Ldr 模块链表mov     rdx, [rax+0x18]         ; RDX = PEB->Ldrmov     r8,  [rdx+0x10]         ; R8  = Ldr->InMemoryOrderModuleList.Flink                                ;        (链表第 1 个 LDR_DATA_TABLE_ENTRY)
接下去的代码就是不断循环查找链表然后计算hash是否和入参一致(忽然发现F5大法恢复了,瞬间舒服了)

  • 一共有两个return
    return 0就说明已把所有模块都扫完,都没有这个函数,然后返回说明错误了cmp [r8+30h], r14 → jz loc_54C这边做的比较然后跳到loc_54C把EAX置0

  • return v6,也就是循环的下一个模块的基址,v6在循环的时候就被定义了。

复制代码 隐藏代码; 0x0495  外层循环取模块基址mov     r9, [r8+30h]        ; R9 ← DllBase        → v6 = v5[6]; 0x0519  函数名哈希命中后lea     eax, [rbx+rdx]cmp     eax, ebpjz      loc_52E             ; 跳到构造返回值; 0x052E  计算最终地址mov     eax, [r10+24h]      ; AddressOfNameOrdinalsadd     rax, r9             ;   ↑ 这里 r9 仍为 DLL 基址mov     eax, [rcx+rdx*4]    ; Function RVAadd     rax, r9             ; RAX = r9 + RVA  → 返回

获取函数基指


了解了sub_45C我们看看很清楚了,他获取了一堆函数,然后吧基指放在了rbp中,有的也直接放在了r13、r15等寄存器里 复制代码 隐藏代码mov ecx, <hash>call sub_45Cmov  <某寄存器/栈>, rax

对照后续调用可以推断出大致映射(直接问ai了):[tr=rgb(246, 248, 250)][td]r13[tr=rgb(246, 248, 250)][td]rdi[tr=rgb(246, 248, 250)][td][rbp-20h][tr=rgb(246, 248, 250)][td]r14
保存位置真实 API用途
rbx(第二次)socket/WSASocketA创建 TCP socket
connect连接远程主机
rsisetsockopt 或 select调整 sock 参数
send发送握手数据
r15recv下载主体数据
VirtualAlloc申请可执行内存
[rbp-18h]closesocket收尾清理
sprintf / _snprintf字符串拼装
(其余为 Sleep、GetVersion, VirtualProtect 等)


调用部分



准备回连



最后将调用获取函数


上报


下面调用了很多次的rdi也就是发送送握手数据,来发送设备的识别码[rbp-80h] = 0x20343677 → "w64 4";紧跟空格和 0 组成 w64 64。告诉 C2 这是 Windows-64 位客户端。

加载大马


最后就是加载大马的部分


木马整体流程


到这里整体流程已经被摸清楚了

  1. 启动与栈帧准备入口只做两件事:
    • 保存全部通用寄存器,防止被破坏;
    • 在 RSP 下方一次性开 0x398 字节大栈帧,供后续拼接字符串和存放临时数据。

  • 动态解析 API
    • 自带的 sub_45C 函数按“ROR-13 + 大小写无关”遍历 PEB 中所有模块的导出名哈希。
    • 仅凭 32 位哈希值就能拿到 LoadLibraryA、socket、connect、recv、VirtualAlloc 等真实地址,完全省掉了明文字符串。(但是没有用,还是杀爆)

  • 加载必需 DLL利用刚解析出的 LoadLibraryA 动态加载 user32.dll, ws2_32.dll, msvcrt.dll,用作网络通信、字符串处理、内存管理

  • 一次性解析所有关键 API将 socket / connect / send / recv / VirtualAlloc / closesocket / _snprintf … 等 API 地址分别保存到寄存器或栈槽里,后面直接调用。

  • 拼接关键字符串
    • C2 地址:硬编码字节组合成 xxx.xxx.xxx.141,端口 55841。
    • 客户端指纹:"w64 <本机IP> <OS版本>"。
    • 日志文件名:log_<日期>.ed。这些都用 _snprintf 现场拼出,避免静态特征。

  • 建立 TCP 连接并上报信息
    • socket(AF_INET, SOCK_STREAM) → connect 到 C2。
    • 依次 send 客户端指纹、日志名等,完成“注册/握手”。

  • 申请可执行内存VirtualAlloc(NULL, 0x1C9C380 ≈ 30 MB, MEM_COMMIT, PAGE_EXECUTE_READWRITE) 为后续载荷准备 RWX 缓冲区。

  • 循环下载并异或解密
    • 每次 recv 0x64000 字节。
    • 对收到的数据逐字节 XOR 0x99 写回缓冲区。
    • 循环直到 recv 返回 < 要求长度(服务器主动断流)。

  • 收尾并执行载荷
    • closesocket 关闭连接。
    • 直接 call 指向缓冲区首地址 → 跳入已解密的 Stage-2 Shellcode,不落地文件,纯内存执行。

  • 退出 Loader如果第二阶段代码返回,则恢复之前保存的寄存器并 retn;否则进程控制权彻底交给后续载荷。

  • 这是一个只有 0x56C 字节的Stage-0 TCP Loader:通过 API 哈希隐藏所有函数名,联网到硬编码 C2,下载 XOR 加密的下一阶段并在内存中直接执行,为远控恶意模块有点类似反射加载dll。
    本主题包含附件,请 登录 后查看, 或者 注册 成为会员
    我不喜欢说话却每天说最多的话,我不喜欢笑却总笑个不停,身边的每个人都说我的生活好快乐,于是我也就认为自己真的快乐。可是为什么我会在一大群朋友中突然地就沉默,为什么在人群中看到个相似的背影就难过,看见秋天树木疯狂地掉叶子我就忘记了说话,看见天色渐晚路上暖黄色的灯火就忘记了自己原来的方向。