shellcode
shellcode
SourceSHELLCODE
shellcode 说到底其实就是系统调用命令,跟ret2syscall是很相似的,但是区别就是,shellcode需要有可执行权限,而ret2syscall一般发生在text段上,自动就具备可执行权限
SHELLCODE分为手动生成和机器生产,
机器生成shellcode
其实在pwntools中就集成了shellcode的生成,比如:
1 | shellcode = asm(shellcraft.sh()) #生成用于提权的shllcode |
但是一定要注意程序是x86_64的还是i386的,因为shellcraft默认生成32位的
当然还有其他的shellcode
1 | shhellcode = asm(shellcraft.cat(f*)) #生成用于打印所有f开头的文件 |
手写shellcode
这个才是核心,众所周知,机器生产的shellcode唯一的优势是方便,不用手搓,但是也仅此而已,
所以这里介绍一些写shellcode常用的基本的汇编指令(以x86_64汇编为例)
- pop 寄存器名 –>将栈中的下一个4/8字节数的地址弹入对应寄存器中
- push 数字或寄存器 –>将对应数字、寄存器中的值压入栈中
- mov 寄存器a, (数字或寄存器) –> 将对应数字或寄存器中的值赋值给寄存器a
- xor 寄存器a, (数字或寄存器) –> 将对应数字或寄存器中的值与寄存器a中的值进行异或并将结果存在寄存器a中
- add 寄存器a, (数字或寄存器) –> 将对应数字或寄存器中的值与寄存器a中的值进行相加并将结果存在寄存器a中
- sub 寄存器a, (数字或寄存器) –> 将对应数字或寄存器中的值与寄存器a中的值进行相减并将结果存在寄存器a中
- syscall –>x64系统调用命令(机器码为’’)
- int 0x80 –>x86系统调用命令
- ret –>相当于pop eip
接下来就是寄存器的讲解了,我们写shellcode的指令和系统调用都依赖于寄存器中的值
直接参与系统调用的寄存器:
RAX、RDI、RSI、RDX、R10、R8、R9
其中rax是作为syscall调用时的系统调用号,调整rax的值以调用不同的系统函数
剩下6个寄存器按顺序作为系统调用函数的第n个参数
间接参与系统调用的寄存器
RSP、RBP、RIP
RSP和RBP作为栈顶栈底指针寄存器在pop和push指令的调用上起着重要作用
RIP则是指令指针寄存器通过其进行指令运行
基本不参与系统调用的寄存器
RBX、R11、R12、R13、R14、R15
他们的作用大概仅限于传值
1 | 附上linux系统调用号------https://blog.csdn.net/weixin_51055545/article/details/128722431 |
那么首先让我们写一个用于调用execve(“/bin/sh ”,0,0)的shellcode
那么我们需要什么呢? 一个函数,三个参数,所以只需要把对应的值传入就行了,二参和三参很好弄,只需要使用mov指令就行
/bin/sh字符串地址怎么办呢?这时我们就要用到push和rsp的关系了
因为push会将一个值直接压入栈顶,那么执行push后rsp的值就是我们push的这个值的地址
那么我们只要把/bin/sh 转换成16进制ascall码push后再把rsp的值赋值给rdi(第一参数)即可
但是push接立即数的话只能push四个字节,所以我们要先把值存到寄存器中再push
不过根据小端序的原理,每个字母需要倒过来,故而
1 | /bin/sh ----- 0x0068732f6e69622f |
那么就可以下面这样
1 | shellcode = ''' |
精简
这样生成的shellcode的大小有0x25个字节,但是如果限制了shellcode的长度就没办法使用了
因此我们需要对shellcode的长度进行简化,
而,精简化的方法其实就是将字节长度小的指令,可以混合使用,来替换长度大的
1 | 比如: 使用push pop连用来替换mov : |
依照这个原理,可以修改shellcode为:
1 | shellcode = asm(''' |
长度仅仅0x16 也就是21个字节
一些小妙用
double read
这是我取的名字;就是一般的shellcode的题目会将读入的长度设置的非常短,往往不够,那么我们就可以使用shellcode再次进行一次read,从而可以执行execve(‘/bin/sh ’,0,0)
emmm,注意寄存器的值的变化,往往这个是会出现问题的😂
一些常用的shellcode
read(x64)
1 | read = asm( |
write(x64)
1 | write = asm( |
open(x64)
1 | open = asm( |
做题时候的一些注意:
假设存在一个
mmap((void *)0x123000, 0x1000uLL, 6, 34, -1, 0LL);
read(0,buf,0x38);buf在rbp-0x20的位置,现在需要shellcode—>ORW去读取出flag,应该怎么做?我们可以往0x123000这个地址去读shellcode,并且把rsp迁到那里去,jmp_rsp;
所以在这里我们可以这样做
1 | pay = asm(shellcraft.read(0,0x123000,0x100)) # 这里是为了方便将shellcode读入,其实换一个可写的段即可 |
注:一些心得
其实shellcode给我的感觉就是给你一个能直接操作程序的机会,并且它的作用很灵活,它不仅能让攻击者getshell或者get flag还能辅助攻击者去getshell or flag,
接下来是我常用的一些汇编,会不断补充:
常用的汇编:
mov byte ptr [xxx1], xxx2;
向指定内存—->xxx1写入一个字节,内容为xxx2的数据,这部分可以用来分批次写shellcode,
1 | print(len(asm("mov byte ptr [rsp+1],0x1"))) |
jnz,jz,jmp,
这些可以用来帮助自己进行跳转,从而跳到某个函数或者shellcode
XCHG
允许我们交换两个操作数的值,可以交换两个寄存器,寄存器到内存,内存到寄存器的值,效果与mov几乎相同,
ING和DEC
用于操作数加一和减一,操作数可以是内存,也可以是寄存器,
侧信道攻击
不直接对程序进行攻击,而是根据其他信号的变化推测出flag,用于绕过沙箱,
需要几个条件,
1:侧信道爆破需要执行我们编写的shellcode(因为程序中必然无法找到全部对应的gadget),因此能够写入和执行一定字节的shellcode是必要的
2:程序在禁用了execve系统调用后,同时关闭了标准输出流后,才有必要使用侧信道爆破。
3:同时标准错误不能被关闭(stderr 因为我们需要它来反馈信息),还必须要保证read可以从指定文件中读取flag,open或者openat系统调用要保证至少有一个可用。
pwn题目开启沙箱后,我们通常可以采用open、read、write函数输出flag,
但是如果沙箱禁用了write函数,使我们只能利用open和read函数,这时候就要利用侧信道爆破了。
侧信道攻击在pwn中的主要思想就是通过逐位爆破获得flag,一般是判断猜测的字符和flag的每一位进行对比,如果相同就进入死循环,然后利用时间判断是否正确,循环超过一秒则表示当前爆破位爆破字符正确。通常侧信道攻击一般都是通过shellcode来实现的,并且比较的方法最好是使用‘二分法’这样的话节约时间并且效率高
