从 uClibc 部分源码总结固件利用思路的变化【转】
转自:https://eqqie.cn/index.php/archives/1641
0x00 Before
审计固件的时候碰到了一个 mips64 下 uClibc 堆管理利用的问题,恰巧网络上关于这个的分析不是很多,于是研究了一下。并不是很全面,做个索引,若有进一步了解时继续补全。
0x01 何为 uClibc?
面向百度百科的废话
uClibc 是一个面向嵌入式 Linux 系统的小型的 C 标准库。最初 uClibc 是为了支持 uClinux 而开发,这是一个不需要内存管理单元的 Linux 版本,因此适合于微控制器系统。
uClibc 比一般用于 Linux 发行版的 C 库 GNU C Library (glibc) 要小得多,glibc 目标是要支持最大范围的硬件和内核平台的所有 C 标准,而 uClibc 专注于嵌入式 Linux. 很多功能可以根据空间需求进行取舍。
uClibc 运行于标准的以及无 MMU 的 Linux 系统上,支持 i386,x86 64,ARM (big/little endian), AVR32,Blackfin,h8300,m68k,MIPS (big/little endian), PowerPC,SuperH (big/little endian), SPARC,和 v850 等处理器。
人话
对于某些架构的嵌入式硬件,需要一个低开销的 C 标准库实现,于是 uClibc 就出现了。但是由于其实现方式与 glibc 差别较大,所以利用思路上需要一些转变。好在 uClibc 没有傻大笨 glibc 的各种检查,利用思路较为简单明确。
0x02 内存管理器
关于 uClibc 利用分析首当其冲的就是 malloc 和 free 等内存管理函数的实现。事实上通过观察其源码可以发现,uClibc 中 malloc 有三种实现,包括malloc
, malloc-simple
和malloc-standard
。其中 malloc-standard
是最近更新的。它就是把早期 glibc
的 dlmalloc
移植到了 uClibc
中。本文关于利用的分析重点在 malloc
malloc-simple
在这个版本的内存管理逻辑中,内存的分配和释放几乎就一一对应了mmap
和munmap
...
malloc()
[libc/stdlib/malloc-simple/alloc.c]
可以发现 size 没有做过多检查和处理就进了 mmap 的参数,而返回的地址则由 mmap 决定,并不存在一个特定的heap
段
free()
[libc/stdlib/malloc-simple/alloc.c]
直接调用了 munmap
malloc-standard
我分析的固件使用的是这个机制
location: libc/stdlib/malloc-standard/*
相对而言 malloc-standard 较为复杂,具体逻辑可以直接参考dlmalloc
malloc
这个版本我愿称之为 “无敌大套娃”
malloc()
使用malloc
函数时发生了如下调用链
void *malloc (size_t size)
[libc/stdlib/malloc/malloc.c]
↓
__malloc_from_heap (size_t size, struct heap_free_area **heap)
[libc/stdlib/malloc/malloc.c]
↓
尝试使用__heap_alloc
获取堆区中管理的已释放的内存:
↓
__heap_alloc (struct heap_free_area **heap, size_t *size)
[libc/stdlib/malloc/heap_alloc.c]
如果请求的 size 小于下面结构体的大小会被自动扩大(原因见注释):
注意这个结构体在被 free 的块的底部,这很重要
然后就是在一条链表(就是一开始传入的&__malloc_heap
)上遍历查找第一个 size 大于等于请求 size 的节点进入一个内联函数__heap_free_area_alloc
[libc/stdlib/malloc/heap.h]:
该函数判断分配掉目标大小的 size 之后,剩余体积是否足够 HEAP_MIN_FREE_AREA_SIZE,不够的话就整个从链表中取出(使用的双链表 unlink),否则只取出对应大小的部分内存(切割)。
如果你有疑问:为啥在切割是不涉及链表操作?
那么请往上看:struct heap_free_area
这个区域在 freed 区域的底部,只需要修改其中的 size,然后把需要的 mem 取出,就完成了一次切割,节省了很多链表操作,提高了效率。
...
回到__malloc_from_heap
,假如没有足够大小的 freed 区域用于取出,则会用 mmap 或者 sbrk 的方式向操作系统取得一块新的内存,具体使用 mmap 还是 sbrk 取决于编译时使用的宏:
注意 mem 在返回到用户前会经过下列宏处理,以设置 malloc_header,并让 mem 指向用户区域:
free
有了 malloc 的逻辑,free 的逻辑也差不多明晰了
void free (void *mem)
[libc/stdlib/malloc/free.c]
↓
static void __free_to_heap (void *mem, struct heap_free_area **heap)
[libc/stdlib/malloc/free.c]
首先调用__heap_free
把被 free 的内存放入链中:
/* Put MEM back in the heap, and get the free-area it was placed in. */
fa = __heap_free (heap, mem, size);
↓
struct heap_free_area *__heap_free (struct heap_free_area **heap, void *mem, size_t size)
[libc/stdlib/malloc/hewp_free.c]
看注释
这段代码主要处理被释放内存在入链时的合并和插入
0x03 利用思路
前置知识
uClibc 中没有类似 Glibc 那样的__free_hook
和__malloc_hook
的机制,但是部分函数间调用使用了类似 got 表的机制,这里可以看反汇编后的结果:
关于这块这么设计的原因我不太清楚...
既然如此,那么如果能通过任意地址写改 libuClibc.so 中某些函数的 got 的地址也许就可以借助system("/bin/sh\x00")
来 getshell。
不过要与程序本身的 got 表区分,如果程序已经导入了某些函数符号,直接修改掉 so 中这些函数符号的 got 是不能影响程序本身调用的目标的。(重要)
malloc-simple
很明显,释放内存的munmap
是一个很好的攻击目标,它的第一个参数正好是一个字符串指针,并且可控程度很高,如果能劫持其 got 表就可以爽歪歪了。
malloc
大部分操作都是一个基本没啥保护的双链表的操作,而且负责管理链表的 heap_free_area 在每个内存块的末尾。意味着如果有 UAF 的和堆溢出情况下可以修改 free_size,然后取出被修改的节点造成向低地址的 overlap。
在取出内存的过程中存在分割操作,如果可以找到目标区域附近某些值作为 free_size(最好特别大),然后修改链表的某个 next 指针到这。当申请内存合适的时候可以拿到目标区域的内存。注意这种利用方式不能触发__heap_delete
,否则容易出错。
malloc-standard
由于这种分配器只有 fastbin 和 unsortedbin 两种结构,并且检查很稀松,所以大部分ptmalloc
的知识可以迁移过来。并且伪造 fastbin 并取出时不检查目标区域的 size... 这简直给了和 tcache 一样的大方便。
刨除这部分,重点讲下怎么 getshell(因为没有各种 hook)...
源码宏太多,这里直接看反编译:
当 chunk-sized 大于一个阈值(不同版本可能不同,我这里是 0x50)并且 is_mmap 标志位为 1 时,会把chunk_header_ptr-prev_size
的地址送入 munmap 中。
假设我们有办法覆盖 munmap 的 got 表为 system,那么如果控制参数为 "/bin/sh\x00"?
这是我的一种思路:
- 控制
prev_size
为0xfffffffffffffff0
(-10) - 控制 size 为
0x63
(大于阈值且 is_mmap 位和 inuse 位为 1) - 在用户区域开头写入 "/bin/sh\x00"
这样当进入 munmap 时就相当于执行了system("/bin/sh\x00")
。
参考链接:
https://blog.csdn.net/heliangbin87/article/details/78962425
https://blog.csdn.net/weixin_30596165/article/details/96114098