暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

离线生成/proc/kallsyms结果

青衣十三楼飞花堂 2019-03-26
1391

TXT原文


http://scz.617.cn:8/unix/201903251646.txt


目录:

    ☆ 原始问题
    ☆ 基本原理
    ☆ 手工定位关键全局变量
    ☆ 手工定位符号"stext"
    ☆ 代码实现


☆ 原始问题

标准kernel的源码是公开的,但很多手机、IoT设备的kernel有定制化开发,比如把一些加解密处理放进kernel,用户态通过ioctl()调用内核态代码。没有改动后的源码,要想搞清楚这些定制化开发在干什么,只能对之进行逆向工程。

如果拥有运行中Linux的root shell,可以"cat /proc/kallsyms",这个结果在针对kernel进行逆向工程时大有用处。

有时由于各种原因,需要通过kernel image直接静态析取/proc/kallsyms的内容,也就是离线生成/proc/kallsyms结果。

与arr[3]、__ksymtab section、__ksymtab_gpl section、__ksymtab_unusedsection相比,/proc/kallsyms提供的信息是前者的超集。一般来说,前者包含导出函数,后者包含的T符号不一定是导出符号。

从kernel image静态析取还原Module.symvers是另一个技术点,此处不展开。

☆ 基本原理

内核源码中kallsyms_lookup_name()用于获取指定符号名的地址,涉及5个全局变量:



/proc/kallsyms展现的信息靠上述5个全局变量来维护。整个原理是复用token。kallsyms_names[i]不直接保存ASCIZ串或其指针,而是将符号名(ASCIZ串)切割成若干个token,kallsyms_names[i]保存的数据可以定位这些token,将这些token拼接后还原出原始ASCIZ串。kallsyms_names[i]对应的原始ASCIZ串也不直接是符号名,而是"单字节类型字符+符号名",所以kallsyms_expand_symbol()中有个变量skipped_first。

图解上述5个全局变量的关系:


kallsyms_lookup_name()就是遍历kallsyms_num_syms个kallsyms_names[i],如果找到指定符号名(用strcmp比较),用相应的索引i去取kallsyms_addresses[i]。如果没找到指定符号名,调用module_kallsyms_lookup_name()接着找定符号名。显然kallsyms_lookup_name()效率很低。

此处实际展示了一种针对符号名的压缩算法,据说能压缩50%,我不太信。

原始符号名被切割成若干token存放,意味着/proc/kallsyms中看到的符号名在Strings中搜不到,如果碰巧搜到,那只是其他同名字符串。

c031b698 T sys_clone
c03a8768 T sys_close
c03a8c10 T sys_creat
c03a9320 T sys_chown
c03a945c T sys_chmod
c03a947c T sys_chroot
c03a9588 T sys_chdir
c07555b0 r __ksymtab_sys_close
c075cc3b r __kstrtab_sys_close

T并不表示是导出符号,global (external)与export不等价,前者是后者的超集。t表示"local symbol in the text (code) section"。

在Strings中搜不到"sys_chown"、"sys_chmod",但能搜到"sys_close",因为sys_close是内核导出函数,在__ksymtab section中有一项:

C07555B0 68 87 3A C0 off_C07555B0 DCD sys_close
C07555B4 3B CC 75 C0     DCD aSys_close                      ; "sys_close"

这一项对应数据结构:

☆ 手工定位关键全局变量

原理所涉5个全局变量在/proc/kallsyms中没有对应项,必须用其他手段定位。即使有,也不能用,原始需求是静态析取与/proc/kallsyms一致的信息。

# grep kallsyms_lookup_name /proc/kallsyms
c03722c8 T module_kallsyms_lookup_name
c0376058 T kallsyms_lookup_name
c0757948 r __ksymtab_kallsyms_lookup_name
c075bb9d r __kstrtab_kallsyms_lookup_name

__ksymtab_kallsyms_lookup_name表示kallsyms_lookup_name()是内核导出函数,在原始需求下不能直接用0xc0376058定位它。

kallsyms_lookup_name()是内核导出函数,可在Strings中搜到函数名字,比如:

C075BB9D 6B 61 6C 6C+aKallsyms_lookup_n DCB "kallsyms_lookup_name",0

在IDA中搜"9d bb 75 c0",即上面这个地址(little-endian序),其中有一处对应struct kernel_symbol的name字段:

C0757948 58 60 37 C0     DCD kallsyms_lookup_name
C075794C 9D BB 75 C0     DCD aKallsyms_lookup_n              ; "kallsyms_lookup_name"

据此可以定位kallsyms_lookup_name():

C0376058             kallsyms_lookup_name

0xc0757948位于[__start___ksymtab_gpl,__stop___ksymtab_gpl),即位于"__ksymtab_gpl section"中。

对照相应版本内核源码逆向分析kallsyms_lookup_name()定位另一个非导出函数:

C0375408             kallsyms_expand_symbol

分析kallsyms_lookup_name()、kallsyms_expand_symbol()即可定位原理所涉5个全局变量。

/proc/kallsyms中有kallsyms_expand_symbol,但现在是假设没有/proc/kallsyms可用。

上面是普适方案,具体到某个全局变量,可能有其他定位手段。

# head -5 /proc/kallsyms
c0008000 T stext
c0008000 T _sinittext
c0008000 T _stext
c0008000 T __init_begin
c000804c t __create_page_tables

通过/proc/kallsyms返回的符号是按其地址升序显示的,因此kallsyms_addresses[]的前2个元素对应字节流"00 80 00 c0 00 80 00 c0",在IDA中搜它,即可定位:

C06B2920 00 80 00 C0 kallsyms_addresses DCD 0xC0008000
C06B2924 00 80 00 C0     DCD loc_C0008000

0xc06b2920即kallsyms_addresses[],其中一个交叉引用指向kallsyms_lookup_name()。

kallsyms_num_syms紧挨在kallsyms_addresses[]之后,这是一个鸡生蛋、蛋生鸡的问题,只能反向验证对kallsyms_num_syms的定位。当然也可以进行某种启发式定位,但那还不如直接逆向kallsyms_lookup_name()。

kallsyms_names[]在kallsyms_num_syms后面。

查看"r__kstrtab_s"或"r__ksymtab_s"所在区域,这是kallsyms_token_table[256]所在。

本例中它们的位置关系是:



☆ 手工定位符号"stext"

# head -1 /proc/kallsyms
c0008000 T stext

符号"stext"的地址保存在kallsyms_addresses[0],尝试手工定位符号"stext"。

符号"stext"在kallsyms_addresses[]中的索引值是0,意味着kallsyms_names[0]对应字符串"stext"。

访问kallsyms_names[0]:

C06CB270 04          kallsyms_names DCB    4
C06CB271 BF              DCB 0xBF
C06CB272 B3              DCB 0xB3
C06CB273 78              DCB 0x78
C06CB274 74              DCB 0x74

原理篇所说的"xx yy yy yy yy"即此处的"04 bf b3 78 74"。

第一个字节4表示后面有4个索引值用于索引kallsyms_token_index[]:

0xc070e240+0xbf*2=0xc070e3be
0xc070e240+0xb3*2=0xc070e3a6
0xc070e240+0x78*2=0xc070e330
0xc070e240+0x74*2=0xc070e328

kallsyms_token_index[i]类型是"unsigned short int",所以索引值乘以2。

C070E3BE 93 02           DCW 0x293
C070E3A6 67 02           DCW 0x267
C070E330 6F 01           DCW 0x16F
C070E328 67 01           DCW 0x167

得到4个偏移量(不是下标)用于kallsyms_token_table[]:

0xc070dec0+0x293=0xc070e153
0xc070dec0+0x267=0xc070e127
0xc070dec0+0x16f=0xc070e02f
0xc070dec0+0x167=0xc070e027

kallsyms_token_table+off类型是ASCIZ串(token)

C070E153 54 73 00    aTs DCB "Ts",0
C070E127 74 65 00    aTe DCB "te",0
C070E02F 78 00       aX  DCB "x",0
C070E027 74 00       aT  DCB "t",0

第一个ASCIZ串"Ts",打头的"T"是符号类型,真正的符号名要越过"T"。

图解手工定位符号"stext"的完整过程:



☆ 代码实现

$ vi dump_kallsyms_mini.py

(见TXT原文)

这是Python 2.x的实现,核心代码是kallsyms_expand_symbol()、dumpsymbols(),只处理32位内核,64位内核原理类似,我没64位需求就没处理。

dump_kallsyms_mini.py不是傻瓜式实现,假设使用者能自行定位如下全局变量的文件偏移:

kallsyms_addresses      = 0x6aa920
kallsyms_num_syms       = 0x6250
kallsyms_names          = 0x6c3270
kallsyms_token_index    = 0x706240
kallsyms_token_table    = 0x705ec0

其中kallsyms_num_syms是元素个数本身,不是变量的偏移(没必要read_u32)。

有人会说启发式定位,我不好那个,除非这种启发式定位手段非常靠谱。在程序中增加并不怎么样的启发式定位,会让程序看起来主次不分,除了能满足小白的需求,实在不符合我的审美。想必用这类程序的人都能手工定位关键全局变量。

$ dump_kallsyms_mini.py kernel > kallsyms_static.txt
$ cat /proc/kallsyms > kallsyms_dynamic.txt

kallsyms_static.txt与kallsyms_dynamic.txt的sha1sum完全一样。

$ dump_kallsyms_mini.py kernel d > kallsyms_static_debug.txt

debug模式下额外输出符号名的索引及其文件偏移:

0000 006c3270 c0008000 T stext
0001 006c3275 c0008000 T _sinittext
0002 006c327c c0008000 T _stext
0003 006c3282 c0008000 T __init_begin

此时可以手工查看某个符号名的压缩格式。

如果有KASLR介入,可以先在IDA中使用符号信息,再"Rebase program"。

文章转载自青衣十三楼飞花堂,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论