为什么getpwnam(daemon)失败
2021-04-14 00:59:00 Author: mp.weixin.qq.com(查看原文) 阅读量:102 收藏

参看

《用特定动态链接器和LIBC执行ELF》
http://scz.617.cn:8/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

关于这方面的讨论,参看:

《未知网络服务分析之调试技巧》
http://scz.617.cn:8/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"可以看到子进程试图动态加载的库。


文章来源: http://mp.weixin.qq.com/s?__biz=MzUzMjQyMDE3Ng==&mid=2247484656&idx=1&sn=9b54b94dc2ad1255eb6dab146d8bf3ae&chksm=fab2c7cfcdc54ed9d092fd28492dbebce09315b6cac5b2bde07b82a5b91e2d025ca634e23f50#rd
如有侵权请联系:admin#unsafe.sh