01 ShellCode
什么是shellcode
shellcode通常是软件漏洞利用过程中使用的一小段机器代码
作用:
1、启动shell进行交互
2、打开服务器端口等待连接
3、反向连接端口
4、。。。。
shellcode编写
我们在linux系统写编写一个最简短的c语言程序:
1 | //gcc -m32 -o shell shell.c |
很显然,这样做出来的程序太大了,在题目中我们一般只能输入几十个字节,其次他直接使用了系统函数,但是我们都不知道系统函数是啥(被包装成sytem@plt了):


我们可以通过中断的方法进行系统调用。
系统中断方法调用shellcode
触发中断(int 0x80或者syscall),进行系统调用
system(“/bin/sh”)底层调用的是execve(“/bin/sh”,0,0)
我们可以看execve函数分别对应的调用:
| 64位 | ||||||||
|---|---|---|---|---|---|---|---|---|
| NR | System call | %rax | %rdi(arg0) | %rsi(arg1) | %rdx(arg2) | %r10(arg3) | %r8(arg4) | %r9(arg5) |
| 59 | sys_execve | 0x3b | const char *filename | const char *const argv[] | const char* const envp[] | |||
| 32位 | ||||||||
| NR | System call | %eax | %ebx(arg0) | %ecx(arg1) | %edx(arg2) | %esi(arg3) | %edi(arg4) | %ebp(arg5) |
| 11 | sys_execve | 0x0b | const char *filename | const char *const argv[] | const char* const envp[] |
其中在syscall中,每一个寄存器都会有各自的参数作用,最后的int 0x80就是linux系统调用的中断,也就是使用这个终端,就会触发syscall(系统调用)
32位shellcode
因为execve(“/bin/sh”,0,0)如表格所示,所以我可以写一个不需要callsys也可以直接进入shell的shellcode,具体如下:
1 | ;;nasm -f elf32 shellcode32.asm |
现在,我们得到一个非常小的shellcode,并且也没有使用系统函数。


因为是i386,也就是32位的程序,很显然我们可以看到这里对应32位的syscall各自的参数是如表格所示的。
64位shellcode
如此这般,我们可以构造一个64位的shellcode:
1 | ;;nasm -f elf64 shellcode64.asm |
在64位里面,相比于32位,首先是传参寄存器的名字有所更改,其中int 0x80变成了syscall。
其中我们要记得一些常用的蠢货十六进制数,用于到时候用来查看或者学习:
| 十六进制数 | 含义 | 用法 |
|---|---|---|
| 0x68732f2f | //sh | plain mov ebx, 0x68732f2f ; 存储 "//sh"(双斜杠是为了对齐) push rbx ; 压入 "//sh" |
| 0x6e69622f | /bin | plain mov ebx, 0x6e69622f ; 存储 "/bin" push rbx ; 压入 "/bin" |
| 0x0068732f6e69622f | /bin/sh |
这边是用于对齐1byte,也就是8位,所以四个字符四个字符的输入(一个字符一byte,一个byte两个十六进制数,四个byte8个十六进制数)
如此这般,我的64位shellcode也可以写成:
1 | ;;nasm -f elf64 shellcode64_nostr.asm |
很显然,我们现在倒是理解了这个最基础的内容,那么我们直接放到pwn里面岂不是还要当场构造汇编嘛?
完全不用,我们只需要熟悉pwntools就可以快速生成对应架构的shellcode了。
使用pwntool快速生成shellcode
使用pwntools快速生成对应架构的shellcode,总共两步:
1、设置架构目标 2、生成shellcode
#32位
1 | from pwn import* |
其中他的shellcode如下:
1 | .section .shellcode,"awx" |
#64位
1 | from pwn import* |
其中它的shellcode如下:
1 | .section .shellcode,"awx" |
这些方法生成的shellcode非常有用,在与他把0x00(也就是\0)(或者64位补0)的情况给消灭了。不会出一些奇怪的bug。
普通shellcode
了解完上面的东西之后,我们可以学一下这道题目:
64位经典shellcode:mrctf2020_shellcode
这道题目就是一个典型的64位系统的shellcode,输入完shellcode之后就可以直接进入终端。然后这里还有一个32位的
32位经典shellcode:ciscn_2019_s_9
ORW
有一种比较特殊的shellcode,就是这样的:shellcode1_dahuan02
这道题目是ORW,所谓ORW就是Open、Read、Write。
因为几乎所有的程序都需要打开文件,读取数据和输出。而有些题目会封锁systemcall里面的sys_execve。按照这个逻辑,我们可以通过ORW来读取所有我们需要的文件,如下:

这是一个非常基本的64位流程图,因为这些题目都会有一个特性:执行用户输入进去的内容。
然后在这里我们详细讲一下ORW的残割参数和里面是如何传递输出的。
首先我们先要学习一下这三个函数:
就拿32位的举例:
| Num | syscall | %eax | arg0 (%ebx) | arg1 (%ecx) | arg2 (%edx) |
|---|---|---|---|---|---|
| 3 | read | 0x03 | unsigned int fd | char *buf | size_t count |
| 4 | write | 0x04 | unsigned int fd | const char *buf | size_t count |
| 5 | open | 0x05 | const char *filename | int flags | umode_t mode |
eax是调用这个syscall所需要的值,就像是sys_execve的里面的0x0b一样,是调用号。再然后,我们来讲这三个函数,先是open函数。
open函数
我们首先使用open函数打开文件,第一参数位文件名,第二参数为打开模式,第三参数为打开权限
其中第一参数文件名就不多赘述了,打开模式必选第二参数,大概如下:
O_RDONLY:只读模式(值为 0)。
O_WRONLY:只写模式(值为 1)。
O_RDWR:读写模式(值为 2)。
1 | // 只读打开,若文件不存在则报错 |
第三参数为打开权限,一般不用填写。
随后open会返回一个返回符号fd。
read函数
当我们使用完open之后,会得到一个返回值存储在eax里面,这个返回值一般被叫做_fd(文件调用符)_,fd的值会从0开始,作为一个等差数组一个一个往上加,例如0,1,2,3,4这样。
其中,每个程序一开始会自我定义三个fd,分别如下:
| 文件描述符 | 名称 | 含义 | 通常关联的设备 |
|---|---|---|---|
| 0 | STDIN_FILENO |
标准输入 | 键盘 |
| 1 | STDOUT_FILENO |
标准输出 | 终端屏幕 |
| 2 | STDERR_FILENO |
标准错误输出 | 终端屏幕(错误信息) |
就像这样,所以一般我们用户开始使用程序的时候创建的fd都是从3开始的,不过最好就是调用完open函数后把eax或rax的值立刻放到read里面。而read的第一参数就是fd。fd里面有很多内容。
read的第二参数是缓冲区地址,也就是我们要要把读取的数据存在哪里。是的,read其实是用来将读取的内容送到缓冲区的一个函数。一开始的时候,我们获得了fd,其代表哪一个文件被我们授权打开了,上面说到fd有很多的内容,在应用层面就是一个数字,但是这个数字可以指向一堆系统层面的东西,比如说这个fd指向的文件的信息,大小等等。所以我们获得fd的时候,也就获得了对这个文件的使用权限,read函数也就知道了读取什么了。
然后我们把读取到的数据存到缓冲区地址。随后就是第三参数了:第三参数是读取的字节数量。也就是我要读取多少个数据。
read的返回值是成功读取的参数数量,也就是字符长度。
下面是Write函数:
write函数
他的三个参数和read函数差不多。
不过这里我们要注意,第一参数这里不是返回值,而是1,也就是标准输出。我们要将数据write到标准输出(终端屏幕)上。然后第二参数标注读取哪里的缓冲区的数据,读取第三参数的数量
shellcode变型
这是最后一种类型的shellcode,和mrctf2020_shellcode类似(在上面栏目的普通shellcode里面),限度如shellcode,再call rax执行shellcode。
他们的区别在,对输入的shellcode字符进行了过滤:只能输入特定的字符。
这边的例题是:mrctf2020_shellcode_revenge
然后偷了大欢老师那边的现成的shellcode:
1 | #不可见版本 |
也可以用工具生成:alpha3.py
还有一些比较特殊的shellcode,需要用XOR异或加密方法来构造,可以看笔记[NewStarCTF 2023 公开赛道]shell code revenge,算是有点难度。
总结
- 对于长度和字符没有限制的 shellcode,可以使用 pwntools 来生成或者搜索现成的 shellcode
- 长度有限制的 shellcode,可以对照系统调用表手写 shellcode
- 字符集有限制的 shellcode,可以使用 ALPHA3、msf 等工具对 shellcode 进行编码。或者根据限制的字符先生成可用的汇编指令再进行指令等价替换。










