ubuntu 内核双机调试方法
由于想要学习root的原理,分析linux内核的本地提权漏洞(CVE-2017-8890),因此需要对linux kernel做源码调试。这篇文章主要参考张银奎前辈的三篇文章,主要记录如何下载调试需要的image、vmlinux和source code,以及借助vmware在两个ubuntu之间做linux kernel源码调试的方法。
选择ubuntu的一个好处是kernel已经开启了KGDB调试选项,可以通过cat /boot/config-
uname -r | grep -i "GDB"
查看当前内核是否支持KGDB。搭建过程持续了一周(2018/3/13 - 2018/3/17),环境搭建过程还是比较累的。
环境:
- vmware workstation 14.0
- ubuntu*2
- Linux version 4.10.0-19-generic (buildd@lgw01-15) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.4) ) #21~16.04.1-Ubuntu SMP Fri Apr 7 08:20:02 UTC 2017
0x00 选择调试的linux kernel版本
CVE-2017-8890在 Linux 4.11-rc8 版本中已经被启明星辰ADLab发现。启明星辰ADLab已第一时间将“Phoenix Talon”漏洞反馈给了Linux 内核社区,漏洞上报后Linux社区在Linux 4.12-rc1中合并了修复该问题的补丁。
http://www.cnnetsec.com/2619.html
虽然漏洞公告中给出了受影响的kernel version list,但是实际测试时,POC在kernel 4.4.X也有受影响。本来考虑选择在 Linux 4.11-rc8 kernel分析和调试。问题在于:Linux 4.11-rc8 可以下载到kernel 的image 和 header二进制文件,却下载不到对应准确版本的符号文件vmlinux。因此,首先搜索ubuntu哪些版本的linux kernel的vmlinux和 source code可以已下载到:
- 搜索linux内核版本
apt-cache search linux-image | grep linux-image-4.10.0 |grep generic
- 搜索含有dbgsym的内核版本:
apt-cache search linux-image | grep dbgsym |grep 4.10
- 搜索特定source code的内核版本
apt-cache search linux-source
linux-source - Linux kernel source with Ubuntu patches
linux-source-4.4.0 - Linux kernel source for version 4.4.0 with Ubuntu patches
linux-source-4.10.0 - Linux kernel source for version 4.10.0 with Ubuntu patches
linux-source-4.11.0 - Linux kernel source for version 4.11.0 with Ubuntu patches
linux-source-4.13.0 - Linux kernel source for version 4.13.0 with Ubuntu patches
linux-source-4.8.0 - Linux kernel source for version 4.8.0 with Ubuntu patches
最终我发现4.10.0的东西是比较全的,这里我选择下载kernel version 4.10.0-19-generic
。
0x01 安装kernel image、dbsym、source code
kernel image是内核会运行的文件,dbgsym一般是gdb用来调试的vmlinux,source code就是源码。
安装kernel image
- 搜索要下载的linux内核版本
apt-cache search linux-image | grep linux-image-4.10.0 |grep generic
- 安装内核
sudo apt-get install linux-image-4.10.0-19-generic
- 查看已安装的内核版本
sudo dpkg --list | grep linux-image
也可以从网站上下载deb包后安装:
- http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.10/
- https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/stable-review/
重启并验证新的内核已经被使用 uname -sr
安装dbgsym符号文件
增加符号文件对应的source.list ,更新源文件。
# 增加source.list
codename=$(lsb_release -c | awk '{print $2}')
sudo tee /etc/apt/sources.list.d/ddebs.list << EOF
deb http://ddebs.ubuntu.com/ ${codename} main restricted universe multiverse
deb http://ddebs.ubuntu.com/ ${codename}-security main restricted universe multiverse
deb http://ddebs.ubuntu.com/ ${codename}-updates main restricted universe multiverse
deb http://ddebs.ubuntu.com/ ${codename}-proposed main restricted universe multiverse
EOF
# 添加访问符号服务器的秘钥文件
wget -O - http://ddebs.ubuntu.com/dbgsym-release-key.asc | sudo apt-key add -
# 更新源文件
sudo apt-get update
通过apt-get下载dbgsym,默认保存在/usr/lib/debug/boot中,文件名是vmlinux-4.10.0-19-generic。
sudo apt-get install linux-image-`uname -r`-dbgsym
(可选)另一种搜索当前内核的 dbgsym的方法:
sudo apt-get install aptitude
sudo aptitude search 'linux-image-'$(uname -r)'-dbgsym'
获取kernel对应的源码
一、apt安装
打开/etc/apt/sources.list,启用deb-src,执行sudo apt-get update更新源文件:
deb-src http://cn.archive.ubuntu.com/ubuntu/ xenial main restricted
- 搜索所有的source code:
apt-cache search linux-source
- 安装指定版本的source code:
sudo apt-get install linux-source-4.10.0
第一种方法搜索kernel source code时只存在几个大版本,如4.10.0,下载之后发现可能是4.10.0的一个细分版本:4.10.0-xx(Makefile中的前几行定义了源码的版本)。所以猜测实际上运行的kernel和source code还是有一些出入的,gdb调试的时候对应关系可能不太一致。
二、 下载linux kernel源码切换分支
查看git仓库地址:https://wiki.ubuntu.com/Kernel/Dev/KernelGitGuide
git clone git://kernel.ubuntu.com/ubuntu/ubuntu-xenial.git.
git tag | grep 4.10.0-19.
git checkout ubuntu-4.10.0-19……..
这时的源码和内核二进制文件就是一一对应的了。
一切都安装好了之后,就可以把ubuntu复制一份了,一份作为debugging(调试机),一份作为debuggee(被调试机、目标机)。
0x02 搭建双机调试环境
双机调试原理
在vmware中实现ubuntu双机调试的原理,是建立两台ubuntu之间的串口通信,在虚拟机设置中增加serial port
。假设两台ubuntu一台是debugging、一台是debuggee。配置debugging 的串口为server,debuggee的串口为client。两者通过串口借助物理机(linux)的/tmp/serial 通信,如果是windows需要设置为//./pipe/com_1。
注意这里有几个坑点:
- 最好给debuggee至少两个核,否则单步调试非常慢。
- 在vmware中默认是开启了打印机的,默认也会使用串口。因此/dev/ttyS0可能会被打印机占用,导致使用/dev/ttyS0设备实现串口通信失败,这里将打印机删除即可。(参考 http://bbs.chinaunix.net/thread-4085165-1-1.html)
搭建过程
安装好了kernel之后,ubuntu默认会选择进入一个最新的kernel,也可以在开机时按住SHIFT
选择想进入的kernel版本。
配置debuggee
需要让debuggee在开机时进入KGDB的被调试状态,首先需要修改grub文件,增加grub引导时的菜单项(menuentry)。
sudo vim /etv/grub.d/40_custom
修改的内容可以从/boot/grub/grub.cfg中复制。在菜单名中增加调试信息,并在内核命令行中增加KGDB选项。
#!/bin/sh
exec tail -n +3 $0
# This file provides an easy way to add custom menu entries. Simply type the
# menu entries you want to add after this comment. Be careful not to change
# the 'exec tail' line above.
menuentry 'Ubuntu, KGDB with nokaslr' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-bf306d0a-28c8-49c6-bffc-446be272ddcf' {
recordfail
load_video
gfxmode $linux_gfx_mode
insmod gzio
if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi
insmod part_msdos
insmod ext2
set root='hd0,msdos1'
if [ x$feature_platform_search_hint = xy ]; then
search --no-floppy --fs-uuid --set=root --hint-bios=hd0,msdos1 --hint-efi=hd0,msdos1 --hint-baremetal=ahci0,msdos1 bf306d0a-28c8-49c6-bffc-446be272ddcf
else
search --no-floppy --fs-uuid --set=root bf306d0a-28c8-49c6-bffc-446be272ddcf
fi
echo 'Loading Linux 4.10.0-19 with KGDB built by GEDU lab...'
linux /boot/vmlinuz-4.10.0-19-generic root=UUID=bf306d0a-28c8-49c6-bffc-446be272ddcf ro find_preseed=/preseed.cfg auto noprompt priority=critical locale=en_US quiet kgdbwait kgdb8250=io,03f8,ttyS0,115200,4 kgdboc=ttyS0,115200 kgdbcon nokaslr
echo 'Loading initial ramdisk ...'
initrd /boot/initrd.img-4.10.0-19-generic
}
更新grub后,重启按住SHIFT 进入grub选择刚才的menu即可等到进入被调试模式。
sudo update-grub
reboot
一开始并没有加入nokaslr
选项时。会存在不能下断点,bt栈回溯无法获取地址对于的文件信息。本来以为是linux内核的编译选项导致,这里也有人遇到了同样的情况(https://linux.cn/forum.php?mod=viewthread&tid=16243)。
配置debugging
- 设置串口通信波特率:
stty -F /dev/ttyS0 115200
- 编写gdb config,在vmlinux时,可以通过source加载。
set architecture i386:x86-64:intel
target remote /dev/ttyS0
- 使用gdb来调试带符号的vmlinux,这时gdb就会尝试使用串口/dev/ttyS0来调试。
gdb -s /usr/lib/debug/boot/vmlinux-4.10.0-19-generic
gdb> source config
符号加载完成,bt
查看当前栈帧,c
运行内核。
关于源码的查看:
在gdb中使用l
可以查看相应的源码,但是由于vmlinux编译时似乎是使用绝对路径编译的,因此可以建立相应的绝对路径把source code放进去。还有一种方法是,设置set substitute-path PATH1 PATH2
,PATH1是vmlinux中的路径信息,PATH2是source code存放的真实路径。
单步调试
c
运行内核后,debuggee就跑起来了。这时,基本的gdb命令都可以使用,但是如果用n单步运行时,可能过了很久都没有反应。举个栗子:
- 在debugging随便下一个断
break ip_mc_leave_src
,后c运行debuggee。 - 在debuggee中运行poc,在断点处停了。
- 在debugging中,使用n单步,则发现gdb处于等待状态。debugeee也处于阻塞状态:
debugging:
经过一番探索之后,找到了解决方案:
- debuggee> 使用"echo g > "/proc/sysrq-trigger" 进入kgdb的被调试状态;
sudo su && echo g > "/proc/sysrq-trigger"
- debugging> 下断点后
c
继续运行debuggee; - debuggee> 运行poc触发断点;
- debugging> 可以使用n单步调试。
注:debuggee如果不能进入kgdb中断,可用cat /sys/module/kgdboc/parameters/kgdboc
检查的kgdboc参数是否设置正确。
sysrq还有很多强大的功能,具体可以参考这里:https://www.cnblogs.com/justin-y-lin/p/5424555.html
#查看
cat /proc/sys/kernel/sysrq
#配置
echo X >/proc/sys/kernel/sysrq
#或修改/etc/sysctl.conf中设置kernel.sysrq=1
echo m > /proc/sysrq-trigger #导出内存分配信息
echo t > /proc/sysrq-trigger #导出当前任务状态信息
echo p > /proc/sysrq-trigger #导出当前CPU寄存器和标志位信息
echo c > /proc/sysrq-trigger #产生空指针panic事件,人为导致系统崩溃
echo s > /proc/sysrq-trigger #即时同步所有挂载的文件系统
echo u > /proc/sysrq-trigger #即时重新挂载所有的文件系统为只读
echo w > /proc/sysrq-trigger #转储处于uninterruptable阻塞状态的任务
转向kdump
调试后期,由于借助kgdb来调试时,一旦执行到用户态的shellcode时,gdb中就会输出大量的垃圾信息,这时可以借助kdump来看oops信息,使用过程如下:
#安装kdump
sudo apt-get install linux-crashdump
service kdump-tools start
#检查kdump是否可用
查看/boot/grub/grub.cfg是否含有crashkernel
如果是自定义启动项,手工修改/etc/grub.d/40-custom中增加crashkernel
#检查另一个kernel是否启动
/sys/kernel/kexec_crash_loaded 1表示已经加载 0表示没有加载
#测试
echo c > "/etc/sysrq-trigger"
....等待重启后在/var/crash下生成了日志
解压 crash /usr/lib/debug/boot/vmlinux-4.10.0-19-generic /tmp/kernel-crash
查看/tmp/kernel-crash/VmCoreDmesg信息即可。
success
至此调试环境搭建完成。(由于现在换到windows下调试了,补一张windows的vmware截图:
0xFF 运行poc查看崩溃信息
内核跑起来后,在debuggee中运行CVE-2017-8890的poc,发现程序崩溃了。oops信息可以在gdb中看到,如果在gdb中看不到,可以尝试用CTRL+SHIFT+F1/F7 切换到命令行模式,可以看到很完整的oops信息。
查看崩溃点<ip_mc_leave_src+37>: mov ecx,DWORD PTR [rbx+0x4]
,此时rbx为0x2,因此从0x6中取值失败,存在对空指针引用。
gdb-peda$ info registers
rax 0x0 0x0
rbx 0x2 0x2
rcx 0x1 0x1
rdx 0x0 0x0
rsi 0xffff8800777bd2c0 0xffff8800777bd2c0
rdi 0x0 0x0
rbp 0xffffc90003ecfde0 0xffffc90003ecfde0
rsp 0xffffc90003ecfdc0 0xffffc90003ecfdc0
r8 0x0 0x0
r9 0x0 0x0
r10 0xffff880064c24db0 0xffff880064c24db0
r11 0xffff880074911110 0xffff880074911110
r12 0xffff8800777bd2c0 0xffff8800777bd2c0
r13 0xffff8800772a2138 0xffff8800772a2138
r14 0xffff8800772a2000 0xffff8800772a2000
r15 0x0 0x0
rip 0xffffffff8182f4a5 0xffffffff8182f4a5 <ip_mc_leave_src+37>
eflags 0x10202 [ IF RF ]
cs 0x10 0x10
ss 0x18 0x18
ds 0x0 0x0
es 0x0 0x0
fs 0x0 0x0
gs 0xb 0xb
gdb-peda$ x/50i ip_mc_leave_src
0xffffffff8182f480 <ip_mc_leave_src>: nop DWORD PTR [rax+rax*1+0x0]
0xffffffff8182f485 <ip_mc_leave_src+5>: push rbp
0xffffffff8182f486 <ip_mc_leave_src+6>: mov rbp,rsp
0xffffffff8182f489 <ip_mc_leave_src+9>: push r14
0xffffffff8182f48b <ip_mc_leave_src+11>: push r13
0xffffffff8182f48d <ip_mc_leave_src+13>: push r12
0xffffffff8182f48f <ip_mc_leave_src+15>: push rbx
0xffffffff8182f490 <ip_mc_leave_src+16>: mov r14,rdi
0xffffffff8182f493 <ip_mc_leave_src+19>: mov rbx,QWORD PTR [rsi+0x18]
0xffffffff8182f497 <ip_mc_leave_src+23>: mov r12,rsi
0xffffffff8182f49a <ip_mc_leave_src+26>: mov rdi,rdx
0xffffffff8182f49d <ip_mc_leave_src+29>: test rbx,rbx
0xffffffff8182f4a0 <ip_mc_leave_src+32>: je 0xffffffff8182f4ef <ip_mc_leave_src+111>
0xffffffff8182f4a2 <ip_mc_leave_src+34>: mov edx,DWORD PTR [rsi+0x14]
=> 0xffffffff8182f4a5 <ip_mc_leave_src+37>: mov ecx,DWORD PTR [rbx+0x4]
0xffffffff8182f4a8 <ip_mc_leave_src+40>: lea rsi,[rsi+0x8]
0xffffffff8182f4ac <ip_mc_leave_src+44>: lea r8,[rbx+0x18]
0xffffffff8182f4b0 <ip_mc_leave_src+48>: xor r9d,r9d
0xffffffff8182f4b3 <ip_mc_leave_src+51>: call 0xffffffff8182f110 <ip_mc_del_src>
0xffffffff8182f4b8 <ip_mc_leave_src+56>: mov QWORD PTR [r12+0x18],0x0
0xffffffff8182f4c1 <ip_mc_leave_src+65>: mov r13d,eax
0xffffffff8182f4c4 <ip_mc_leave_src+68>: mov eax,DWORD PTR [rbx]
0xffffffff8182f4c6 <ip_mc_leave_src+70>: lea eax,[rax*4+0x18]
0xffffffff8182f4cd <ip_mc_leave_src+77>: sub DWORD PTR ds:[r14+0x138],eax
0xffffffff8182f4d5 <ip_mc_leave_src+85>: lea rdi,[rbx+0x8]
0xffffffff8182f4d9 <ip_mc_leave_src+89>: mov esi,0x8
0xffffffff8182f4de <ip_mc_leave_src+94>: call 0xffffffff810f4a40 <kfree_call_rcu>
0xffffffff8182f4e3 <ip_mc_leave_src+99>: pop rbx
0xffffffff8182f4e4 <ip_mc_leave_src+100>: mov eax,r13d
0xffffffff8182f4e7 <ip_mc_leave_src+103>: pop r12
0xffffffff8182f4e9 <ip_mc_leave_src+105>: pop r13
0xffffffff8182f4eb <ip_mc_leave_src+107>: pop r14
0xffffffff8182f4ed <ip_mc_leave_src+109>: pop rbp
0xffffffff8182f4ee <ip_mc_leave_src+110>: ret
0xffffffff8182f4ef <ip_mc_leave_src+111>: mov edx,DWORD PTR [rsi+0x14]
0xffffffff8182f4f2 <ip_mc_leave_src+114>: lea rsi,[rsi+0x8]
0xffffffff8182f4f6 <ip_mc_leave_src+118>: xor r9d,r9d
0xffffffff8182f4f9 <ip_mc_leave_src+121>: xor r8d,r8d
0xffffffff8182f4fc <ip_mc_leave_src+124>: xor ecx,ecx
0xffffffff8182f4fe <ip_mc_leave_src+126>: call 0xffffffff8182f110 <ip_mc_del_src>
0xffffffff8182f503 <ip_mc_leave_src+131>: pop rbx
0xffffffff8182f504 <ip_mc_leave_src+132>: pop r12
0xffffffff8182f506 <ip_mc_leave_src+134>: pop r13
0xffffffff8182f508 <ip_mc_leave_src+136>: pop r14
0xffffffff8182f50a <ip_mc_leave_src+138>: pop rbp
0xffffffff8182f50b <ip_mc_leave_src+139>: ret
0xffffffff8182f50c: nop DWORD PTR [rax+0x0]
0xffffffff8182f510 <igmpv3_newpack>: nop DWORD PTR [rax+rax*1+0x0]
0xffffffff8182f515 <igmpv3_newpack+5>: push rbp
0xffffffff8182f516 <igmpv3_newpack+6>: mov rbp,rsp
REF
- Ubuntu内核调试要点(上)——准备符号和源文件 http://advdbg.org/blogs/advdbg_system/articles/7147.aspx
- Ubuntu内核调试要点(中)——配置目标机和建立调试会话 http://advdbg.org/blogs/advdbg_system/articles/7148.aspx
- Ubuntu内核调试要点(下)——常见问题 http://advdbg.org/blogs/advdbg_system/articles/7149.aspx
- https://help.ubuntu.com/lts/serverguide/kernel-crash-dump.html
- https://www.ibm.com/developerworks/cn/linux/l-cn-dumpanalyse/
- kdump https://www.cnblogs.com/wwang/archive/2010/11/19/1881304.html
- 讲oops信息很不错的一篇文章 http://www.cnblogs.com/wwang/archive/2010/11/14/1876735.html