标题: Patch crash绕过兼容性BUG 创建: 2021-01-26 12:09 更新: 2021-01-28 10:47 链接: https://scz.617.cn/unix/202101261209.txt 因故要对付古董内核2.6.18-308.24.1.el5,本文具体内容已过时,但思路是通用的。 crash有点Windows上livekd的味道,我猜这东西是受Solaris的scat启发,就跟 SystemTap绝对受Solaris的DTrace启发一样。如果折腾的事涉及内核,SystemTap和 crash或许都可以试试,gdb就不用强调了。 $ yum install crash $ crash /usr/lib/debug/lib/modules/2.6.18-308.24.1.el5/vmlinux /dev/mem ... WARNING: /usr/lib/debug/lib/modules/2.6.18-308.24.1.el5/vmlinux and /proc/version do not match! WARNING: /proc/version indicates kernel version: 2.6.18-308.24.1.el5 crash: please use the vmlinux file for that kernel version, or try using the System.map for that kernel version as an additional argument. 这就扯淡了,一面说内核版本不匹配,一面显示出来的内核版本是匹配的。都是用官 方RPM装的,没有自编译内核。在非常确认内核版本匹配的前提下,只能说crash的兼 容性有BUG。 若你的目标系统crash可以用/dev/mem,就不要往下看了,本文与你无关。 $ crash /usr/lib/debug/lib/modules/2.6.18-308.24.1.el5/vmlinux /proc/kcore ... /proc/kcore: read: No such file or directory crash: /proc/kcore: initialization failed 同样扯淡,/proc/kcore明明存在、可访问,crash说找不到,BUG真多。 $ ls -l /proc/kcore -r-------- 1 root root 4096 Jan 26 11:11 /proc/kcore $ file -b /proc/kcore ELF 64-bit LSB core file AMD x86-64, version 1 (SYSV), SVR4-style, from 'vmlinux' 最后只能这样启动crash: $ crash /usr/lib/debug/lib/modules/2.6.18-308.24.1.el5/vmlinux /boot/System.map-2.6.18-308.24.1.el5 ... SYSTEM MAP: /boot/System.map-2.6.18-308.24.1.el5 DEBUG KERNEL: /usr/lib/debug/lib/modules/2.6.18-308.24.1.el5/vmlinux (2.6.18-308.24.1.el5) DUMPFILE: /dev/crash ... 此种方式进入crash提示符与是否Patch内核函数devmem_is_allowed()使之恒返回1无 关。 crash> dis devmem_is_allowed 0xffffffff80083bb8 : cmp $0x100,%rdi 0xffffffff80083bbf : jbe 0xffffffff80083c0d 0xffffffff80083bc1 : mov 0x437938(%rip),%r8d # 0xffffffff804bb500 0xffffffff80083bc8 : xor %esi,%esi 0xffffffff80083bca : jmp 0xffffffff80083c08 0xffffffff80083bcc : movslq %esi,%rax 0xffffffff80083bcf : imul $0x14,%rax,%rcx 0xffffffff80083bd3 : cmpl $0x1,-0x7fb44aec(%rcx) 0xffffffff80083bda : jne 0xffffffff80083c06 0xffffffff80083bdc : mov -0x7fb44afc(%rcx),%rdx 0xffffffff80083be3 : lea 0xfff(%rdx),%rax 0xffffffff80083bea : shr $0xc,%rax 0xffffffff80083bee : cmp %rax,%rdi 0xffffffff80083bf1 : jb 0xffffffff80083c06 0xffffffff80083bf3 : add -0x7fb44af4(%rcx),%rdx 0xffffffff80083bfa : shr $0xc,%rdx 0xffffffff80083bfe : cmp %rdx,%rdi 0xffffffff80083c01 : jae 0xffffffff80083c06 0xffffffff80083c03 : xor %eax,%eax 0xffffffff80083c05 : retq 0xffffffff80083c06 : inc %esi 0xffffffff80083c08 : cmp %r8d,%esi 0xffffffff80083c0b : jl 0xffffffff80083bcc 0xffffffff80083c0d : mov $0x1,%eax 0xffffffff80083c12 : retq crash> rd -8 devmem_is_allowed 91 ffffffff80083bb8: 48 81 ff 00 01 00 00 76 4c 44 8b 05 38 79 43 00 H......vLD..8yC. ffffffff80083bc8: 31 f6 eb 3c 48 63 c6 48 6b c8 14 83 b9 14 b5 4b 1.. rd -16 0xffffffff80083c03 ffffffff80083c03: c031 1. 上面这些命令都正常执行。但这种方式启动crash,只读不可写: crash> wr -k -16 0xffffffff80083c03 0xc0ff wr: cannot write to /dev/crash! 就读操作而言,crash与/dev/mem同步,真实反应内核态内存变化。 为了可读可写,还得用/dev/mem。 考虑到crash这种工具也不是给普通人用的,还是自己解决兼容性BUG吧。或许高版本 crash已无此兼容性BUG,但基于各种原因,没有自编译安装高版本crash。 解决思路很简单,肯定有个函数在检查内核版本是否匹配,冒险让它返回真试试。可 以直接IDA反汇编crash,不过这是Linux开源世界,没必要跟自己过不去,先看源码。 $ rpm -qif $(which crash) | grep src.rpm Group : Development/Debuggers Source RPM: crash-5.1.8-1.el5.centos.src.rpm http://ftp.iij.ad.jp/pub/linux/centos-vault/5.9/os/SRPMS/crash-5.1.8-1.el5.centos.src.rpm Source Insight中搜"do not match!",在filesys.c中找到match_proc_version(): -------------------------------------------------------------------------- /* * If only a namelist argument is entered for a live system, and the * version string doesn't match /proc/version, try to avert a failure * by assigning it to a matching System.map. */ static void match_proc_version(void) { char buffer[BUFSIZE], *p1, *p2; if (pc->flags & KERNEL_DEBUG_QUERY) return; if (!strlen(kt->proc_version)) return; /* * 目标环境中match_file_string()返回假,设法让其返回真 */ if (match_file_string(pc->namelist, kt->proc_version, buffer)) { if (CRASHDEBUG(1)) { fprintf(fp, "/proc/version:\n%s\n", kt->proc_version); fprintf(fp, "%s:\n%s", pc->namelist, buffer); } return; } error(WARNING, "%s%sand /proc/version do not match!\n\n", pc->namelist, strlen(pc->namelist) > 39 ? "\n " : " "); ... -------------------------------------------------------------------------- 修改源码并重新编译crash是一种方案,因故不选这条路。此外,若只是绕过一个检 查,只改几个字节,直接Patch二进制更符合我们的人设。 用IDA反汇编crash,match_proc_version()被inline展开到主调函数fd_init()中, match_file_string()的符号则被strip掉了。通过"/proc/version do not match!" 的交叉引用定位: -------------------------------------------------------------------------- /* * match_file_string()是我自己重命名回来的,缺省没有这个符号 */ 00000000004926D1 E8 BA E2 FF FF call match_file_string /* * 流程至此时ZF未置位,将"test eax,eax"改成"inc eax"或"nop nop"即可 */ 00000000004926D6 85 C0 test eax, eax 00000000004926D8 0F 84 C6 05 00 00 jz loc_492CA4 -------------------------------------------------------------------------- 上面注释已经说了理论上的Patch方案,先用gdb动态Patch测试一下: $ gdb -q -nx -x /tmp/gdbinit_x64.txt /usr/bin/crash (gdb) display/5i $pc (gdb) x/5i 0x4926D6 0x4926d6 : test eax,eax 0x4926d8 : je 0x492ca4 0x4926de : mov rax,QWORD PTR [rip+0x6b09db] # 0xb430c0 0x4926e5 : cmp QWORD PTR [rax+0xcb0],0x0 0x4926ed : je 0x492556 (gdb) tb *0x4926d6 (gdb) r /usr/lib/debug/lib/modules/2.6.18-308.24.1.el5/vmlinux /dev/mem (gdb) i r rax eflags rax 0x0 0 eflags 0x206 [ PF IF ] 热Patch: (gdb) set $rax=1 (gdb) c 另一种热Patch: (gdb) set *(unsigned short int *)0x4926d6=0x9090 (gdb) c 热Patch后对/proc/version的检查就绕过了,但这样还是用不了/dev/mem: crash: read error: kernel virtual address: ffffffff804bd2e0 type: "possible" WARNING: cannot read cpu_possible_map crash: read error: kernel virtual address: ffffffff8045f5a0 type: "present" WARNING: cannot read cpu_present_map crash: read error: kernel virtual address: ffffffff8045a260 type: "online" WARNING: cannot read cpu_online_map crash: read error: kernel virtual address: ffffffff80461210 type: "xtime" crash: this kernel may be configured with CONFIG_STRICT_DEVMEM, which renders /dev/mem unusable as a live memory source. crash: trying /proc/kcore as an alternative to /dev/mem /proc/kcore: read: Operation not permitted crash: /proc/kcore: initialization failed Program exited with code 01. 目标系统crash用不了/dev/mem,除了crash本身的兼容性BUG,还有一个来自内核态 的安全限制。crash会检查编译内核时是否指定过CONFIG_STRICT_DEVMEM,是否能不 受限地读取/dev/mem,这个安全限制是内核态的,不是crash应用程序本身的。缺省 情况下用户态进程读取/dev/mem受限,只能读写前1MB多的数据,于是crash转而使用 /proc/kcore,也失败了。crash使用/proc/kcore失败应该是另一个兼容性BUG,我懒 得理它,盯着/dev/mem用吧。 不考虑自编译内核时禁用CONFIG_STRICT_DEVMEM,至少有两种方式热消除这个安全限 制。用SystemTap让内核函数devmem_is_allowed()恒返回1: $ stap -g -e 'probe kernel.function("devmem_is_allowed").return {$return=1}' 或者自己写LLKM去Patch devmem_is_allowed(): $ insmod /tmp/kernelwrite.ko address=0xffffffff80083c03 value=ffc0 insmod: error inserting '/tmp/kernelwrite.ko': -1 Operation not permitted $ dmesg | tail -2 ffffffff80083c03: 31 c0 1. ffffffff80083c03: ff c0 .. $ xxd -g 1 -s $[0xffffffff80083c03-0xffffffff7fe00000] -l 2 /dev/mem 0283c03: ff c0 .. 切勿照搬上面的命令,领会精神后自行反汇编并Patch相应地址。 假设已经消除CONFIG_STRICT_DEVMEM限制,如下gdb操作对crash进行动态Patch: $ gdb -q -nx -x /tmp/gdbinit_x64.txt /usr/bin/crash tb *0x4926d6 commands $bpnum silent i r rax eflags set *(unsigned short int *)$rip=0x9090 c end r /usr/lib/debug/lib/modules/2.6.18-308.24.1.el5/vmlinux /dev/mem 进入crash提示符,可读可写。 静态Patch如下: $ fc /b crash.orig crash 000926D6: 85 90 000926D7: C0 90 后续使用的常规组合是: $ stap -g -e 'probe kernel.function("devmem_is_allowed").return {$return=1}' 或 $ insmod /tmp/kernelwrite.ko address=0xffffffff80083c03 value=ffc0 $ crash /usr/lib/debug/lib/modules/2.6.18-308.24.1.el5/vmlinux /dev/mem