凯韬教我GDB:下划线的堆调试

在GDB中可以使用p或call指令调用进程内存中的任意函数,这并不新奇,但用此法直接去调函数调试堆,则需要调用一套带下划线的函数: __libc_malloc()__malloc()__libc_free()__free() ,而不能直接用malloc()free(),这是为什么呢?

函数调用

在gdb中,当断点断下,可以使用p或者call指令调用内存中的函数:

gef  p printf("%s\n","hello")
hello
$1 = 0x6
gef  call printf("%s\n","hello")
hello
$2 = 0x6

这并不新奇,很多地方能看到:

并且在2021年的XCTF Final,此法还是赛题babydebug的解法之一。但如果用这个方法直接去调用malloc、free,则无法在堆中看到正常结果,并且还需要手动指明函数返回类型才能调用成功:

gef  p malloc(20)
'malloc' has unknown return type; cast the call to its declared return type
gef  p (void *)malloc(20)
$1 = (void *) 0x7ffff7fb4fd0
gef  heap chunks
[!] Heap not initialized
[!] Could not find heap for arena

gef  p free(0x7ffff7fb4fd0)
'free' has unknown return type; cast the call to its declared return type
gef  p (void)free(0x7ffff7fb4fd0)
$2 = void
gef  heap chunks
[!] Heap not initialized
[!] Could not find heap for arena
gef  

xkt 告诉我正确姿势是使用 __libc_malloc()__libc_free() 这套带下划线的函数,的确成功:

gef  p __libc_malloc(20)
$1 = (void *) 0x55555555f2a0
gef  heap chunks
Chunk(addr=0x55555555f010, size=0x290, flags=PREV_INUSE)
    [0x000055555555f010     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0x55555555f2a0, size=0x20, flags=PREV_INUSE)
    [0x000055555555f2a0     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0x55555555f2c0, size=0x20d50, flags=PREV_INUSE)    top chunk
gef  p __libc_free(0x55555555f2a0)
$2 = void
gef  heap bins
──────────────────────────────────────── Tcachebins for thread 1 ────────────────────────────────────────
Tcachebins[idx=0, size=0x20] count=1    Chunk(addr=0x55555555f2a0, size=0x20, flags=PREV_INUSE) 
─────────────────────────────────── Fastbins for arena 0x7ffff7facb80 ───────────────────────────────────
Fastbins[idx=0, size=0x20] 0x00
Fastbins[idx=1, size=0x30] 0x00
Fastbins[idx=2, size=0x40] 0x00
Fastbins[idx=3, size=0x50] 0x00
Fastbins[idx=4, size=0x60] 0x00
Fastbins[idx=5, size=0x70] 0x00
Fastbins[idx=6, size=0x80] 0x00
────────────────────────────────── Unsorted Bin for arena 'main_arena' ──────────────────────────────────
[+] Found 0 chunks in unsorted bin.
─────────────────────────────────── Small Bins for arena 'main_arena' ───────────────────────────────────
[+] Found 0 chunks in 0 small non-empty bins.
─────────────────────────────────── Large Bins for arena 'main_arena' ───────────────────────────────────
[+] Found 0 chunks in 0 large non-empty bins.
gef  heap chunks
Chunk(addr=0x55555555f010, size=0x290, flags=PREV_INUSE)
    [0x000055555555f010     01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0x55555555f2a0, size=0x20, flags=PREV_INUSE)
    [0x000055555555f2a0     00 00 00 00 00 00 00 00 10 f0 55 55 55 55 00 00    ..........UUUU..]
Chunk(addr=0x55555555f2c0, size=0x20d50, flags=PREV_INUSE)    top chunk

这是为什么呢?

源码探索

下划线libc这个符号并不陌生,很多ptmalloc的源码阅读文章都是从__libc_malloc开始的:

glibc-2.31/malloc/malloc.c

strong_alias (__libc_free, __free) strong_alias (__libc_free, free)
strong_alias (__libc_malloc, __malloc) strong_alias (__libc_malloc, malloc)

glibc-2.31/include/libc-symbols.h

# define strong_alias(name, aliasname) _strong_alias(name, aliasname)
# define _strong_alias(name, aliasname) \
  extern __typeof (name) aliasname __attribute__ ((alias (#name))) \
    __attribute_copy__ (name);

strong_alias大概的意思就是别名:

所以__libc_free、__free、free应该是等效的,陷入困境。

打印地址

突然想到gdb可以打印符号地址,尝试打印一下:

gef  p &free
$1 = (<text variable, no debug info> *) 0x7ffff7fec5f0 <free>
gef  p &__libc_free
$2 = (void (*)(void *)) 0x7ffff7e5e850 <__GI___libc_free>

发现这俩符号对应的地址居然不一样!!!查看地址发现free这个符号居然在ld.so中存在!!!

gef  vmmap 0x7ffff7fec5f0
[ Legend:  Code | Heap | Stack ]
Start              End                Offset             Perm Path
0x00007ffff7fd0000 0x00007ffff7ff3000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-2.31.so
gef  vmmap 0x7ffff7e5e850
[ Legend:  Code | Heap | Stack ]
Start              End                Offset             Perm Path
0x00007ffff7de6000 0x00007ffff7f5e000 0x0000000000025000 r-x /usr/lib/x86_64-linux-gnu/libc-2.31.so

真相大白

把ld.so扔进IDA,的确有malloc和free,并且还标明了是weak符号:

.text:000000000001CF60; __int64 __fastcall malloc(unsigned __int64)
.text:000000000001CF60                public malloc ; weak
.text:000000000001CF60malloc          proc near               ; CODE XREF: _malloc+4j
.text:000000000001CF60                                        ; DATA XREF: LOAD:0000000000000778
.text:000000000001CF60; __unwind {
.text:000000000001CF60                endbr64                 ; 
.text:000000000001CF64                mov     rcx, cs:qword_2F0F0
.text:000000000001CF64
.text:000000000001CF6B                test    rcx, rcx        ; Logical Compare
.text:000000000001CF6E                jz      loc_1D048       ; Jump if Zero (ZF=1)
.text:000000000001CF6E

所以如果在源码中声明了符号的weak属性,在其二进制中是有体现的,并且IDA认识:

ld的源码也在glibc里,但是如果没用源码分析工具不太容易直接找到ld中的malloc源码,所以尝试寻找一些好找的符号。在IDA分析_dl_exception_create_format调用了malloc,在源码中找到其位置:glibc-2.31/elf/dl-exception.c,在附近找到:

glibc-2.31/elf/dl-minimal.c

/* Allocate an aligned memory block.  */
void * weak_function
malloc (size_t n)
{
  if (alloc_end == 0)
    {
      /* Consume any unused space in the last page of our data segment.  */
      extern int _end attribute_hidden;
      alloc_ptr = &_end;
      alloc_end = (void *) 0 + (((alloc_ptr - (void *) 0)
				 + GLRO(dl_pagesize) - 1)
				& ~(GLRO(dl_pagesize) - 1));
    }

至此破案:

  • 由于malloc和free这两个符号在ld中也存在,gdb在函数调用的符号处理时,选择了ld中的符号,而不是libc中的符号
  • 另外,由于ld中的malloc和free是weak,所以在程序正常执行时,调用的malloc和free会去libc中执行,而不是ld
  • gdb不用手动指定libc中的__libc_free的返回类型是因为本地安装了带调试符号的libc

想想也对,ld需要加载目标ELF以及相关动态链接库到进程内存中,必然也需要向操作系统申请内存空间。但没想到的是,ld用的函数名也叫malloc,可能是习惯,或者看起来兼容c标准库函数的写法。虽然可以用符号的weak属性避免冲突的问题,但还是给分析者造成了困惑。进而再想,人家写这玩意也不为让你分析懂,就为了能用,写这么底层代码的人都是真正的黑客,无论是为了效率,简洁,又或是炫技,写法上必然有非常多的trick,所以外人看起来必然是困惑。

不过如果你平时足够细心,其实能够发现,在gdb中给malloc打断点时,会打上两个断点:

gef  b malloc
Breakpoint 2 at 0x7ffff7e5e260: malloc. (2 locations)
gef  i b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   <MULTIPLE>         
1.1                         y   0x00007ffff7e5e260 in __GI___libc_malloc at malloc.c:3023
1.2                         y   0x00007ffff7fec490 <malloc>

你发现了么?

扩展玩法

gdb能动态调用函数,感觉这个可玩性很大

gdb命令的pwn示例

ctf的pwn教学一般的漏洞在二进制,利用在python。而how2heap的漏洞和利用都在c源码中,利用gdb调用函数的方法,可以让漏洞和利用都落实在gdb命令中,如这个在libc2.31下的堆越界写打tcache的fd:

$ cat gdb.cmd
b exit
r
p $a=__malloc(20)
p $b=__malloc(20)
p $c=__malloc(20)
p $d=__malloc(20)
p __free($c)
p __free($b)
set *(long long *)($a+0x20)=&_rtld_global._dl_rtld_lock_recursive
p $e=__malloc(20)
p $f=__malloc(20)
set *(long long *)($f)=0xdeadbeef
c

gdb调试总要有个进程,或者有个二进制,为了省事,找一个没有什么堆操作的自带程序,选择了true这个程序:

$ gdb -q `which true` -x ./gdb.cmd

Program received signal SIGSEGV, Segmentation fault.
0x00000000deadbeef in ?? ()

[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x00007ffff7ffd060    0x00007ffff7ffe190    0x0000555555554000    0x00010102464c457f
$rbx   : 0x00007ffff7ffd060    0x00007ffff7ffe190    0x0000555555554000    0x00010102464c457f
$rcx   : 0x1               
$rdx   : 0x00007ffff7fe0d50     endbr64 
$rsp   : 0x00007fffffffded8    0x00007ffff7fe0dc7     mov ecx, DWORD PTR [rbx+0x8]
$rbp   : 0x00007fffffffdf30    0x0000000000000000
$rsi   : 0x0               
$rdi   : 0x00007ffff7ffd968    0x0000000000000000
$rip   : 0xdeadbeef        
$r8    : 0x0               
$r9    : 0x0               
$r10   : 0x0               
$r11   : 0x00007ffff7f738f0    0x0000800003400468
$r12   : 0x0               
$r13   : 0x1               
$r14   : 0x00007ffff7fb1fc8    0x0000000000000000
$r15   : 0x00007ffff7fae980    0x0000000000000000
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
──────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffded8+0x0000: 0x00007ffff7fe0dc7     mov ecx, DWORD PTR [rbx+0x8]	  $rsp
0x00007fffffffdee0+0x0008: 0x00005555555594d0     endbr64 
0x00007fffffffdee8+0x0010: 0x00007fffffffdef8    0x00007fffffffdf0f    0x0000000000000000
0x00007fffffffdef0+0x0018: 0x0000000055556610
0x00007fffffffdef8+0x0020: 0x00007fffffffdf0f    0x0000000000000000
0x00007fffffffdf00+0x0028: 0x0000000000000000
0x00007fffffffdf08+0x0030: 0x00007ffff7fac718    0x00007ffff7fae980    0x0000000000000000
0x00007fffffffdf10+0x0038: 0x0000000000000000
────────────────────────────────────────────────────────────────── code:x86:64 ────
[!] Cannot disassemble from $PC
[!] Cannot access memory at address 0xdeadbeef
────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "true", stopped 0xdeadbeef in ?? (), reason: SIGSEGV
──────────────────────────────────────────────────────────────────────── trace ────
───────────────────────────────────────────────────────────────────────────────────

成功展示控制流劫持

c语法交互式shell

当年学web时,觉得php命令行很好用,调一调就知道函数咋用了。但是后来学pwn,经常要编译一个二进制出来,然后gdb调试才能看到咋回事,就想着有没有c语言的命令行。gdb的动态调用函数并返回结果展示,这在某种程度上是脚本语言的特性,所以仍然使用true程序并把断点打在exit上:

$ gdb -q `which true` -ex "b exit" -ex "r"

执行后的gdb命令行,不正是我想要的么!可以当成一个纯净的c语言的交互式shell去调用一些函数:

$ gdb -q `which true` -ex "b exit" -ex "r"
gef  p atoi("456")
$1 = 0x1c8
gef  p strcpy(__malloc(20),"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
$2 = 0x55555555f2a0 'A' <repeats 45 times>
gef  heap chunks
Chunk(addr=0x55555555f010, size=0x290, flags=PREV_INUSE)
    [0x000055555555f010     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0x55555555f2a0, size=0x20, flags=PREV_INUSE)
    [0x000055555555f2a0     41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41    AAAAAAAAAAAAAAAA]
Chunk(addr=0x55555555f2c0, size=0x4141414141414140, flags=PREV_INUSE)    top chunk

当遇到一个陌生的c函数,希望尝试调用,此法可能比编译一个demo来的更迅速。但这个shell貌似并不支持什么循环,定义结构体等,故这只是一种理解或者学习程序的方法,并不真的是c语言的交互式shell。其实在刚开始学习计算机的时候,会有脚本语言和编译语言的区分,入门时只知道概念,基本是一知半解。如果现在让我来解释这两个东西的区别就是:运行时有无源码。当然python,php都有字节码,但他们仍算脚本语言,因为原生态的用法就是用源码运行。不过这么想的肯定不止我一个:

但对如上方法,我觉得是:只得其形未得其神。他只是把编译过程脚本化,但是运行时不依赖源码。从此角度理解,gdb命令的确是运行时从文本命令中解析得到,然后在进程里做一些动态调用,此时的c语言的确变成了脚本语言。其实程序都是一回事,最终还是看这个技术把编译和运行拆的开不开。

其他技巧

除了以下的调试技巧,使用gdb来getshell的方法也非常多,可自行探索