shellcode编写
- 这里详细介绍一下shellcode可以怎么生成,怎么编写
- 注意本篇文章侧重点在如何写shellcode,并不会详细介绍汇编
介绍
-
shellcode
:是一段专门为利用计算机安全漏洞而编写的机器码,通常是为了在目标系统上执行命令或启动一个新的 shell(因此得名 “shellcode”)。 -
在一开始没有汇编基础的时候可以先使用
pwntools
内置的自动生成shellcode的脚本进行编写,然后直接发送,这样就可以getshell。但是这毕竟是脚本生成的东西,在一些题中对shellcode没有限制的情况下还是能使用的。 -
如果在一些shellcode被限制的情况下就没办法使用脚本生成了,这个时候就需要自己编写shellcode
基础
pwntools生成
- pwntools有自带的生成shellcode的工具,生成的代码格式如下,了解该代码就可以进行实战。做出题目level_1_shellcode
- 在使用pwntools生成shellcode的时候注意以下问题
- 请使用虚拟机执行该脚本,而不要使用windows上的Pycharm工具,因为可能没有汇编器
- 在
wsl
或者VM
的虚拟机上运行,确保有汇编器,如果没有使用该指令安装sudo apt install binutils
1 | context.arch = 'i386' # 指定cpu架构, |
编写shellcode基础
- 通常编写shellcode并不是直接写汇编代码
- 通常采用内联汇编的形式。什么是内联汇编:内联汇编可以直接理解为在C代码中嵌入汇编代码。注意并不是所有编译器都支持这么做。但GCC支持在C代码中直接嵌入汇编代码
- 内联汇编的关键字为
asm
,里面汇编代码的形式采用AT&T
的汇编格式而不是采用Intel
的汇编格式(Linux下)。内联汇编的规定如下- 指令必须用双引号引起来,无论双引号中是一条指令或多条指令
- 一对双引号不能跨行,如果跨行需要在结尾用反斜杠
\
转义。 - 指令之间用分号
;
换行符\n
或换行符加制表符\n \t
分隔。 - 寄存器前面加前缀%,立即数前面加前缀$,操作数由左到右的顺序,但是在C语言中。
- 但是在
GUN C
编译器中%
会被当做操作数的占位符,所以这就导致%rax
等寄存器会在GUN C
编译器中编译时会解析失败,这时在我们编写的时候就要使用%%rax
(即俩个%)表示rax寄存器,但是一般shellcode我们在Python脚本中写会更方便一点。
系统调用
- 在Linux32位下,
int 0x80
是其系统调用命令,其机器码为\xCD\x80
- 在Linux64位下,
syscall
是其系统调用命令,其机器码为\x0f\x05
- 简单介绍一下Linux的系统调用号对于32位的系统调用号:
1 | 3 read |
- Linux64位的系统调用号:
1 | 0 read |
编写基础1
level_1_/bin/sh
- 打开ubuntu虚拟机,直接vim创建一个c文件,即可开始编写
- 需要使用
sudo
权限编辑该c文件,刚刚上手写会出现很多错误,问AI慢慢排错 - 写完该内联汇编后就可以做level_2_shellcode的题目了
1 |
|
level_2_write
- 使用内联汇编,通过系统调用将
hello world
打印在终端上
1 |
|
level_3_open
- 使用内联汇编打开
flag
文件,先在当前目录下创建一个flag
文件,写入一些内容 - 为了验证flag文件是否被打开,我使用read函数将该文件读到一个字符串中,并打印出来;
1 |
|
level_4_read
- 使用内联汇编写入
aaaa
到指定字符串上
1 |
|
level_5_orw
-
完成该level,就可以完成题目level3了。
-
使用内联汇编通过系统调用,打开
flag
文件,读取flag
文件到指定字符串上,打印出flag
文件里面的内容。
1 |
|
编写基础2
- 在有些题目中,将汇编指令转换为字节码的过程中不能出现
\x00
。因为会导致截断,这里就来编写一下一些避免\x00
字节的汇编 - 完成该编写后,就可以开始做题目level4
题目
level_1_shellcode
-
题目来源:PolarD&N (polarctf.com),pwn简单题部分,
Easy_Shellcode
-
下载:https://wwsq.lanzoue.com/i7vjd29q8aef 密码:b72p
-
前置知识:学会ret2text就可以了
-
拿到题目附件发现就一个
ELF
文件,先检查保护机制- 发现是
i386
架构 - 然后保护都没开
- Stack: Executable:这个是栈可执行,一般来说栈上的数据可读可写的,但是并不能做为代码执行。
- RWX: Has RWX segments:这个是程序段具有可读、可写、可执行的权限,一般不同部分的程序段权限是不一样的,比如bss段一般是只有可读可写不可执行的权限。本题只是比较简单的shellcode题目,所以权限没有设置很死,或者很宰
- 发现是
level_1分析
-
使用IDA打开这个32位文件
-
先从
main
函数开始看,发现main
函数没有什么东西,只有一个init
初始化和start
函数,那么主要看start
函数
- 进入start函数,查看一下代码,发现有两个输入点,一个是向
str
输入0x100
,str
的地址位于.bss
段
-
还有一个是向栈上的buf输入0x100字节,但是buf的长度达不到0x100所以存在栈溢出
-
这时我们可以将shellcode注入到
str
中,然后使用ret
返回到str处,执行shellcode,即可得到shellcode。其实这题也可以当做ret2libc
来写。 -
结合前面使用pwntools生成的shellcode,可以直接得到shellcode
1 | from pwn import * |
- 这时变量a就是一串由汇编代码组成的字节码
1 | a = b'jhh///sh/bin\x89\xe3h\x01\x01\x01\x01\x814$ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX\xcd\x80' |
- 剩下的就是接收发送问题了
level_1利用
- 这里利用的时段可执行权限,写入shellcode,然后再返回到shellcode地址,执行shellcode
- exp:
1 | from pwn import * |
- 这题看完可以打一题试试PolarD&N (polarctf.com),pwn简单题
play
和name4
、shellcode1
level_2_shellcode
- 题目来源:shellcode进阶之手写shellcode - 先知社区
- 附件如下:https://wwsq.lanzoue.com/icL7L2g7ay0f 密码:8396
1 |
|
level_2分析
- 先使用IDA反编译一下该段代码。程序逻辑就是开辟一段内存空间,让用户写入shellcode,最后再执行shellcode。
- 这时我们直接在Python中进行内联汇编。在Python脚本中写shellcode的格式基本如下(注意需要指定架构),但是在Python中输入汇编语句可以使用更为熟悉的Intel格式。
1 | from pwn import * |
-
我们所写的shellcode,在这里就是要进行系统调用,然后执行
execve("/bin/sh",0,0)
-
所以我们需要
syscall
这个系统调用语句,然后还需要系统调用号59 execve
,以及/bin/sh
字符串、还有指定俩个寄存器参数为0
-
这时我们需要往栈上传递字符串
/bin/sh
,将马上将rsp
所指地址赋值给rdi
,然后指定rsi、rdi
为0
,还要将59
赋值给rax
,最后才能进行系统调用syscall
。这才完整的指向了execve("/bin/sh",0,0)
-
这里注意
/bin/sh
的ASCII码为0x2f 0x62 0x69 0x6e 0x2f 0x73 0x68
,但是由于程序是小端序存储并且栈是由高地址向低地址生长,所以将/bin/sh
压入栈中的时候要/hs/nib/
以这样的形式去压入栈中 -
还要注意的一点就是,不能直接将立即数压入栈中,要借助寄存器,将立即数分成8字节或者4字节逐一压入栈中
level_2利用
- 利用如下:
1 | from pwn import * |
- 执行该程序后就可以getshell了
level_3_shellcode
level_4_shellcode
- 题目来源:国资佬
- 题目附件:https://wwsq.lanzoue.com/ixHYo2g8p4ne 密码:hxvb
level_4分析
- 首先我们先来看一下保护机制,没有开pie,也没有开Canary,部分got表可写
- 接下来使用IDA进行逆向分析
- 该程序先初始化了一下,然后再设置沙箱
- 调用
dofunc()
函数 - 最后再去执行buf2地址里面的内容
- 接下来我们查看
dofunc()
函数中的运行逻辑,开启了buf2的可执行权限,然后将用户的输入复制到buf2中(这就给了我们写shellcode的空间)
- 现在我们再查看沙箱,发现这个沙箱只禁用了
execve
这个系统调用
- 所以本题的思路就是orw,写shellcode,但是这里要注意
strncpy
函数,该函数会逐个字节复制,并且在复制的过程中碰到\x00
字符就会停止复制了。所以在写shellcode的时候一定要避免出现\x00
level_4利用
-
这时我们就要进行open、read、write的系统调用,结合编写基础一的5个level,就可以构造出来一个shellcode(在Python脚本中写使用的是Intel格式的汇编)
-
先一步一步来,使用open函数打开当前目录下的flag文件(如果当前目录下没有flag文件,就自己创建一个flag文件)
-
我们在调用open函数的时候
- 我们先要有
./flag
这个字符,这时我们可以借助栈来注入该字符,将这个字符串转换为小端序存入寄存器当中,再使用push
指令将该寄存器中的值压入栈中 - 然后再调整参数调用相关的寄存器,使其能够成功打开
./flag
文件
- 我们先要有
1 | mov rbx,0x000067616c662f2e |
- 然后再使用
read
函数将该flag字符串读入到程序内存地址中,即读入到.bss
段中,使用vmmap
命令查看得到.bss
段位于0x404000-0x405000
之间
1 | mov rdi,3 |
- 接下来就是write将flag输出到屏幕上
1 | mov rdi,1 |
- 但是我们来查看一下我们所写的shellcode在字节码的时候会不会有
\x00
,发现了一堆\x00
字符串,这时我们就需要改善我们的shellcode(让shellcode在结束之前不能出现\x00
字符串)
- 这时我们就要另辟蹊径,不直接将
./flag
使用push输入到栈上,而是使用syscall
调用read
将让用户输入./flag
到栈上,从而达到与压栈一样的效果,并且我们将寄存器置0,就需要使用xor指令,xor rax rax
1 | xor rax,rax |
- 然后再使用open打开文件,在调用open打开文件,打开文件的过程中又出现了
\x00
字符截断,所以我将mov rdx,0x28
指令换成add rdx,0x20
- 这时候我注意到,不能有
mov 寄存器,立即数
这一个指令一用该指令就会出现\x00
字符串,最好是使用xor 寄存器,寄存器
将寄存器置0后使用add
命令,或者是用sub
命令
- 所以我在调用read之前先使用read,将
.bss
段上的地址写入栈上(其实也可以直接将flag写在栈中),这样就可以成功读取flag的值了,并且把flag读入到了.bss
段上
1 | xor rax,rax |
- 最后一步就是调用write函数,将
.bss
段上的内存打印到屏幕上,一些寄存器的值是通过动态调试得到的,因为在本地是比较好进行动态调试的,更容易看到寄存器的值,所以会更容易些shellcode
1 | xor rdi,rdi |
- 最后通过编写orw的shellcode,成功的得到了flag
- exp如下:
- 注意:在打远程的时候由于版本的原因或者一些情况,存在寄存器的值和动调时寄存器的值不一样,所以尽量在执行
add
指令之前,先执行xor
对寄存器置0,以便更清楚寄存器的值。
1 | from pwn import * |