概要:在实机和虚拟机中都能正常运行的Linux程序放到WSL1中却秒报Segmentation fault退出。通过尝试学习Linux下的动态调试工具并配合IDA静态分析,找到了vsyscall这一问题根源(WSL1的一个小坑)。之后通过替换为syscall解决了问题,使程序在WSL1中成功运行。
1. 背景介绍:主机系统Win10,时常运行第三方虚拟机。因为微软自家的虚拟机平台会妨碍第三方虚拟机诸如AVX指令集等的特性传递,所以关闭了相应的功能,从而无法使用WSL2,但WSL1没有问题,而且轻便高效,与“正经”的Linux虚拟机可以相安无事、相得益彰。
然而,Linux下的某个单纯的CLI计算程序,按说应该很适合放在WSL1中运行,却一运行就报Segmentation fault。虽然放在虚拟机里运行也没有很大影响,但是作为一个折腾爱好者,还是很想搞明白这个问题,并尝试解决一下。
原始报错
2. 问题定位:本人还是逆向新手,懂一点汇编,用WinDbg和IDA复现过一些简单的逆向操作,但是对理论了解很少,而且不太会自动化。这次的程序倒没有脱壳之类的问题,而首先要解决的是找到Linux下的合适工具。因为是这几天现找现学的,所以用得比较粗浅笨拙,希望各位不要见笑。
第一个工具是strace,可以跟踪程序的系统调用和信号传递,通过加上-i参数,还能显示出系统调用时指令指针的位置。从strace的输出结果,我看到程序有一个正常的开始,经过若干正常的系统调用后,最终以如下方式结束。
[Shell] 纯文本查看 复制代码
......
[0000000001c1b730] read(4, "", 4096) = 0
[ffffffffff600400] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0xffffffffff600400} ---
[????????????????] +++ killed by SIGSEGV (core dumped) +++
Segmentation fault (core dumped)
从中可以看到Segmentation fault的更进一步信息:代码是SEGV_MAPERR,大意是内存映射错误;对应地址是0xffffffffff600400,按我理解是一个“明显溢出”的地址。马后炮的说,如果我当时就去百度一下这个地址,应该能更早定位这个问题。
为了搞清这个“指针溢出”是怎么发生的,我下一步想要进行动态调试,这就轮到第二个工具——gdb了。不过首先要解决gdb找不到调试符号的问题。
gdb找不到符号表
首先使用info files命令找到程序的入口地址Entry point: 0x400400,然后用break *0x400400下断点,再执行run就会停在程序入口,之后就能执行各种单步运行、下断点、反汇编的指令了。之前strace得到的指令地址也可以用来下断点,有助于尽快接近结束位置。一顿操作之后,找到了这段代码:
[Asm] 纯文本查看 复制代码
0x0000000001c5a4e0: sub rsp, 8
0x0000000001c5a4e4: mov rax, 0xffffffffff600400
0x0000000001c5a4eb: call rax
0x0000000001c5a4ed: add rsp, 8
0x0000000001c5a4f1: retn
3. 成因解释:和我预想不同,那个“明显溢出”的地址并非由程序错误导致,而是写死在代码里的。而且在这段代码下面还有一段类似的访问0xffffffffff600000的代码。这个发现促使我重新去审视这两个地址的意义,经过搜索,参考这篇文章才得知这是linux内核在ffffffffff600000-ffffffffff601000预留的vsyscall位置,封装了3个系统调用函数gettimeofday, time和getcpu,借用一张图:
vsyscall
使用cat /proc/self/maps命令,可以看到区别:
在可以运行的系统中:
[Shell] 纯文本查看 复制代码
......
7f60750ee000-7f60750ef000 rw-p 00000000 00:00 0
7ffec1210000-7ffec1231000 rw-p 00000000 00:00 0 [stack]
7ffec1238000-7ffec123b000 r--p 00000000 00:00 0 [vvar]
7ffec123b000-7ffec123d000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
在WSL1中:
[Shell] 纯文本查看 复制代码
......
7f72aaaa0000-7f72aaaa1000 rw-p 0000a000 00:00 273107 /usr/bin/cat
7fffe3d60000-7fffe3d81000 rw-p 00000000 00:00 0 [heap]
7fffea617000-7fffeae17000 rw-p 00000000 00:00 0 [stack]
7fffeb056000-7fffeb057000 r-xp 00000000 00:00 0 [vdso]
可知WSL1没有映射vsyscall这段,这就是导致Segmentation fault的原因。
4. 问题解决:了解成因后也找到了相关的issue,只是现在已经被关闭了。关闭的理由是WSL2已经可以设置内核选项 kernelCommandLine = vsyscall=emulate 来修复这个问题,而WSL1就没人管了。好在定位了问题以后,解决方案也比较明确了:只要把vsyscall的调用都换成syscall就行了。rax中的地址换成对应的系统调用序号(可以在 /usr/include/x86_64-linux-gnu/asm/unistd_64.h 中找到),call换成syscall (0F05),其它寄存器等都是兼容的不用修改。
time
gettimeofday
这一修改不改变指令长度,完成gettimeofday和time对应的两处修改后程序在WSL1中成功运行。
PS:现今的多数内核也需要添加启动参数vsyscall=emulate才会映射vsyscall,否则一样会Segmentation fault。隐约记得早先装虚拟机时为了补救Meltdown补丁导致性能下降的问题加了这个参数,没想到隔了这么久又遇到了。 |