标题: 为什么getpwnam("daemon")失败 创建: 2021-04-12 17:43 更新: 2021-04-13 14:54 链接: https://scz.617.cn/unix/202104121743.txt 参看 《用特定动态链接器和LIBC执行ELF》 https://scz.617.cn/unix/202104091427.txt 本文实际是前文中的一段讨论,缘于一个真实案例。 简单说一下背景。在某x64环境中有个32位ELF,假设叫some。现将some及其依赖库( 包含动态链接器)迁移到另一个完全不同的环境中,不是同一种发行版,内核、GLBC 版本有明显差别。第一想让some跑起来,第二想用gdb调试some及其依赖库。 有多种解决方案,但本质上没有太大区别。其中一种解决方案是: cp some some-new-3 patchelf --set-interpreter "./ld-*.so" some-new-3 patchelf --force-rpath --set-rpath "." some-new-3 注意"--force-rpath"的使用,欲知细节,参看前文。将some-new-3及其依赖库(包含 动态链接器)置于同一目录下,再用如下命令检查之: LD_TRACE_LOADED_OBJECTS=1 LD_WARN=yes LD_BIND_NOW=yes ./some-new-3 考虑到最广泛兼容性,此处未用ldd,不推荐ldd。检查无误后自认为依赖库已全部就 位,用gdb调试some-new-3,在main()设断,单步无误。 以为这样就行了,结果云海说有新麻烦。在原环境中some会以daemon形式运行,但在 新环境中执行Patch过的some-new-3,发现其自动结束,"ps auwx | grep some"找不 到进程,需要排查。 strace -v -i -f -ff -o some.log ./some-new-3 strace一般会产生大量输出,应该启用文件输出,并将父子进程的输出分隔开。在父 进程的strace输出中注意到文件: /var/log/some.log /var/run/some.ctl /var/run/some.pid 在some.log尾部看到 Switching to daemon user A FATAL Error has occured: missing 'daemon' id, exiting 用IDA反汇编some-new-3,通过"missing 'daemon' id, exiting"交叉引用发现因为 getpwnam("daemon")失败,导致进程主动结束。 云海找到一个参数使some-new-3不试图进入daemon状态,暂时规避了该问题,但他希 望我能找出getpwnam("daemon")失败的原因并解决之。 为什么getpwnam("daemon")失败?这个函数只是在找名为daemon的用户,如果没有 daemon用户,确实会失败,但新环境/etc/passwd里有daemon用户。曾经怀疑原环境、 新环境passwd文件格式不同,但passwd文件格式多少年前已定型,这种可能性极低。 getpwnam()就是读取passwd填充结构,能有多复杂以致失败? gdb -q -nx -x /tmp/gdbinit_x64.txt -x "/tmp/ShellPipeCommand.py" -x "/tmp/GetOffset.py" -ex 'display/5i $pc' ./some-new-3 此番涉及父子进程,为了调试子进程需要特殊设置 set follow-fork-mode child set follow-exec-mode new catch fork r 命中后 ni 确保在调试子进程。对getpwnam("daemon")的主调位置设断,单步跟踪,进入libc的 代码。先后到达过这些位置: b *__nscd_get_map_ref b *__nss_lookup (gdb) bt #0 0xf7d6e300 in __nscd_get_map_ref () from ./libc.so.6 #1 0xf7d6b5d7 in nscd_getpw_r () from ./libc.so.6 #2 0xf7d6b98d in __nscd_getpwnam_r () from ./libc.so.6 #3 0xf7d050d1 in getpwnam_r@@GLIBC_2.1.2 () from ./libc.so.6 #4 0xf7d04a8f in getpwnam () from ./libc.so.6 #5 0x0804dc0a in main () (gdb) bt #0 0xf7d4bde0 in __nss_lookup () from ./libc.so.6 #1 0xf7d4ce5c in __nss_passwd_lookup2 () from ./libc.so.6 #2 0xf7d05135 in getpwnam_r@@GLIBC_2.1.2 () from ./libc.so.6 #3 0xf7d04a8f in getpwnam () from ./libc.so.6 #4 0x0804dc0a in main () 没想到getpwnam()底层实现如此复杂,下载GLIBC源码,用Source Insight查看。 https://ftp.gnu.org/gnu/libc/glibc-2.12.2.tar.bz2 https://ftp.gnu.org/gnu/libc/glibc-2.1.2.tar.gz 硬是没找到getpwnam()的函数体,将就着看了看相关函数,不得要领。其中 __nscd_get_map_ref()看着像是在找/etc/passwd在内存中的映射,动态调试发现没 找到。 重看getpwnam(3),想到应该检查新环境中/etc/nsswitch.conf,别不是没配置files 项。但在nsswitch.conf中看到的是: passwd: files sss 重看nsswitch.conf(5),注意到: /lib/libnss_files.so.X implements "files" source. 意识到新环境当前目录下没有libnss_files库,getpwnam(3)为了读passwd,必须有 这个库,又一个天坑。用如下命令调试确认: $ LD_DEBUG=libs ./some-new-3 ... 27081: transferring control: ./some-new-3 27081: 27081: find library=libnss_files.so.2 [0]; searching 27081: search path=./tls/i686/sse2:./tls/i686:./tls/sse2:./tls:./i686/sse2:./i686:./sse2:. (RPATH from file ./some-new-3) ... 27081: find library=libnss_dns.so.2 [0]; searching 27081: search path=./tls/i686/sse2:./tls/i686:./tls/sse2:./tls:./i686/sse2:./i686:./sse2:. (RPATH from file ./some-new-3) ... 27081: find library=libnss_myhostname.so.2 [0]; searching ... 因为没找到libnss_files,所以又尝试找libnss_dns、libnss_myhostname。从原环 境中析取libnss_files-2.12.2.so到新环境当前目录,建符号链接: ln -s libnss_files-2.12.2.so libnss_files.so.2 再次执行 ./some-new-3 ps auwx | grep some 已能看到daemon化的some。原始问题已经解决,下面多讨论一些东西。 getpwnam("daemon")失败原因至少有二: a) daemon用户不存在,检查/etc/passwd b) libnss_files库未就位,用LD_DEBUG=libs检查 some-new-3何时加载libnss_files库? gdb -q -nx -x /tmp/gdbinit_x64.txt -x "/tmp/ShellPipeCommand.py" -x "/tmp/GetOffset.py" -ex 'display/5i $pc' ./some-new-3 catch load nss_files (gdb) bt #0 0xf7fef120 in _dl_debug_state () from ./ld-2.12.2.so #1 0xf7ff283c in dl_open_worker () from ./ld-2.12.2.so #2 0xf7fee756 in _dl_catch_error () from ./ld-2.12.2.so #3 0xf7ff2366 in _dl_open () from ./ld-2.12.2.so #4 0xf7d71992 in do_dlopen () from ./libc.so.6 #5 0xf7fee756 in _dl_catch_error () from ./ld-2.12.2.so #6 0xf7d71a86 in dlerror_run () from ./libc.so.6 #7 0xf7d71afb in __libc_dlopen_mode () from ./libc.so.6 #8 0xf7d4bc85 in __nss_lookup_function () from ./libc.so.6 #9 0xf7d4bdff in __nss_lookup () from ./libc.so.6 #10 0xf7d4cc7c in __nss_hosts_lookup2 () from ./libc.so.6 #11 0xf7d51e46 in gethostbyname_r@@GLIBC_2.1.2 () from ./libc.so.6 #12 0xf7d51566 in gethostbyname () from ./libc.so.6 #13 0x0805e9c8 in ... () #14 0x080554e1 in ... () #15 0x0804d382 in main () 父进程就会加载libnss_files库。"catch load"只有加载成功时才会命中,若想拦载 所有加载.so的企图,比如库不存在,但想知道在哪儿试图加载,用"b *do_dlopen"。 删掉符号链接做第二个实验: rm -f libnss_files.so.2 gdb -q -nx -x /tmp/gdbinit_x64.txt -x "/tmp/ShellPipeCommand.py" -x "/tmp/GetOffset.py" -ex 'display/5i $pc' ./some-new-3 b *_start set follow-fork-mode child set follow-exec-mode new catch fork r 命中_start()后增设断点 b *do_dlopen c 命中后查看调用栈回溯 (gdb) bt #0 0xf7d71930 in do_dlopen () from ./libc.so.6 #1 0xf7fee756 in _dl_catch_error () from ./ld-2.12.2.so #2 0xf7d71a86 in dlerror_run () from ./libc.so.6 #3 0xf7d71afb in __libc_dlopen_mode () from ./libc.so.6 #4 0xf7d4bc85 in __nss_lookup_function () from ./libc.so.6 #5 0xf7d4bdff in __nss_lookup () from ./libc.so.6 #6 0xf7d4cc7c in __nss_hosts_lookup2 () from ./libc.so.6 #7 0xf7d51e46 in gethostbyname_r@@GLIBC_2.1.2 () from ./libc.so.6 #8 0xf7d51566 in gethostbyname () from ./libc.so.6 #9 0x0805e9c8 in ... () #10 0x080554e1 in ... () #11 0x0804d382 in main () (gdb) x/s *(*(char***)($esp+4)) 0xffffcfe0: "libnss_files.so.2" 父进程中"b *do_dlopen"还有两次命中,分别对应libnss_dns、libnss_myhostname。 继续调试,直至"catch fork"命中 ni c 子进程中"b *do_dlopen"再次命中,对应libnss_sss。 (gdb) bt #0 0xf7d71930 in do_dlopen () from ./libc.so.6 #1 0xf7fee756 in _dl_catch_error () from ./ld-2.12.2.so #2 0xf7d71a86 in dlerror_run () from ./libc.so.6 #3 0xf7d71afb in __libc_dlopen_mode () from ./libc.so.6 #4 0xf7d4bc85 in __nss_lookup_function () from ./libc.so.6 #5 0xf7d4be34 in __nss_lookup () from ./libc.so.6 #6 0xf7d4ce5c in __nss_passwd_lookup2 () from ./libc.so.6 #7 0xf7d05135 in getpwnam_r@@GLIBC_2.1.2 () from ./libc.so.6 #8 0xf7d04a8f in getpwnam () from ./libc.so.6 #9 0x0804dc0a in main () (gdb) x/s *(*(char***)($esp+4)) 0xffffd140: "libnss_sss.so.2" 好像处理/etc/passwd的是__nss_passwd_lookup2(),未进一步确认。 some-new-3未显式调用dlopen(),gethostbyname()、getpwnam()隐式调用do_dlopen()。 不要用"b *dlopen"。libc中可能没有名为dlopen的符号,"b *dlopen"可能实际断在 其他库的"dlopen@plt"上,不够底层,很可能拦不住你想要的东西。 (gdb) info symbol dlopen dlopen@plt in section .plt of ./libcrypto.so.1.0.2 (gdb) info symbol do_dlopen do_dlopen in section .text of ./libc.so.6 关于这方面的讨论,参看: 《未知网络服务分析之调试技巧》 https://scz.617.cn/unix/201812111322.txt 恢复符号链接做第三个实验: ln -s libnss_files-2.12.2.so libnss_files.so.2 gdb -q -nx -x /tmp/gdbinit_x64.txt -x "/tmp/ShellPipeCommand.py" -x "/tmp/GetOffset.py" -ex 'display/5i $pc' ./some-new-3 set follow-fork-mode child set follow-exec-mode new catch fork r 命中后 ni b *do_dlopen b *__nscd_get_map_ref b *__nss_lookup 因父进程已成功加载libnss_files,子进程的"b *do_dlopen"不会命中,其余两个断 点仍会依次命中。c之后Ctrl-C断不下来,但可以从其他终端"kill -INT"。 父进程的strace日志中能看到加载libnss_files失败,但这是事后诸葛亮,毕竟有很 多失败的系统调用并不真地影响功能,不大可能提前知道哪次失败是致命的。 假设some-new-3自动结束,但没有/var/log/some.log可供排查,此时只能尝试 "b *_exit",待命中后查看调用栈回溯,这是普适方案。 rm -f libnss_files.so.2 gdb -q -nx -x /tmp/gdbinit_x64.txt -x "/tmp/ShellPipeCommand.py" -x "/tmp/GetOffset.py" -ex 'display/5i $pc' ./some-new-3 set follow-fork-mode child set follow-exec-mode new catch fork r 命中后 ni b *_exit c (gdb) bt #0 0xf7d06464 in _exit () from ./libc.so.6 #1 0xf7c95b9a in __run_exit_handlers () from ./libc.so.6 #2 0xf7c95bdf in exit () from ./libc.so.6 #3 0x080609ef in ... () #4 0x0804de88 in main () 收一下,本案例强调,检查ELF的依赖库,不要只用ldd或其变种技巧,要考虑动态加 载尤其是隐式动态加载的情形,"LD_DEBUG=libs"更有效。但是,"LD_DEBUG=libs"看 不到子进程试图动态加载的库,除非export后对子进程也用之,"strace -f -ff"可 以看到子进程试图动态加载的库。