在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+4↑j
.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,所以外人看起来必然是困惑。
- Duplicated memory management symbols in libc.so and ld-linux.so
- (glibc/fw/bug25486) ld.so: Do not export free/calloc/malloc/realloc functions
- c - libc.so 和 ld-linux.so 中的重复内存管理符号
不过如果你平时足够细心,其实能够发现,在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的方法也非常多,可自行探索