stack_
[TOC]
缓冲区溢出攻击
保护模式
nx :
-z execstack / -z noexecstack (关闭/开启)
#栈不可执行
如果开启了,还会有一个RWX,表示是否有可读可写可执行段
Canary :
-fno-stack-protector / -fstack-protector / -fstack-protector-all (关闭/半开启/全开启)
PIE :
-no-pie / -pie (关闭/开启)
#地址随机化
RELRO :
-z norelro / -z lazy / -z now (关闭/部分关闭/完全关闭)
#对GOT表具有写权限
函数调用过程
call dofunc=push eip,jmp
push ebp
mov ebp,esp
sub esp,0x
-向栈内输入进行覆盖并栈溢出
leave =mov esp,ebp ; pop ebp
ret = pop eip
x86(32位)
- 使用栈来传递参数
- 使用eax存放返回值
amd64(64位)
- 前六个参数依次存放于
rdi
rsi
rdx
rcx
r8
r9
寄存器中 - 第七个往后的参数存放于栈中
x64(64位) | x86(32位) | ||
---|---|---|---|
(s)rbp= | rbp指向 | ||
(r) | rip指向 | pop_rdi_ret | func_addr (返回地址) call_func_addr |
参数1 | ‘/bin/sh’_addr | 执行完func之后的返回地址(deafbeef) ‘/sh’_addr | |
参数2 | func_addr | ‘/bin/sh’_addr 参数2 | |
参数3 | 参数2 参数3 | ||
参数3 |
ROP
介绍
ROP: ROP 是一种利用程序中现有的指令序列(即 “gadget”)来构造恶意代码的技术。攻击者可以通过寻找程序中已有的指令序列,将这些指令序列的地址按照特定的顺序组合在一起,从而构造出一种新的控制流程,绕过程序原本的控制流程,实现攻击目标。
gadget: “gadget” 通常指的是一系列指令序列,这些指令序列可以在计算机程序中被利用来进行攻击或实现特定的操作。通常情况下,gadget 是由程序中的现有指令组成的,这些指令可以被攻击者按照特定的方式组合和利用,以实现某种特定的目的。
由于现有的指令序列(gadgets)通常是由程序本身提供的,因此攻击者无需注入任何额外的代码。通过巧妙地组合现有的指令序列,攻击者可以利用这些 gadgets 来绕过程序的安全机制,执行未授权的操作,
查询程序中的gadget的方式
ROPgadget --binary 文件名称 >gadgets
{机器将自认为可以作为gadget的代码段输出出来}
or
1 | ROPgadget --binary 文件名称 --only "pop|ret" |
ropper -f 文件名称
got和plt
PLT(Procedure Linkage Table)过程链接表
用于实现函数的延迟绑定(lazy binding)。延迟绑定是指在程序执行过程中,直到第一次调用函数时才进行函数的解析和绑定操作。
GOT(Global Offset Table) 全局偏移表
用于存储动态链接所需的全局变量的地址。在程序执行时,动态链接器会根据 GOT 中的地址来解析全局变量的实际地址。GOT 中的地址在程序加载时被填充,通常由动态链接器在运行时进行更新。
PLT 和 GOT 通常是配合使用的。通过 PLT,程序可以在第一次调用函数时进行动态链接,将函数的实际地址填充到 GOT 中。在后续的函数调用中,直接从 GOT 中获取函数的实际地址,避免了重复的符号解析过程。
32位和64位的plt表项都是0x10
栈溢出的运用
ret2test
ret2text 即控制程序执行程序本身已有的的代码 (.text)。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码
如果有system
也有/bin/sh
参数
x64
1 | payload = 'a'*(0xn+8)+p64(func_addr) |
x86
1 | payload = 'a'*(0xn+4)+p32(func_addr) |
如果有system
但是没有/bin/sh
参数
x64
1 | pop_rdi_ret |
x86
1 | fun_addr |
ret2libc
ret2libc 即控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)。一般情况下,我们会选择执行 system(“/bin/sh”),故而此时我们需要知道 system 函数的地址。
程序中没有system函数
系统中没有system函数,并不是真的没有,system函数位于libc(别人写好的)里了
使用输出函数输出libc中某个函数的地址,进而计算得到基地址
模仿程序找到libc中某个函数的方式去寻找
利用输出函数将got表打印出来
利用偏移计算 system和binsh地址
x64
比如利用 write 函数布栈 (所利用的函数必须是在布栈前已经利用过的)
1 | pop_rdi_rsi_(rdx_)ret |
然后得到某个函数的真正地址,将这个真正地址减去ida里看到的偏移地址,则为libc函数的基地址
再计算system
真正的地址和"/bin/sh"
真正的地址,再进行布栈
1 | pop_rdi_ret |
第一次布栈是为了找到libc的基地址,然后第二次布栈用的都是真正的地址
x86
**比如利用write函数布栈 ** (所利用的函数必须是在布栈前已经利用过的)
1 | write_sym //write函数在系统中的地址 |
然后得到某个函数的真正地址,将这个真正地址减去ida里看到的偏移地址,则为libc函数的基地址
再计算system
真正的地址和"/bin/sh"
真正的地址,再进行布栈
1 | system_addr |
ret2csu
在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的 gadgets。 这时候,我们可以利用 x64 下的 __libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。
先来看一下这个函数 (当然,不同版本的这个函数有一定的区别)
1 | .text:00000000004005C0 ; void _libc_csu_init(void) |
这里我们可以利用以下几点
- 从 0x000000000040061A 一直到结尾,我们可以利用栈溢出构造栈上数据来控制 rbx,rbp,r12,r13,r14,r15 寄存器的数据。
- 从 0x0000000000400600 到 0x0000000000400609,我们可以将 r13 赋给 rdx, 将 r14 赋给 rsi,将 r15d 赋给 edi(需要注意的是,虽然这里赋给的是 edi,但其实此时 rdi 的高 32 位寄存器值为 0(自行调试),所以其实我们可以控制 rdi 寄存器的值,只不过只能控制低 32 位),而这三个寄存器,也是 x64 函数调用中传递的前三个寄存器。此外,如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数。比如说我们可以控制 rbx 为 0,r12 为存储我们想要调用的函数的地址。
- 从 0x000000000040060D 到 0x0000000000400614,我们可以控制 rbx 与 rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_400600,进而可以继续执行下面的汇编程序。这里我们可以简单的设置 rbx=0,rbp=1。
我们利用的其实只有这两段代码
1 | .text:0000000000400600 mov rdx, r13 |
我们利用的其实就是这两部分的代码,我们给这两段起个名字,上面的部分叫gadget2,下面的部分叫gadget1(因为我们先执行下面的部分,因此就叫下面的gadget1吧)
假设我们现在通过溢出,已经可以控制程序的执行流了,我们此时就把返回地址填写成gadget1的地址0x40059A(因为我们并不需要add rsp,8这个指令,因此直接从0x40059A开始即可)
现在就会把栈中的前6个数据分别弹给rbx,rbp,r12,r13,r14,r15这六个寄存器。
我们通常会把rbx的值设置成0,而rbp设置成1.这样的目的是在执行call qword ptr [r12+rbx*8]这个指令的时候,我们仅仅把r12的值给设置成指向我们想call地址的地址即可,从而不用管rbx。
又因为这三个指令add rbx,;cmp rbx, rbp;jnz short loc_400580,jnz是不相等时跳转,我们通常并不想跳转到0x400580这个地方,因为此刻执行这三个指令的时候,我们就是从0x400580这个地址过来的。因此rbx加一之后,我们要让它和rbp相等,因此rbp就要提前被设置成1.
然后r12要存放的就是指向(我们要跳转到那个地址)的地址。这里有个很重要的小技巧,如果你不想使用这个call,或者说你想call一个函数,但是你拿不到它的got地址,因此没法使用这个call,那就去call一个空函数(_term_proc函数)(并且要注意的是,r12的地址填写的并不是_term_proc的地址,而是指向这个函数的地址)。
然后r13,r14,r15这三个值分别对应了rdx,rsi,edi。这里要注意的是,r15最后传给的是edi,最后rdi的高四字节都是00,而低四字节才是r15里的内容。(也就是说如果想用ret2csu去把rdi里存放成一个地址是不可行的)
接着到了gadget1的结尾ret这里,然后我们紧接着写入gadget2的地址0x400580。
1 | .text:0000000000400600 mov rdx, r13 |
此时开始执行这部分代码,这没什么好说的了,就是把r13,r14,r15的值放入rdx,rsi,edi三个寄存器里面。
然后由于我们前面的rbx是0,加一之后等于了rbp,因此jnz不跳转。那就继续向下执行,如果我们上面call了一个空函数的话,那我们就利用下面的ret。由于继续向下执行,因此又来到了gadget1这里。
1 | .text:0000000000400616 add rsp, 8 |
如果不需要再一次控制参数的话,那我们此时把栈中的数据填充56(7*8你懂得)个垃圾数据即可。
如果我们还需要继续控制参数的话,那就此时不填充垃圾数据,继续去控制参数,总之不管干啥呢,这里都要凑齐56字节的数据,以便我们执行最后的ret,最后ret去执行我们想要执行的函数即可。
ret2syscall
控制程序执行系统调用,获取 shell。
首先介绍三个函数 (read , write ,execve)
read :(调用号,写入地址,写入字节数) 向某个地址写入东西,主要是字符
write:(调用号,字符串 ,字符数量) 从某个地方读取东西,主要是字符
execve:(/bin/sh,0,0) 调用系统
这些函数都是调用syscall来实现的
x64
用syscall
64位的系统调用号
查询方式
1 | vim /usr/include/x86_64-linux-gnu/asm/unistd_64.h |
常用
1 | #define __NR_read 0 |
寻找syscall地址的方式
1 | ROPgadget --binary 文件名 --only ”syscall“ |
如果系统里有/bin/sh
布栈
rbp | ||
---|---|---|
pop_rax_rdi_rsi_rdx_ret_addr | ||
rax | 系统调用号(0x3b) | |
rdi | binsh_addr | |
rsi | 0 | |
rdx | 0 | |
syscall_addr |
如果系统里没有/bin/sh
解决方法:利用syscall,自己向某个可写段并且固定的地方写入一个/bin/sh
类如向align的地方写入(一定可以写入)
布栈
rbp | ||
---|---|---|
利用read函数 | pop_rax_rdi_rsi_rdx_ret_addr | |
写入/bin/sh | rax | 0 |
rdi | 0 | |
rsi | binsh_add(写入一个/bin/sh ) |
|
rdx | 8 | |
syscall_ret_addr | ||
利用execve函数 | pop_rax_rdi_rsi_rdx_ret_addr | |
执行系统 | rax | 0x3b |
rdi | binsh_addr(调用/bin/sh ) |
|
rsi | 0 | |
rdx | 0 | |
syscall_ret_addr |
x86
用int 0x80
32位系统的系统调用号放在eax传参的依次是ebx,ecx,edx,esi,edi,ebp
程序是 32 位,所以我们需要使得
- 系统调用号,即 eax 应该为 0xb
- 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
- 第二个参数,即 ecx 应该为 0
- 第三个参数,即 edx 应该为 0
32位的系统调用号
查询方式
1 | vim /usr/include/x86_64-linux-gnu/asm/unistd_32.h |
常用
1 | #define __NR_restart_syscall 0 |
如果系统里有/bin/sh
布栈
ebp | ||
---|---|---|
pop_eax_ebx_ecx_ret_addr | ||
eax | 11 | |
ebx | binsh_addr | |
ecx | 0 | |
edx | 0 | |
int80h_addr |
如果系统里没有/bin/sh
解决方法:利用syscall,自己向某个可写段并且固定的地方写入一个/bin/sh
类如向align的地方或者bss段写入(一定可以写入)
布栈
ebp | ||
---|---|---|
利用read函数 | pop_eax_ebx_ecx_edx_ret_addr | |
写入/bin/sh |
eax | 3 |
ebx | 0 | |
ecx | binsh_addr | |
edx | 8 | |
int80_ret_addr | ||
利用execve函数 | pop_eax_ebx_ecx_edx_ret_addr | |
执行系统 | eax | 11 |
ebx | binsh_addr | |
ecx | 0 | |
edx | 0 | |
int80h_ret_addr |
ret2shellcode
ret2shellcode,即控制程序执行 shellcode 代码。shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell。一般来说,shellcode 需要我们自己填充。这其实是另外一种典型的利用方法,即此时我们需要自己去填充一些可执行的代码。
在栈溢出的基础上,要想执行 shellcode,需要对应的 binary 在运行时,shellcode 所在的区域具有可执行权限。
shellcode有2个方向 —>长度 —>字符数量 —>绕过检测
payload = asm(shellcraft.sh().ljust(,’\x00’)) +_ addr
shell大全:http://shell-storm.org/shellcode/
shellcode变形:https://github.com/Skylined/alpha3
首先介绍两个函数 :
(mprotect) 和 (getpagesize)
mprotect
是一个在操作系统中使用的函数,用于更改内存页面的保护属性。
mprotect
函数的原型如下:
1 | int mprotect(void *addr, size_t len, int prot); |
参数解释:
addr
:指向要更改保护属性的内存区域的起始地址。len
:要更改保护属性的内存区域的大小(以字节为单位)。prot
:指定新的保护属性。
mprotect
函数允许对内存页面进行以下操作:
- 更改页面的可读性(
PROT_READ
)。 - 更改页面的可写性(
PROT_WRITE
)。 - 更改页面的可执行性(
PROT_EXEC
)。 - 可读可写可执行(0x111=7)
通过使用 mprotect
函数,可以在运行时更改内存页面的保护属性,以实现对内存的灵活管理和保护。
栈迁移
前置知识
首先栈迁移就是因为可写空间太小不够rop,就把栈迁移到别的地方去构造payload。
而栈迁移最重要的是两个汇编命令
1 | leave; ret; |
stack pivoting
stack pivoting,正如它所描述的,该技巧就是劫持栈指针指向攻击者所能控制的内存处,然后再在相应的位置进行 ROP。一般来说,我们可能在以下情况需要使用 stack pivoting
可以控制的栈溢出的字节数较少,难以构造较长的 ROP 链
开启了 PIE 保护,栈地址未知,我们可以将栈劫持到已知的区域。
其它漏洞难以利用,我们需要进行转换,比如说将栈劫持到堆空间,从而在堆上写 rop 及进行堆漏洞利用
此外,利用 stack pivoting 有以下几个要求:
- 可以控制程序执行流。
- 可以控制 sp 指针。一般来说,控制栈指针会使用 ROP,常见的控制栈指针的 gadgets 一般是pop rsp/esp
SROP
signal机制
signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。
- 内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。
- 内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。
- 返回后,内核为执行 sigreturn 系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。其中,**32 位的 sigreturn 的调用号为 *119(0x77)*,64 位的系统调用号为 15(0xf)。
执行一系列函数的做法
- 控制栈指针。
- 把原来 rip 指向的
syscall
gadget 换成syscall; ret
gadget。
如下图所示 ,这样当每次 syscall 返回的时候,栈指针都会指向下一个 Signal Frame。因此就可以执行一系列的 sigreturn 函数调用。
需要注意的是,我们在构造 ROP 攻击的时候,需要满足下面的条件
- 可以通过栈溢出来控制栈的内容
- 需要知道相应的地址
- “/bin/sh”
- Signal Frame
- syscall
- sigreturn
- 需要有够大的空间来塞下整个 sigal frame
对于 sigreturn 系统调用来说,在 64 位系统中,sigreturn 系统调用对应的系统调用号为 15,只需要 RAX=15,并且执行 syscall 即可实现调用 syscall 调用。
pwntools中的工具使用:
1 | sigframe = SigreturnFrame() |
将以上的sigframe以(str)或者(bytes)形式send出去
然后再将rax设置成15或者199来执行sigreturn 系统调用
Canary
介绍
当启用栈保护后,函数开始执行的时候会先往栈底插入 cookie 信息,当函数真正返回的时候会验证 cookie 信息是否合法 (栈帧销毁前测试该值是否被改变),如果不合法就停止程序运行 (栈溢出发生)。攻击者在覆盖返回地址的时候往往也会将 cookie 信息给覆盖掉,导致栈保护检查失败而阻止 shellcode 的执行,避免漏洞利用成功。在 Linux 中我们将 cookie 信息称为 Canary。
Canary 不管是实现还是设计思想都比较简单高效,就是插入一个值在 stack overflow 发生的高危区域的尾部。当函数返回之时检测 Canary 的值是否经过了改变,以此来判断 stack/buffer overflow 是否发生。
Canary原理
在 GCC 中使用 Canary
可以在 GCC 中使用以下参数设置 Canary:
1 | -fstack-protector 启用保护,不过只为局部变量中含有数组的函数插入保护 |
Canary 实现原理
开启 Canary 保护的 stack 结构大概如下:
1 | High |
当程序启用 Canary 编译后,在函数序言部分会取 某个寄存器某处的值,存放在栈中 %ebp-0x8 的位置。
在函数返回之前,会将该值取出,并与原位置的值进行异或。如果异或的结果为 0,说明 Canary 未被修改,函数会正常返回,这个操作即为检测是否发生栈溢出。
如果 Canary 已经被非法修改,此时程序流程会走到 __stack_chk_fail
。__stack_chk_fail
也是位于 glibc 中的函数,默认情况下经过 ELF 的延迟绑定。
这意味可以通过(劫持 __stack_chk_fail
的 got 值劫持流程)或者(利用 __stack_chk_fail
泄漏内容)
后者参考stack smash
Canary 绕过技术
泄露栈中的 Canary
Canary 设计为以字节 \x00
结尾,本意是为了保证 Canary 可以截断字符串。 泄露栈中的 Canary 的思路是覆盖 Canary 的低字节,来打印出剩余的 Canary 部分。 这种利用方式需要存在合适的输出函数,并且可能需要第一溢出泄露 Canary,之后再次溢出控制执行流程。
one-by-one 爆破 Canary
对于 Canary,虽然每次进程重启后的 Canary 不同 (相比 GS,GS 重启后是相同的),但是同一个进程中的不同线程的 Canary 是相同的, 并且 通过 fork 函数创建的子进程的 Canary 也是相同的,因为 fork 函数会直接拷贝父进程的内存。我们可以利用这样的特点,彻底逐个字节将 Canary 爆破出来。 在著名的 offset2libc 绕过 linux64bit 的所有保护的文章中,作者就是利用这样的方式爆破得到的 Canary: 这是爆破的 Python 代码:
1 | print "[+] Brute forcing stack canary " |
劫持__stack_chk_fail 函数
已知 Canary 失败的处理逻辑会进入到 __stack_chk_fail
ed 函数,__stack_chk_fail
ed 函数是一个普通的延迟绑定函数,可以通过修改 GOT 表劫持这个函数。
Stack smash
原理
在程序加了 canary 保护之后,如果我们读取的 buffer 覆盖了对应的值时,程序就会报错,而一般来说我们并不会关心报错信息。而 stack smash 技巧则就是利用打印这一信息的程序来得到我们想要的内容。这是因为在程序启动 canary 保护之后,如果发现 canary 被修改的话,程序就会执行 __stack_chk_fail
函数来打印 argv[0] 指针所指向的字符串,正常情况下,这个指针指向了程序名。其代码如下
1 | void __attribute__ ((noreturn)) __stack_chk_fail (void) |
所以说如果我们利用栈溢出覆盖 argv[0] 为我们想要输出的字符串的地址,那么在 __fortify_fail
函数中就会输出我们想要的信息。
[^批注]: 这个方法在 glibc-2.31 之后不可用了, 具体看这个部分代码 fortify_fail.c 。
ORW
简介
seccomp 的函数来禁用一部分系统调用,往往会把 execve 这种系统调用禁用掉,基本上拿 shell 是不可能了,但是一定会有一个文件中有flag,可以通过orw打开文件来获得flag
mmap
Open/openv
Read/readv
Write/writev
1 | open(file,0,mode) |
seccomp
通过seccomp-tools dump ./pwn
来查看程序沙箱