由于想要学习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 ([email protected]) (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

  1. 搜索要下载的linux内核版本 apt-cache search linux-image | grep linux-image-4.10.0 |grep generic
  2. 安装内核 sudo apt-get install linux-image-4.10.0-19-generic
  3. 查看已安装的内核版本 sudo dpkg --list | grep linux-image

也可以从网站上下载deb包后安装:

重启并验证新的内核已经被使用 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。
1521276992152

注意这里有几个坑点:

  1. 最好给debuggee至少两个核,否则单步调试非常慢。
  2. 在vmware中默认是开启了打印机的,默认也会使用串口。因此/dev/ttyS0可能会被打印机占用,导致使用/dev/ttyS0设备实现串口通信失败,这里将打印机删除即可。(参考 http://bbs.chinaunix.net/thread-4085165-1-1.html) Alt text

搭建过程

安装好了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

  1. 设置串口通信波特率: bash stty -F /dev/ttyS0 115200
  2. 编写gdb config,在vmlinux时,可以通过source加载。 set architecture i386:x86-64:intel
    target remote /dev/ttyS0
  3. 使用gdb来调试带符号的vmlinux,这时gdb就会尝试使用串口/dev/ttyS0来调试。 gdb -s /usr/lib/debug/boot/vmlinux-4.10.0-19-generic
    gdb> source config

符号加载完成,bt查看当前栈帧,c运行内核。
Alt text

关于源码的查看:

在gdb中使用l可以查看相应的源码,但是由于vmlinux编译时似乎是使用绝对路径编译的,因此可以建立相应的绝对路径把source code放进去。还有一种方法是,设置set substitute-path PATH1 PATH2,PATH1是vmlinux中的路径信息,PATH2是source code存放的真实路径。

单步调试

c运行内核后,debuggee就跑起来了。这时,基本的gdb命令都可以使用,但是如果用n单步运行时,可能过了很久都没有反应。举个栗子:

  1. 在debugging随便下一个断break ip_mc_leave_src,后c运行debuggee。
  2. 在debuggee中运行poc,在断点处停了。
  3. 在debugging中,使用n单步,则发现gdb处于等待状态。debugeee也处于阻塞状态:Alt text

debugging:

Alt text

经过一番探索之后,找到了解决方案:

  1. debuggee> 使用"echo g > "/proc/sysrq-trigger" 进入kgdb的被调试状态; sudo su && echo g > "/proc/sysrq-trigger"
  2. debugging> 下断点后c继续运行debuggee;
  3. debuggee> 运行poc触发断点;
  4. 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信息即可。

Alt text

success

至此调试环境搭建完成。(由于现在换到windows下调试了,补一张windows的vmware截图:
Alt text

0xFF 运行poc查看崩溃信息

内核跑起来后,在debuggee中运行CVE-2017-8890的poc,发现程序崩溃了。oops信息可以在gdb中看到,如果在gdb中看不到,可以尝试用CTRL+SHIFT+F1/F7 切换到命令行模式,可以看到很完整的oops信息。
Alt text

查看崩溃点<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

  1. Ubuntu内核调试要点(上)——准备符号和源文件 http://advdbg.org/blogs/advdbg_system/articles/7147.aspx
  2. Ubuntu内核调试要点(中)——配置目标机和建立调试会话 http://advdbg.org/blogs/advdbg_system/articles/7148.aspx
  3. Ubuntu内核调试要点(下)——常见问题 http://advdbg.org/blogs/advdbg_system/articles/7149.aspx
  4. https://help.ubuntu.com/lts/serverguide/kernel-crash-dump.html
  5. https://www.ibm.com/developerworks/cn/linux/l-cn-dumpanalyse/
  6. kdump https://www.cnblogs.com/wwang/archive/2010/11/19/1881304.html
  7. 讲oops信息很不错的一篇文章 http://www.cnblogs.com/wwang/archive/2010/11/14/1876735.html