Linux下的shellcode技巧总结
对
shellcode
知识点的一些总结。
relax and learn…
- 本文尽可能地全面地总结有关
shellcode
的知识点。目前重点关注linux
系统用户态的x86
汇编指令。持续更新中…… - 本文只将现有有关
shellcode
的知识点提炼出来,没有做细致地解释与分析,不会涉及到具体的例题。若师傅们有疑问可以在评论中提出……
可以在Bilibili上观看视频进行学习:
或者在Youtube上观看视频进行学习:
1-如何编写shellcode
shellcode
是一段可被CPU
直接执行的程序码。
使用shellcode
进行攻击是一项经典而强大的技术,借助shellcode
几乎可以完成任何事情,包括但不限于泄露敏感信息、反弹shell
、布置后门等。
编写shellcode
的方式有很多,而方式的选择取决于实际场景。如当需要编写复杂的shellcode
的时候,需要手搓;当需要注入特定模板的shellcode
时,可以采用工具生成;当需要观察shellcode
的字符、长度时,可以采用在线网站生成。
1-1 纯手搓
学到最后,你会发现还是会回归到最原始的方式:手搓shellcode
。
1-1-1 纯汇编
如果使用gcc
编译,模板如下:
|
|
也可以使用nasm
,只是编译的命令不一样。
1-1-2 内联汇编
有时候需要调用一下库函数,可以使用内联汇编Extended Asm (Using the GNU Compiler Collection (GCC)),直接在程序中使用asm(...);
编写内联汇编语句。
但是在剥离shellcode
的时候,需要将call xxxxxxxx
的偏移进行修正。
1-1-3 使用tiny_libc
为了快速、高效、准确地编写出复杂的shellcode
,我参考了musl
库实现了一个简单的libc
库,姑且称之为tiny_libc
,项目地址在CVE-ANALYZE/tiny_libc at main · RoderickChan/CVE-ANALYZE (github.com)。
实现了一些基本的系统调用函数、字符串操作函数,glibc
常用函数,比如:system、popen、sleep、strcpy、memcpy、puts
等等。编译时不依赖任何其他库文件,编译后text
段的大小基本不会超过1 page
。
在main.c
中编写程序逻辑。如果需要剥离出shellcode
,执行get_shellcode.py
文件即可在当前目录生成shellcode
文件,然后读取该文件,直接输入给目标程序即可。
这个库当初是为了研究dirty pipe
漏洞开发的,因为该漏洞不能写超过一页的shellcode
。因此,需要编写复杂的shellcode
,又不想写汇编的时候,可以用这个库直接写C
,然后一键得到要注入的shellcode
。
如在main.c
写入如下内容:
|
|
执行后输出为:
|
|
可以很方便的剥离shellcode
。
1-2 借助工具
1-2-1 pwntools的shellcraft
pwntools
的shellcraft
定义了非常多的模板,支持的架构有x86/x64/arm/arm64
等等。
点击pwnlib.shellcraft — Shellcode generation — pwntools 4.10.0dev documentation进行学习。
使用的示例如下:
|
|
1-2-2 alpha3
建议使用TaQini/alpha3: Automatically exported from code.google.com/p/alpha3 (github.com)这个版本。
使用需要指定基址寄存器:
|
|
学习原文地址在Alphanumeric Shellcode:纯字符Shellcode生成指南 - FreeBuf网络安全行业门户。
1-2-3 AE64
另一位师傅写的可见字符编码的工具,地址在veritas501/ae64: basic amd64 alphanumeric shellcode encoder (github.com),与上一个工具的比较如下:
以上两个工具都能生出x86/x64
的shellcode
,但是alpha3
支持的选项更多一点,阅读两者的文档和使用示例即可熟练使用。
1-2-4 shellcode encoder
指这个工具:rcx/shellcode_encoder: x64 printable shellcode encoder (github.com)
没有前两个好用,有限考虑前两个工具。
1-2-5 msf生成
很多编码的方式,但是对可见字符的编码支持受限。只有x86
支持字符编码。
|
|
1-3 在线网站
搜集了一些非常有用的与shellcode
相关的网站
- Online x86 and x64 Intel Instruction Assembler (defuse.ca): 在线编写
shellcode
和反汇编shellcode
,目前只支持x86/x64
- Online Assembler and Disassembler (shell-storm.org): 另一个更全的在线编写
shellcode
和反汇编shellcode
网站 - Shellcodes database for study cases (shell-storm.org):
shellcode
数据库,支持很多指令集与操作系统 - Exploit Database Shellcodes (exploit-db.com): 另一个
shellcode
数据库 - Online - Reverse Shell Generator (revshells.com): 生成反弹
shell
的命令
2-突破沙箱规则
由于沙箱规则比较多,规则制定比较自由,所以下文重点探讨绕过的技术。
2-1 使用at/v/2系统调用
这里分别指的是几个系统调用的后缀和前缀,比如:
- 使用
execveat
代替execve
,拿到shell
后,使用shell
内置命令读取flag
:echo *; read FLAG < /flag;echo $FLAG
,否则使用子shell
执行命令还是会被沙箱杀死。同样的,使用openat
代替open
。 - 使用
readv/writev
代替read/write
- 使用
mmap2
代替mmap
- 还有一些特殊的系统调用,使用
sendfile
,代替read/write
。这类的系统调用需要平时多关注、收集和整理。
2-2 使用orw读取flag
一般来说,会禁止system/execve/fork
等,这个时候使用open+read+write
输出flag
即可。
或者使用open+sendfile
,指令会更短。
2-3 切换指令模式
随便找一个seccomp-tools
解析的沙箱规则:
|
|
这个沙箱规则判断了当前触发系统调用的时候,arch
是否为x64
,如果不是64
就会kill
;然后,判断了sys-number
是否大于等于0x40000000
,如果大于,程序也会被kill
;然后设置了黑名单,分别是:execve/execveat/fork/clone/rt_sigreturn
。处于黑名单的系统调用会被kill
掉,其他系统调用则会放行。
如果没有 0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
这一句的检查,那么可以使用retf(return far)
指令实现架构切换,或者在x64
环境下直接调用int 0x80
陷入到内核态。
retf
相当于pop ip; pop cs
,cs
是段寄存器,寄存器为0x23
时表示32
位运行模式,0x33
表示64
位运行模式。
从64
位切换到32
位的模板如下:
|
|
2-4 使用0x40000000+X系统调用
接着2-3
,如果没有限制: 0003: 0x35 0x06 0x00 0x40000000 if (A >= 0x40000000) goto 0010
的话,那么可以使用0x40000000 + X
来执行系统调用。
|
|
关于x32 ABI
可查看x32 ABI - Wikipedia。
比如要执行read
:
|
|
需要注意的是,从5.16
开始,linux
内核不支持x32 abi
了:Bug #1994516 “Kernels after 5.16 cannot execute x32-ABI binaries…” : Bugs : linux package : Ubuntu (launchpad.net)
2-5 使用 io_uring 系统调用
最近 io_uring
系统调用受到了广泛关注,因为这个系统调用几乎可抵千军万马。
特别的,高版本 (Linux Kernel Version >= 6.5
) 的 io_uring
中引入了 IORING_SETUP_NO_MMAP
标志,配合 IORING_SETUP_SQPOLL
可以一次 syscall
完成 orw
操作。
3-字符型shellcode
3-1 可打印字符
关于ascii shellcode
,这篇博客必看:Ascii shellcode - NetSec。
如果要求是可打印字符,直接使用上面提到的alpha3/ae64
等工具生成即可。
3-2 字母和数字
同上,直接使用上面提到的alpha3/ae64
等工具生成即可。
3-3 限制字母和数字
很多时候,会限制为纯小写字母或者部分字母或者部分数字。这个时候,需要根据限制条件,把能用的shellcode
组合梳理出来,然后结合shellcode
的执行地址,利用xor/add
等指令,构造出其他所需要的指令。
举个例子,假如shellcode
执行地址0x333100
,只能用0x30-0x40
编写shellcode
,如果需要\x0f\x05
,可以用异或:
|
|
可以用pwntools
的disasm
爆破所有可能的shellcode
组合:
|
|
然后根据需要进行组合即可。
字符型的shellcode
要善于使用xor
和add
,善于借助已有的地址:
0000
对应的是add [rax], al
,借助溢出可以构造出任意的单字符出来\x35XXXX
,对应的是xor eax, XXXX
\x34X
,对应的是xor al, X
\x31\x31
对应的是xor [rax], esi
- ……
4-shellcode编写技巧
总结一下其他的shellcode
编写技巧。
4-1 观察寄存器状态
最后执行shellcode
的指令大概率是call/jmp
等,用gdb
调试的时候,在此处下个断点,观察寄存器的值。
一般来说寄存器会残留一些与程序相关的地址、变量等,合理的利用寄存器的值可以有效地减小shellcode
的长度。
4-2 观察栈的状态
与观察寄存器的值是一样的,观察一下栈上有没有可以利用的地址或者变量。因为pop/push reg
,一般是一个字节,非常的短。
4-3 使用更短的指令
要把rax
清零,可以使用的指令有:
|
|
可以发现,这些指令的长度都不一样,那么我们应该结合当时的上下文环境选用最短的指令,指令越短,自由度越高。
怎么编写较短的指令,需要多积累多总结,比如:
- 尽量使用
pop/push
- 尽量使用
xor reg reg
- 尽量使用
xchg
- 使用
cdq
将edx
置零 - ······
4-4 构造read再次读入
很多时候由于第一次输入的shellcode
长度受限,可以构造出read
再输入一次shellcode
,构造的方式有:
- 直接构造
read(0, data, len)
- 构造
socket+connnect+recv
从远程读shellcode
- 构造从文件读
shellcode
- ······
4-5 侧信道爆破内存
有时候不知道哪个内存是可读可写的,那么可以使用系统调用爆破内存。比如:
- read
- write
- nanosleep
- mprotect
- ……
这些系统调用,如果猜测的内存为非法地址,则系统调用会失败,然后返回负数,那么可以根据系统调用的返回值来判断是否内存爆破成功。
4-6 侧信道爆破flag
有时候1/2
输出流被关闭了,又无法使用socket
,就可以使用侧信道的当时来爆破。主要有几种方式:
- 使用
cmp
,如果判断正确,陷入循环或者read
等,如果判断错误,触发异常 - 使用
alarm
,使用多线程的方式猜测每个字符(有时候不是很准) - 使用
mmap
,在内存的末位存入字符,通过异常判断是否猜测正确 - ······
4-7 借助rax寄存器
rax
寄存器存储系统调用号,也存储返回值。主要有两种方式:
- 上面提到的根据系统调用的返回值爆破内存等
- 利用
read
等返回值构造系统调用号
4-8 借助段寄存器
段寄存器可以拿到代码段、堆、栈等地址,如:
|
|
还可以借助load effective address
指令:
|
|
适用于rsp
为异常值但需要栈的情况。
4-9 ptrace注入
ptrace
注入非常强大可以使用ptrace(attach, pid...)
读写其他进程的reg
或者data
区域的内存内容,详细的技巧可以访问ptrace(2) - Linux manual page (man7.org)进行学习。
除此之外,还可以用lseek
和open/read/write
文件/proc/pid/mem
,进而可以直接读写与修改某个进程的虚拟空间内存内容。
由于代码逻辑会比较复杂,推荐使用tiny_libc
直接写C
语言,之后剥离出shellcode
。
4-10 多架构通用shellcode
指一段shellcode
可以同时在x86/x64/arm/arm64/mips
等架构上运行。可以阅读下面的博客:
-
ixty/xarch_shellcode: Cross Architecture Shellcode in C (github.com)
-
Midnightsun CTF 2019 Polyshell Writeup · Alan’s Blog (tcode2k16.github.io)
主要思想是借助各个架构下的jmp
指令:A
的jmp
是B
的nop
。就可以把A/B
的shellcode
放在一起了。
4-11 有限字符的shellcode
其实是一种shellcode
的编码技巧,比如只用三个字符编码shellcode
: 仅用三种字符实现 x86_64 架构的任意 shellcode-安全客 - 安全资讯平台 (anquanke.com)。
步骤仍然是:
- 找出各字符组合后可能的
shellcode
片段 - 根据已有的片段、上下文环境编码目标
shellcode
4-12 寄存器全为0的syscall
一个没啥用的冷知识:当寄存器都是0
的时候执行syscall
,rcx
会被赋值。