百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术流 > 正文

在调试器里看LINUX内核态栈溢出(linux 内核 调试)

citgpt 2024-08-10 15:18 11 浏览 0 评论

图灵最先发明了栈,但没有给它取名字。德国人鲍尔也“发明”了栈,取名叫酒窖。澳大利亚人汉布林也“发明”了栈,取名叫弹夹。1959年,戴克斯特拉在度假时想到了Stack这个名字,后来被广泛使用。

今天的CPU和软件都是基于所谓的栈架构的,CPU运行时,几乎每时每刻都是离不开栈的。

在调试器里看LINUX内核态栈溢出(linux 内核 调试)

简单说来,每个普通线程一般都有两个栈,一个位于用户空间,供在用户空间执行时使用,另一个位于内核空间,供这个线程执行系统调用、掉入陷阱或者当CPU在执行这个线程时遇到中断时用。

因为系统中每个进程都有一个用户空间,但是内核空间只有一个,所以内核空间的栈一般都是比较小的。对LINUX内核来说,更是这样。多大呢?32位时是8KB,64位时是16KB。

栈是流动性很大的一个临时空间,每次想到栈,我都不由得想起那部经典的电影——《新龙门客栈》,茫茫大漠之中,孤零零的一座客栈,不同身份的客人从不同方向,带着不同的目的,在这里相遇,可能是姻缘邂逅,可能是狭路相逢,也可能是千里追杀......



软件世界的栈也很忙碌,也很变幻莫测,也很危机四伏,也很有故事。

几年前曾经开笔写了一本书,叫《栈上风云》,可惜只完成了5章,便搁置在那里了。

闲言打住 ,今天先说说LINUX内核态栈溢出。

启动一个Ubuntu作为调试目标,再启动一个Ubuntu作为调试主机。在主机上启动GDB,开始双机内核调试。(详细过程可以参阅高端调试网站的文章)

准备好GDB后,在目标机中按Alt + PrtScr + g触发其中断到调试器,片刻之后,GDB中收到消息,执行bt命令观察执行官过程。



上图中的栈回溯比较完美地展示了LINUX内核处理中断的过程,特别地,这一次是在处理键盘中断,也就是我们刚才按下的中断热键。

执行frame 20命令切换到#20栈帧,执行info locals观察函数的局部变量:

(gdb) info locals
old_regs = 0x0 <irq_stack_union>
desc = 0xffff88003e030000
vector = 49
__func__ = "do_IRQ"

执行info args观察函数的参数:

(gdb) info args
regs = 0xffff88003b44b9e8

使用万能的print命令打印regs变量:

(gdb) p *regs
$16 = {r15 = 18446612133308578668, r14 = 33617120, r13 = 142166, r12 = 0, bp = 18446612133308578480, bx = 18446612133308578496, r11 = 659, r10 = 34472032, r9 = 1, r8 = 8392803, 
 ax = 13282968638105362717, cx = 23, dx = 868807409, si = 1928222438591233, di = 1302, orig_ax = 18446744073709551566, ip = 18446744071579876365, cs = 16, flags = 662, sp = 18446612133308578456, 
 ss = 24}

这些寄存器是处理中断前保存的寄存器状态。其中的sp寄存器就是栈指针(stack pointer)。目前显示为10进制,观察不便,使用printf格式化一下:

(gdb) printf "%p\n", regs->sp

0xffff88003b44ba98

Linux的内核态栈使用一种特殊的约定,分配栈时绝对是按栈大小对齐,然后在栈的最低地址处存放一个thread_info结构体,即如下图所示。



这样设计带来的一个好处是非常容易低从栈指针得到栈的位置,方法就是把低位抹掉(换为0)。

因为目标系统是64位,栈的大小是16KB,因此,只要把低14位替换为零就得到栈空间的起始地址。

(gdb) printf "%p\n", regs->sp&(~(0x4000-1))
0xffff88003b448000

看着是不有点野蛮,有点吓人?

或者你可能怀疑这样做的正确性,来验证一下吧,先打印thread_info结构体:

(gdb) p *(struct thread_info*)0xffff88003b448000
$19 = {task = 0xffff88003bdf0e80, flags = 8, status = 0, cpu = 0}

看着靠谱么?靠谱的,第一个字段是著名的任务结构体,Linux内核源代码中著名的current宏就是从这里取到的哦。所属CPU为0也是合理的。

进一步验证task结构体:

(gdb) p *((struct thread_info*)0xffff88003b448000)->task
$20 = {state = 0, stack = 0xffff88003b448000, usage = {counter = 2}, flags = 4194304, ptrace = 0, wake_entry = {next = 0x0 <irq_stack_union>}, on_cpu = 1, wakee_flips = 0, 

各个字段的字也都很合理,特别是其中的stack字段代表这个线程的内核态栈空间起始地址,和我们手工算出来的一模一样啊。

在GDB中执行monitor ps命令列出所有线程,找到线程号2112:

(gdb) monitor ps A
Task Addr Pid Parent [*] cpu State Thread Command
0xffff88003bdf0e80 2112 1778 1 0 R 0xffff88003bdf18c0 *gnome-software

可以看到它的任务结构体地址和我们前面通过thread_info找到的完全相同,关联的CPU为0号,所属进程的程序名为gnome-software,名字前面的*代表它是CPU当前正在执行的线程。

时光倒流一下,在我们按Alt + PrtScr + g热键那一刹那,CPU正在执行gnome-software进程的2112号线程,键盘硬件通过中断控制器给CPU发中断信号,CPU响应中断信号跳转到IDT表里的中断处理函数,处理中断,因为这是个普通的中断,IDT里面登记的是普通的中断门,不需要切换栈,于是CPU就借用当前线程的栈来处理中断。对中断处理函数来说,必须要做好准备,“借栈使用”,这一般被称为可以在arbitrary context(任意上下文里)执行。

对内核态栈有所了解后,可能有朋友想到了,这么小的栈,不够用可怎么办?

这确确实实是个严肃的问题。

不轻信不迷信,做个实验来看看吧!

在老雷编写的llaolao内核模块中增加一个函数,名叫wastestack,故意做递归调用:

static void wastestack(int recursive)
{
	int nothing;
	printk("wastestack %d\n",recursive);
	while(recursive)
		wastestack(recursive-1);
}

加载这个驱动,通过debugfs中的虚文件触发这个调用,好戏便开始了!

这样递归的话,只要recursive的初始值足够大,那么肯定会把栈用完的,用完了会怎么样呢?

执行了一会后,GDB中先接收到一个Oops,内核打了个喷嚏。



理解Oops开始的描述:

[ 6810.797013] NMI watchdog: BUG: soft lockup - CPU#0 stuck for 22s! [bash:2441]

看来是NMI看门狗超时了,通过NMI激发得到执行机会后,打印出这个Oops给我们看,意思是0号CPU在2441进程上粘住了,已经22秒。

值得单独写一篇文章来讨论系统的狗,今天暂且放过。

再仔细观察Oops信息中的栈起始地址,以及目前的RSP值(代表已经使用到的位置):

 task.stack: ffff88003aea4000
 RSP: 0018:ffff88003aea61c8

二者的差大约是还可以用的栈空间(要刨掉thread_info结构体的大小),看来还有大约8K,用了一半。

在GDB中发出c命令,让CPU继续残忍执行。

过了一会,又出现一个Oops,继续循环,又一个Oops,CPU义无反顾,继续勇敢地奔跑,系统忙碌着,风扇的声音变大,.......

但是如此持续大约60秒时,突然安静了,GDB中最后的几行信息如下:

[ 8430.351318] wastestack 7621
[ 8430.354240] wastestack 7620
[ 8430.357109] wastestack 7619
[ 8430.359711] wastestack 7618
Ignoring packet error, continuing...
Thread 390 received signal SIGSEGV, Segmentation fault.
Ignoring packet error, continuing...
Ignoring packet error, continuing...

GDB报告通信错误,对方失联了!

在失联之前,内核报告在390线程发生段错误,访问了不该访问的。

追溯GDB记录下的最后一次Oops:



比较当时的栈信息和栈指针:

 task.stack: ffff88003aea4000
 RSP: 0018:ffff88003ae48788 

已经越界很多了,大约超出了368KB:

0:000> ? (a4-48)*1000
Evaluate expression: 376832 = 00000000`0005c000

也就是,2441号线程里的递归调用不仅用完了自己的栈空间,扫荡了自己线程的thread_info结构体,还继续碾压了368KB的内核数据,哪些数据被碾压了呢?内核的那个字节都很敏感,何况300多K啊。

怎么没有保护呢?

就是没有,有点不可思议,但事实上就是没有。

其它操作系统也是这样么?不是的,或者说肯定不全是。

比如在Windows系统中,每个线程的内核态栈也是有明确的登记:

 kd> !thread
Stack Init 8eb6eed0 Current 8eb6e9d0 Base 8eb6f000 Limit 8eb6c000 Call 00000000

而且前后都有保护页:

kd> dd 8eb6c000-10
8eb6bff0 ???????? ???????? ???????? ????????
8eb6c000 ffffffff ffffffff ffffffff ffffffff
8eb6c010 ffffffff ffffffff ffffffff ffffffff
8eb6c020 ffffffff ffffffff ffffffff ffffffff
8eb6c030 ffffffff ffffffff ffffffff ffffffff
8eb6c040 ffffffff ffffffff ffffffff ffffffff
8eb6c050 ffffffff ffffffff ffffffff ffffffff
8eb6c060 ffffffff ffffffff ffffffff ffffffff

一旦有溢出,就会碰到保护页,触发著名的双误异常。

相关推荐

js中arguments详解

一、简介了解arguments这个对象之前先来认识一下javascript的一些功能:其实Javascript并没有重载函数的功能,但是Arguments对象能够模拟重载。Javascrip中每个函数...

firewall-cmd 常用命令

目录firewalldzone说明firewallzone内容说明firewall-cmd常用参数firewall-cmd常用命令常用命令 回到顶部firewalldzone...

epel-release 是什么

EPEL-release(ExtraPackagesforEnterpriseLinux)是一个软件仓库,它为企业级Linux发行版(如CentOS、RHEL等)提供额外的软件包。以下是关于E...

FullGC详解  什么是 JVM 的 GC
FullGC详解 什么是 JVM 的 GC

前言:背景:一、什么是JVM的GC?JVM(JavaVirtualMachine)。JVM是Java程序的虚拟机,是一种实现Java语言的解...

2024-10-26 08:50 citgpt

使用Spire.Doc组件利用模板导出Word文档
  • 使用Spire.Doc组件利用模板导出Word文档
  • 使用Spire.Doc组件利用模板导出Word文档
  • 使用Spire.Doc组件利用模板导出Word文档
  • 使用Spire.Doc组件利用模板导出Word文档
跨域(CrossOrigin)

1.介绍  1)跨域问题:跨域问题是在网络中,当一个网络的运行脚本(通常时JavaScript)试图访问另一个网络的资源时,如果这两个网络的端口、协议和域名不一致时就会出现跨域问题。    通俗讲...

微服务架构和分布式架构的区别

1、含义不同微服务架构:微服务架构风格是一种将一个单一应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中,服务间通信采用轻量级通信机制(通常用HTTP资源API)。这些服务围绕业务能力构建并...

深入理解与应用CSS clip-path 属性
深入理解与应用CSS clip-path 属性

clip-pathclip-path是什么clip-path 是一个CSS属性,允许开发者创建一个剪切区域,从而决定元素的哪些部分可见,哪些部分会被隐...

2024-10-25 11:51 citgpt

HCNP Routing&Switching之OSPF LSA类型(二)
  • HCNP Routing&Switching之OSPF LSA类型(二)
  • HCNP Routing&Switching之OSPF LSA类型(二)
  • HCNP Routing&Switching之OSPF LSA类型(二)
  • HCNP Routing&Switching之OSPF LSA类型(二)
Redis和Memcached的区别详解
  • Redis和Memcached的区别详解
  • Redis和Memcached的区别详解
  • Redis和Memcached的区别详解
  • Redis和Memcached的区别详解
Request.ServerVariables 大全

Request.ServerVariables("Url")返回服务器地址Request.ServerVariables("Path_Info")客户端提供的路...

python操作Kafka

目录一、python操作kafka1.python使用kafka生产者2.python使用kafka消费者3.使用docker中的kafka二、python操作kafka细...

Runtime.getRuntime().exec详解

Runtime.getRuntime().exec详解概述Runtime.getRuntime().exec用于调用外部可执行程序或系统命令,并重定向外部程序的标准输入、标准输出和标准错误到缓冲池。...

promise.all详解 promise.all是干什么的
promise.all详解 promise.all是干什么的

promise.all详解promise.all中所有的请求成功了,走.then(),在.then()中能得到一个数组,数组中是每个请求resolve抛出的结果...

2024-10-24 16:21 citgpt

Content-Length和Transfer-Encoding详解
  • Content-Length和Transfer-Encoding详解
  • Content-Length和Transfer-Encoding详解
  • Content-Length和Transfer-Encoding详解
  • Content-Length和Transfer-Encoding详解

取消回复欢迎 发表评论: