r2与gdb关于glibc的身世之谜

作为一个正经的CTFer,我似乎从来没写过什么正经的东西
今天就来记录一下困扰了我半个月的radare2和gdb关于glibc的符号表问题

gdb GNU正统调试器,功能强大,插件可怕
radare2 被开源界给予厚望的多平台调试器,支持多种架构

最近在跟队里的pwn师傅学点手艺,发现了gdb和r2两款调试器
一般来说gdb+插件基本上是pwn题的首选,但r2作为开源新星也想尝试一下
在两个调试器一起用的时候,就遇到了很奇怪的问题
gdb需要glibc的调试符号才能查看堆状态,但是r2似乎可以直接查看堆状态
经过师傅介绍,Arch有一个个人维护的glibc带调试符号的版本
装上这个调试符号之后,gdb稳如狗,r2就看不了堆状态了
我觉得很奇怪
这时候
就得

1
git clone https://github.com/radare/radare2.git

仔细品味一番

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//linux_heap_glibc.c line 315 function GH(r_resolve_symbol)
is_debug_file[0] = str_start_with (libc_ver_end, "/usr/lib/");
is_debug_file[1] = str_start_with (libc_ver_end, "/usr/lib32/");
is_debug_file[2] = str_start_with (libc_ver_end, "/usr/lib64/");
is_debug_file[3] = str_start_with (libc_ver_end, "/lib/");
is_debug_file[4] = str_start_with (libc_ver_end, "/lib32/");
is_debug_file[5] = str_start_with (libc_ver_end, "/lib64/");

if (!is_debug_file[0] && !is_debug_file[1] && \
!is_debug_file[2] && !is_debug_file[3] && \
!is_debug_file[4] && !is_debug_file[5]) {
path = r_cons_input ("Is a custom library? (LD_PRELOAD=..) Enter full path glibc: ");
//这里非常鬼畜,如果你是一个自定义的glibc版本的话,在每次执行堆命令时
//他都会问你glibc的地址
//我们师傅去给人家提了issue,结果人家当天就给修了..
if (r_file_exists (path)) {
goto found;
}
}

if (is_debug_file[0] || is_debug_file[1] || is_debug_file[2]) {
//我的glibc符合这个规则,但他找的是一点调试信息都没有的纯.so
free (path);
path = r_str_newf ("%s", libc_ver_end);
if (r_file_exists (path)) {
goto found;
}
}

if ((is_debug_file[3] || is_debug_file[4] || is_debug_file[5]) && \
r_file_is_directory ("/usr/lib/debug")) {
free (path);
path = r_str_newf ("%s%s", dir_dbg, libc_ver_end);
if (r_file_exists (path)) {
goto found;
}
path = r_str_append (path, ".debug");
//真正的调试信息在这里
if (r_file_exists (path)) {
goto found;
}
}

if ((is_debug_file[3] || is_debug_file[4] || is_debug_file[5]) && \
r_file_is_directory ("/usr/lib/debug/.build-id")) {
get_hash_debug_file (core, libc_ver_end, hash, sizeof (hash) - 1);
libc_ver_end = hash;
free (path);
path = r_str_newf ("%s%s%s", dir_dbg, dir_build_id, libc_ver_end);
if (r_file_exists (path)) {
goto found;
}
}

这样一来他每次都会报错

Warning: glibc library with symbol main_arena could not be found. Is libc6-dbg installed?

叹气.
在新版本中,他提供了一个dbg.libc.dbglib这样一个配置可以保存dbglib的地址,我把这个地址指定到我的调试信息.debug
他又会报一个错

Warning: Cannot initialize dynamic strings
Warning: Cannot initialize dynamic section

由于只是一个调试符号,.debug没有常规的head
所以他的elf解析出来异常
叹气.X2
于是我又切换到了core源里的glibc,gdb不出意料的挂了,r2又能畅快的跑起来了(黑人问号
真的是黑魔法??
叹气.X3
他怎么可能不用调试信息???
跟师傅研究了半个晚上
经过一番readelf objdump,我们发现,虽然两个libc都带.symtab节,但是包含的调试信息相差巨大
objdump -g 两个libc的比较
而这个调试信息包含了各种变量的结构定义
我们在不带调试信息的gdb上执行p main_arena
可以看到gdb是知道这个变量的位置的,只不过不知道他是什么类型
可以在.symtab中找到main_arena的地址
这样我们就可以推测出,r2肯定保存了main_arena也就是结构体malloc_state的信息
继续代码审计(突然切换到web频道)
在r2的入口函数cmd_dbg_map_heap_glibc可以找到main_arena的类型
linux_heap_glibc.c line 1700
跟踪进去,可以看到他定义了一个r_malloc_state的结构体
r_heap_glibc.h line 173
跟glibc-2.28的malloc_state比较一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct malloc_state
{
__libc_lock_define (, mutex);
int flags;
int have_fastchunks;
mfastbinptr fastbinsY[NFASTBINS];
mchunkptr top;
mchunkptr last_remainder;
mchunkptr bins[NBINS * 2 - 2];
unsigned int binmap[BINMAPSIZE];
struct malloc_state *next;
struct malloc_state *next_free;
INTERNAL_SIZE_T attached_threads;
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};

大致是一样的,只不过r2包含了32位和64位
翻了翻r_heap_glibc.h,发现它实现了malloc_state 64 32 带tcache64 带tcache32四种结构

大概的过程是,先通过r_resolve_symbol,寻找符号地址,找到地址后先就理解为r_malloc_state类型,在通过update_main_arena判断带不带tcache,最后按照正确的结构解析二进制数据,读取main_arena
这个作者可真是煞费苦心

师父似乎对这种奇怪的做法并不看好
我觉得可能是读调试信息比直接自己实现结构还麻烦,作者才这么写的吧

最后师傅给我重新打包了一个带调试信息的glibc,这才让gdb和r2都能抛弃了
不过还是觉得gdb+插件比较好用=_=
最后感谢师傅感谢arch linux的包管理