标题: 离线生成/proc/kallsyms结果 创建: 2019-03-25 16:46 更新: 2019-03-26 11:36 链接: https://scz.617.cn/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_unused section相比,/proc/kallsyms提供的信息是前者的超集。一般来说,前者包含导出 函数,后者包含的T符号不一定是导出符号。 从kernel image静态析取还原Module.symvers是另一个技术点,此处不展开。 ☆ 基本原理 内核源码中kallsyms_lookup_name()用于获取指定符号名的地址,涉及5个全局变量: -------------------------------------------------------------------------- kallsyms_addresses[] 每个元素是一个地址(或者说指针),是相应符号名的地址。 kallsyms_num_syms kallsyms_addresses[]的元素个数。 kallsyms_names[] kallsyms_addresses[i]是地址,kallsyms_names[i]是符号名,i值一致,i取值 范围是[0,kallsyms_num_syms),左闭右开区间。 kallsyms_names[]不是普通意义上的数组,每个元素是形如"xx yy yy yy yy"的 字节流,其中xx指明后面yy的字节数,每个yy都是kallsyms_token_index[]的下 标。 kallsyms_names[i]只是形象化表述,实际编程中无法使用kallsyms_names[i] 直接定位第i个符号名。内核源码中用kallsyms_expand_symbol()处理 kallsyms_names[]的单个元素。 kallsyms_token_index[256] 这个数组只有256个元素,每个元素是"unsigned short int",元素值是在 kallsyms_token_table[]中的偏移量(不是下标)。 kallsyms_token_table[256] kallsyms_token_table[]不是普通意义上的数组,它的每个元素都是ASCIZ串, 所谓token。kallsyms_token_table[]就是一个buf[],只不过其中存放了256个 ASCIZ串(token)。kallsyms_token_table[]中有很多单字节token。 kallsyms_token_table[i]只是形象化表述,实际编程中无法使用 kallsyms_token_table[i]直接定位第i个token,只能用 kallsyms_token_table+kallsyms_token_index[i]定位第i个token。 -------------------------------------------------------------------------- /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_addresses[i] <=> kallsyms_names[i] (i值一致) xx aa bb cd dd ... kallsyms_token_table+kallsyms_token_index[aa] => kallsyms_token_table+kallsyms_token_index[bb] => kallsyms_token_table+kallsyms_token_index[cc] => kallsyms_token_table+kallsyms_token_index[dd] => ... +++ 的第1个字符是类型,比如"T" 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" 这一项对应数据结构: -------------------------------------------------------------------------- struct kernel_symbol { unsigned int value; // +0x0 函数地址 char *name; // +0x4 函数名 // +0x8 }; -------------------------------------------------------------------------- ☆ 手工定位关键全局变量 原理所涉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] 所在。 本例中它们的位置关系是: -------------------------------------------------------------------------- /* * 前3个变量算是挨着 * * IDA有时会自动为kallsyms_addresses[]识别元素个数,不要信它的 * * 0xc06b2920+0x6250*4=0xc06cb260 */ C06B2920 00 80 00 C0 kallsyms_addresses DCD 0xC0008000 C06CB260 50 62 00 00 kallsyms_num_syms DCD 0x6250 C06CB270 04 kallsyms_names DCB 4 C070DEC0 69 6C 00 kallsyms_token_table DCB "il",0 /* * 0xc070e240+256*2=0xc070e440 */ C070E240 00 00 kallsyms_token_index DCW 0 -------------------------------------------------------------------------- ☆ 手工定位符号"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"的完整过程: kallsyms_addresses[0] <=> kallsyms_names[0] 04 bf b3 78 74 kallsyms_token_table+kallsyms_token_index[0xbf] => "Ts" kallsyms_token_table+kallsyms_token_index[0xb3] => "te" kallsyms_token_table+kallsyms_token_index[0x78] => "x" kallsyms_token_table+kallsyms_token_index[0x74] => "t" "Tstext" "T" + "stext" ☆ 代码实现 $ vi dump_kallsyms_mini.py -------------------------------------------------------------------------- #! /usr/bin/env python # -*- coding: cp936 -*- import os, sys, struct # ########################################################################## # # # 单字节string转成8位无符号整数 # # ord()实现同样功能 # def stoi8 ( sth ) : return( int( struct.unpack( '=B', sth )[0] ) ) # # end of stoi8 # # # 两字节string转成16位无符号整数 # def stoi16 ( sth ) : return( int( struct.unpack( '=H', sth )[0] ) ) # # end of stoi16 # # # 四字节string转成32位无符号整数 # def stoi32 ( sth ) : return( int( struct.unpack( '=I', sth )[0] ) ) # # end of stoi32 # def read_u8 ( buf, off ) : return( stoi8( buf[off] ) ) # # end of read_u8 # def read_u16 ( buf, off ) : return( stoi16( buf[off:off+2] ) ) # # end of read_u16 # def read_u32 ( buf, off ) : return( stoi32( buf[off:off+4] ) ) # # end of read_u32 # # # 返回ASCIZ串,不包括结尾的NUL字符 # def read_c_str ( buf, off ) : ret = "" while ( '\0' != buf[off] ) : ret += buf[off] off += 1 # # end of while # return( ret ) # # end of read_c_str # # ########################################################################## # # # 这是文件偏移,不是虚拟地址。针对特定kernel image需要自行指定这批值,未做 # 启发式定位。 # kallsyms_addresses = 0x6aa920 kallsyms_num_syms = 0x6250 kallsyms_names = 0x6c3270 kallsyms_token_index = 0x706240 kallsyms_token_table = 0x705ec0 def kallsyms_expand_symbol ( buf, off ) : ret = [ None, None, None ] i = read_u8( buf, off ) off += 1 ret[0] = off + i x = "" for j in xrange( i, 0, -1 ) : k = read_u8( buf, off ) off += 1 l = kallsyms_token_table + read_u16( buf, kallsyms_token_index + k * 2 ) x += read_c_str( buf, l ) # # end of for # ret[1] = x[0] ret[2] = x[1:] return( ret ) # # end of kallsyms_expand_symbol # # # debug模式下额外输出符号名的索引及其文件偏移 # def dumpsymbols ( buf, debug=False ) : off = kallsyms_names for i in xrange( 0, kallsyms_num_syms ) : addr = read_u32( buf, kallsyms_addresses + i * 4 ) x = kallsyms_expand_symbol( buf, off ) if ( debug ) : sys.stdout.write \ ( "%04x %08x %08x %s %s\n" % ( i, off, addr, x[1], x[2] ) ) else : sys.stdout.write \ ( "%08x %s %s\n" % ( addr, x[1], x[2] ) ) off = x[0] # # end of for # # # end of dumpsymbols # # ########################################################################## # if '__main__' == __name__ : try : # # 'nt' == os.name # if 'win32' == sys.platform : #""" from msvcrt import setmode # # stdin/stdout/stderr # setmode( 0, os.O_BINARY ) setmode( 1, os.O_BINARY ) setmode( 2, os.O_BINARY ) #""" argc = len( sys.argv ) if ( argc < 2 ) : sys.stderr.write \ ( 'Usage: ' + os.path.basename( sys.argv[0] ) + ' [d]\n' ) raise SystemExit else : with open( sys.argv[1], 'rb' ) as f : kernel = f.read() debug = False if ( argc >= 3 ) : if ( 'd' == sys.argv[2] ) : debug = True dumpsymbols( kernel, debug ) # # end of with # except KeyboardInterrupt : pass -------------------------------------------------------------------------- 这是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"。