get_symbol_from_kernel_img

[EXPTECH]__Get symbols from Android kernel image

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

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

代码位置kernel: lib/vsprintf.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
case 'K':
/*
* %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;
break;

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

1
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,这个函数传入一个符号名,返回这个符号的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

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
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;
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]];
data++;
len--;
while (*tptr) {
if (skipped_first) {
if (maxlen <= 1)
goto tail;
*result = *tptr;
result++;
maxlen--;
} else
skipped_first = 1;
tptr++;
}
}
tail:
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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"
return
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"
return
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就是下一张表的首地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
while addresses[0] > 0xffffffc000000000:
offset = offset + 1
if loc + offset * 8 >= len(img):
print "can not find end of addresses list"
return
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"
return
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:边测边写,丑到无法直视