多种方法获取sys_call_table(linux系统调用表)的地址

时间:2022-09-01 15:41:40

一.方法一:常用方式,也是一google一堆的方式

我们首先需要找到call table-with-offset的特征,先看下面的代码

syscall_call:
        call *sys_call_table(,%eax,4)
假设我们没有vmlinux可供gdb反汇编,那也只有采用模拟的方式了,模拟出一个call *sys_call_table(,%eax,4),然后看其机器码,然后在system_call的附近基于这个特征进行寻找

然后用objdump进行dump可见下面一行:
080483ac <main>:
...
 80483bc:       ff 14 85 1c 95 04 08    call   *0x804951c(,%eax,4)
...


于是ff 14 85后面就是sys_call_table的地址,注意大小端,x86机器是小端机器,因此是反着的。如果system_call也不知道,比如不能挂载procfs,并且也没有System.map,那么就只有通过中断描述符来先获取system_call的地址了,方法如下:
0.你必须知道中断描述符的结构以及有中断描述符寄存器这么一回事。不过就算不知道也比较好查,google即可;
1.通过sidt指令获取中断描述符的基地址;
2.将这个地址加上8*0x80就是系统调用描述符的地址了;
3.从这个描述符中取出系统调用处理程序即system_call地址的高16位和低16位,拼接在一起即可。

二.方法二:使用dump_stack
写一个很简单的内核模块,内部调用dump_stack,然后就可以看到:
 [<f88f300b>] init_module+0xb/0x53 [gettable]
 [<c013adc4>] sys_init_module+0x104/0x250
 [<c010620b>] syscall_call+0x7/0xb

既然看到了syscall_call+0x7的地址,那么也就知道了标号syscall_call的地址,而我们需要找的sys_call_table的地址就在它下面地址的指令中,对于2.6.8内核而言,就是它下面的第一条指令:
syscall_call:
        call *sys_call_table(,%eax,4)
实际上syscall_call这个标号可以在/proc/kallsym中取到的,如果没有procfs再使用dump_stack的方法。为了不让人说我是胡扯的,贴上代码:

然后dmesg的结果如下:
 [<f88e000b>] init_module+0xb/0x50 [gettable]
 [<c013adc4>] sys_init_module+0x104/0x250
 [<c010620b>] syscall_call+0x7/0xb
ff 14 85 1c ad 2d c0 89

在这个方法中,即使不能从procfs中获取任何信息,还是可以使用dump_stack的,就算有一天这个函数也不能用了,那怎么办呢?很好办,在模块中故意访问一个NULL指针,然后内核就算替你打印stack了...逼到最后,大不了遍历所有的memory(通过/dev/mem?或者/proc/kcore?),然后从中查找匹配的机器码,这是最后的办法,即使这样,我们也可以肯定这个地址不会太靠后的。
三.方法三:直接使用栈结构获取
这种方式不是那么直观,然而却很直接,在x86机器上,我们知道栈的重要性,栈保存了函数调用的路径,它就是程序执行流的家,任何后续需要的本执行流都有一个栈。对于内核模块而言,在insmod加载它并初始化的时候,这个栈是存在的,实际上就是insmod进程的内核栈。我们可以顺着这个内核栈来向上回溯,直到找到call *sys_call_table(,%eax,4)的下一跳指令的地址,这有个基本原则,那就是我们知道在调用call指令的时候,需要将下一条指令的地址压入栈(注意是地址),因此这个call *sys_call_table(,%eax,4)指令的下一条指令的地址肯定能在回溯的途中遇到,既然找到了call的下一条指令,那么往上一条指令不就是call吗?既然找到了call指令,通过分析Intel的指令格式,我们就能抽出sys_call_table的地址。
3.1.如何判断谁是call指令的下一条地址
答曰:在加载内核的时候,内核的text段被载入到了0xC0000000 + 0x100000这个地址,这是通过vmlinux.lds链接文件知道的,并且system_call这个0x80的entry直到call sys_call_table(,%eax,4)没有调用任何call指令(在正常的前提下,既然已经到了模块的init函数,当然正常了),而system_call这个entry是insmod进程切到内核栈的第一条指令,因此内核栈到此为止,因此从回溯的末尾开始,第一个遇到的0xc01XXXXX附近的值就是了。
3.2.前置知识
想这么干,并不需要知道内核栈的结构以及current宏的相关知识,不过理解了也没什么坏处!
3.3.如何找到call指令中的sys_call_table值
答曰:通过源代码知道这是一条:CALL dword ptr [REG*SCALE+BASE]
查阅Intel的指令手册或者google前人发现的捷径,可以知道这类指令是带有SIB的call指令,应该是FF 14 xx的样子,因为base是一个地址,因此xx就应该是85,这是从intel提供的一张表中获取的,从而最终,这条指令就应该是FF 14 XX Y1 Y2 Y3 Y4这个样子,于是从找到的call指令的下一条指令地址直接减去4之后就能获取Y1 Y2 Y3 Y4了,而这就是最终需要的sys_call_table
3.4.代码:

3.5.此方法不需要获取system_call的地址。
四.方法四:通过/dev/mem在用户态完成
这种方式不会污染运行中的内核(不会载入任何模块),然而弄不好很容易PTD(panic to death,对应windows的blue screen...)。这种方式实际上是最直接的,相当于直接使用机器码对整个物理内存编程,需要相当高的水平。不过,整个内存都拥有了,还有什么做不到呢?
     之所以可以动态修改机器码,是因为冯诺依曼机器是基于存储模型的,指令和数据一样是存储在内存中的,而内存是可存取的,虽然现代机器架构使用了保护模型比如内存存取权限或者特权环等机制限制了某些存取,但是却无法从根本上改变冯诺依曼模型的存取特征,因为在任何领域,对于主体的鉴权都是有缺陷了,比如一旦有特权的主体被以某种方式劫持了,那么它的行为将是危险的和有害的(暂且不考虑禁止向下写等单向信息鉴权模型,那样会引入新的复杂性和新的不确定因素),所以模型决定了一切而不是局部的设计决定了一切。
五:总结
1.不赞成替换系统调用。因为这是linux,不是windows,你既然能编译并加载模块,说明你有root权限,既然你有root权限,替换一个系统调用毫无疑义,说明不了你的水平
2.不赞成用gdb反汇编内核,我们需要的仅仅是一个地址值而已,没有必要那么麻烦反汇编内核。
3.我所作的一切只是为了调试新添加的系统调用而不希望重新编译内核,并不是搞攻击,如果真的搞攻击,最难的不是写代码,是如何发现漏洞以及如何利用漏洞,首先你不是root,弄到最后你成了root,接下来你就可以做替换系统调用这种简单至极的事了,前提是你怎么从非root成为root?最简单也是最难的攻击办法就是:直接逼问管理员!