=Start=
缘由
当初学习《UNIX环境高级编程》的时候不认真,现在只好慢慢来还之前欠下的技术债。下面的内容主要摘抄自百度运维团队的博客文章:现代 *NIX 的进程与 shell,根据自己的学习和理解程度有部分删减。
内容
现在 *nix 系统的进程运行机制都比较类似,99.9% 的系统都已经使用 ELF 格式作为可执行文件格式,a.out 格式已经基本淘汰。此外 parser 的形式也被几乎所有系统支持,即文件头部写 #!/some/parser 的脚本。以下全文都以这两种情况展开,不考虑历史或者极特殊系统。
1.什么是可执行文件
一个文件可以被执行的意思是这个文件不需要外界驱动,而可以直接作为 execve(2) 的第一参数。与之相反的就是不能被 execve(2) 直接作为第一参数,而必须在第二参数中作为参数传给第一参数指定的可执行文件。
对于 ELF 系统而言,只有 ELF 格式的文件是可执行的。一个 ELF 文件还需要在文件系统上具备执行权限,否则也无法作为第一参数。ELF 文件是否可以被执行取决于 ELF 文件自身是否是可执行的,例如 so 或者 detached debug symbols 也是 ELF,但是(99.9%)不能被执行。存在极少量特制的 so 可以被执行,例如 libc.so ,是特殊的 ELF 结构。ELF 的格式、结构不在本文的讨论当中。
大量非 ELF 格式的文件,例如脚本,是不能被操作系统载入并启动的。但是脚本文件可以作为 execve(2) 的第一参数,原因是 parser 体系。脚本解析器是解读脚本并且执行操作的 ELF 文件,例如常见的 shell 脚本,后缀名为 .sh 的,他们可以通过 sh script.sh 来运行。这个道理很简单,sh 是 ELF 文件,script.sh 被作为「数据」传递给了 sh 进行解读。为了方便这一类情况(这个情况占了可执行文件的大多数),操作系统规定了自动调用 parser 的格式:
#!/some/elf/file -param1 -param2
script line 1
script line 2
…
第一行顶格写 #! 表示这个文件需要 parser 支持,execve(2) 会去读取第一行的内容(为了防止溢出,通常第一行只会去读 80 个字节),然后把第一个字符串作为第一参数,重新传给 execve(2) 执行,第一行的其他参数会完整填充到 argv[1] 中,然后命令行上传来的 argv 被重新映射到新的位置。此时,由于 argv 数组的体积增加,有可能本来没有超过参数数组上限的命令现在超过了。这个过程不会递归,即如果 parser 字符串给出的不是 ELF 格式,那就不会再找下去了。
例如以下文件(a.awk):
#!/usr/bin/awk -f { print $1 }
shell 执行:
./a.awk hello world
shell 调用:
execve("./a.awk", ["hello", "world", NULL], environ)
操作系统发现 a.awk 需要 parser 操作系统调用:
execve("/usr/bin/awk", ["-f", "./a.awk", "hello", "world", NULL], environ)
此时的实际效果就变成了:
/usr/bin/awk -f ./a.awk hello world
所谓的完整填充:
#!/some/elf/file -param1 -param2
执行./a.out xxx yyy 会变成
execve("/some/elf/file", ["-param1 -param2", "./a", "xxx", "yyy", NULL], environ)
即除了第一参数,后面所有的内容都是直接填进 argv[1] 的。
注意这个过程用户态可能可见(例如有些 shell 会手工完成这个过程而不是依靠操作系统),此外这个过程与扩展名无关,这与 DOS/Windows 不同(除非 shell 做了什么诡异的事),例如 sh a.awk 是完全合法的,如果 a.awk 内容其实是 shell 脚本。
当然,没有写 parser 头的文件也可以直接以 ELF 文件参数的形式启动,此时甚至不需要脚本文件自身有可执行权限,也不依赖 ${PATH}展开。 例如:
$ sh script.sh
2.可执行文件的定位
*nix 系统没有“内部命令”的概念,这与 DOS/Windows 不同。例如平时执行的 ls,实际上是 /bin/ls,或者其他部署在磁盘上的文件。存在所谓的内部命令,例如 cd 就在磁盘上找不到,但是这个内部命令并不是由操作系统执行的,而是 shell 自己内部处理的,因此可以认为 *nix 的内部命令就是 shell 的内建命令。
对于一个磁盘上的可执行文件,操作系统需要给出其绝对路径,或者相对路径,但是不能没有路径。例如以下情况都是合法的:
execve("/bin/ls", ["ls", NULL], environ) execve("../ls", ["ls", NULL], environ)
但是以下是非法的:
execve("ls", ["ls", NULL], environ)
不带路径的可执行文件需要由 shell 进行解析,解析结果会变成带路径的形式,操作系统才会接受,其依据一般为 ${PATH}环境变量。如果这个环境变量没有设置(不是为空),则 shell 通常会内部使用固定的值。这个值通常会包含 /bin 或者 /usr/bin 等常用 FHS 路径。对于大部分 shell 来说,这个值不一定在环境变量上得到体现,例如这样启动一个 bash:
env -i /bin/bash --norc
此时可以通过 env 工具看出环境变量中没有 ${PATH},但是 set 内建命令会给出实际生效的值。和 DOS/Windows 不同的是,DOS/Windows 没有内部变量,一切都是环境变量,并且不管 %PATH% 取值如何,当前路径(即 .)一定是在生效路径的首位。
为了方便这类情况(也是占了大多数),libc 提供了若干 exec(3) 家族函数,用于 execve(2) 的封装。例如 execlp(3) 或者 execvp(3)。
3.动态库的定位
这一节的内容,有兴趣的可以去看一看关于 /lib/ld-linux.so.2 或者 /lib64/ld-linux-x86-64.so.2 的相关知识,以及其配置文件 /etc/ld.so.conf 外加环境变量 LD_LIBRARY_PATH 以及 LD_PRELOAD 等。熟练使用 ldd(1)、readelf(1) 以及 ld-linux.so.2 会给你很多深入的理解。
==
LD_LIBRARY_PATH
- http://blog.chinaunix.net/uid-354915-id-3568853.html
- http://blog.csdn.net/wangeen/article/details/8159500
- http://tldp.org/HOWTO/Program-Library-HOWTO/shared-libraries.html
- http://www.ituring.com.cn/article/22101
- http://unix.stackexchange.com/questions/tagged/environment-variables?sort=votes&pagesize=50
LD_PRELOAD
- http://stackoverflow.com/questions/426230/what-is-the-ld-preload-trick
- http://hbprotoss.github.io/posts/li-yong-ld_preloadjin-xing-hook.html
- https://www.exploit-db.com/papers/13233/
- https://www.exploit-db.com/papers/37606/
$ cd ~/download_s/exploit-database-2015-10-28 $ ./searchsploit "ld_"
==
ldd
ldconfig
strace
/proc/$pid/maps
objdump
readelf
lsof
nm
strings
strip
pldd
pmap
==
参考链接
- 现代 *NIX 的进程与 shell
- http://tldp.org/HOWTO/Program-Library-HOWTO/shared-libraries.html
- 为什么说LD_LIBRARY_PATH不好
=EOF=
《 “Linux系统上的可执行文件/动态库” 》 有 12 条评论
在Linux系统上有什么办法可以限制LD_PRELOAD 和 LD_LIBRARY_PATH这两个变量的使用么?
http://security.stackexchange.com/questions/63599/is-there-any-way-to-block-ld-preload-and-ld-library-path-on-linux
`
这个问题本质上,是需要你能完全控制应用的执行环境。没有什么魔法,我暂时想到的一些解决方案有:
1. 将你关心的二进制文件添加 setuid/setgid 位。因为Linux正常情况下会阻止 setuid/setgid 程序的附加,所以请确认一下那个程序目前来说不属于root且设置了setuid位。
2. 你可以使用一个更安全的装载程序来运行你的应用程序而不是ld,这相当于是拒绝承认LD_PRELOAD这样的变量。但这可能会破坏某些现有的应用程序。
3. 在禁用LD_PRELOAD 和 dlsym机制的情况下用libc重新编译你的二进制程序。
4. 在沙盒环境中运行你的应用,以阻止该应用使用自定义环境变量直接启动其它进程。
选用什么解决方案具体取决于你需要运行什么程序,用户是谁,面临着哪些威胁。
记住,恶意用户只能修改自己的执行环境(除非他可以提权至root)。所以,用户通常不使用LD_PRELOAD变量进行代码注入,因为他们已经有可运行代码的相同权限了。攻击一般会出现在一下几个场景中:
1. 对于C/S模型的软件,在Client端突破一些安全相关的检查(欺骗通常出现在视频游戏中,或使客户端应用程序绕过一些分发服务器的检查);
2. 当你接管了用户的会话或进程后,你可以用于安装恶意软件(比如:他们忘了注销计算机且你有对设备的物理访问权限 或者 你利用精心构造的内容获取了他们的应用程序的权限时);
`
LD_PRELOAD的那些事
https://www.zzsec.org/2013/04/something-about-ld_preload/
动态库的搜索路径搜索的先后顺序是:
`编译目标代码时指定的动态库搜索路径
环境变量LD_PRELOAD指定的动态库
环境变量LD_LIBRARY_PATH指定的动态库搜索路径
配置文件/etc/ld.so.conf中指定的动态库搜索路径
默认的动态库搜索路径/lib
默认的动态库搜索路径/usr/lib
`
在上述1、3、4指定动态库搜索路径时,都可指定多个动态库搜索路径,其搜索的先后顺序是按指定路径的先后顺序搜索的。
LD_PRELOAD技术允许第一个载入动态链接库并且允许它轻易hook到不同的函数中。如果在这样的动态链接库中一个标准的库函数被覆写,那么这个库将会拦截所有的对那个函数的调用。
https://www.virusbtn.com/virusbulletin/archive/2014/07/vb201407-Mayhem
http://drops.wooyun.org/tips/2646
静态库和动态库的优缺点 #详细
http://chriszeng87.iteye.com/blog/1186094
Linux动态库相关知识整理 #详细
http://www.zkt.name/linuxgong-xiang-ku-de-chuang-jian-yu-shi-yong/
Linux静态库和动态库 #详细
http://www.cnblogs.com/feisky/archive/2010/03/09/1681996.html
技巧:Linux 动态库与静态库制作及使用详解 #比较系统
https://www.ibm.com/developerworks/cn/linux/l-cn-linklib/
Linux下C调用静态库和动态库 #不错
http://answerywj.com/2016/10/10/Linux%E4%B8%8BC%E8%B0%83%E7%94%A8%E9%9D%99%E6%80%81%E5%BA%93%E5%92%8C%E5%8A%A8%E6%80%81%E5%BA%93/
Linux下编译链接动态库 #不错
http://hbprotoss.github.io/posts/linuxxia-bian-yi-lian-jie-dong-tai-ku.html
Linux下的静态库、动态库和动态加载库
http://www.techug.com/linux-static-lib-dynamic-lib
Linux下动态库(.so)和静态库(.a)
http://blog.csdn.net/felixit0120/article/details/7652907
Linux系统中“动态库”和“静态库”那点事儿
http://blog.jobbole.com/107977/
静态库和动态库
http://leanote.com/blog/post/57907b2cab644133ed01bbf3
静态库与动态库的使用
https://www.gitbook.com/book/leon_lizi/-framework-/details
Linux下静态、动态库(隐式、显式调用)的创建和使用及区别
http://blog.csdn.net/star_xiong/article/details/17301191
ELF 文件格式分析
http://staff.ustc.edu.cn/~sycheng/ssat/exp_crack/ELF.pdf
ELF 文件格式入门教程
http://www.cirosantilli.com/elf-hello-world/
UNIX/LINUX 平台可执行文件格式分析
https://www.ibm.com/developerworks/cn/linux/l-excutff/
可执行文件(ELF)格式的理解
http://www.cnblogs.com/xmphoenix/archive/2011/10/23/2221879.html
在Linux下内核是如何让一个可执行文件运行起来的? # http://stackoverflow.com/a/31394861/895245
https://xinqiu.gitbooks.io/linux-insides-cn/content/SysCall/syscall-4.html
用户空间的程序启动过程
https://xinqiu.gitbooks.io/linux-insides-cn/content/Misc/program_startup.html
Linux中系统调用的概念 & 系统调用和库函数之间的关系
https://xinqiu.gitbooks.io/linux-insides-cn/content/SysCall/syscall-1.html
用strace进行问题定位
https://stackoverflow.com/questions/174942/how-should-strace-be-used
http://www.thegeekstuff.com/2011/11/strace-examples
https://linoxide.com/linux-command/linux-strace-command-examples/
`
$ strace ls
$ strace -e open cat /etc/passwd #只追踪特定的系统调用
$ strace -e trace=open,read ls /etc/ #同时追踪多个系统调用
$ sudo strace -p 1846 #对由pid指定的进程进行分析
$ sudo strace -o process_strace.log -p 1846 #保存输出至文件
$ strace -c ls #显示最终的统计结果
$ strace -t ls #显示时间戳
$ strace -tt ls #显示时间戳(微秒)
$ strace -r ls #显示相对时间
`
https://stackoverflow.com/questions/15339279/sys-execve-hooking-on-3-5-kernel
https://github.com/kfiros/execmon
https://stackoverflow.com/questions/8372912/hooking-sys-execve-on-linux-3-x
https://api.github.com/gists/ba7b8c934f858fadc28b
https://api.github.com/gists/f39b7366f9dd2f15479d
https://unix.stackexchange.com/questions/262098/hook-action-on-process-creation
https://my.oschina.net/macwe/blog/603583
https://unix.stackexchange.com/questions/84847/is-there-an-easy-way-to-log-all-commands-executed-including-command-line-argume
`
auditctl -a exit,always -S execve -F arch=b64
auditctl -a exit,always -S execve -F path=/usr/bin/rrdtool
`
理解与分析 ELF 二进制文件格式
https://linux-audit.com/elf-binaries-on-linux-understanding-and-analysis/
mandibule – linux elf 进程注入工具
https://github.com/ixty/mandibule
利用 PATH 环境变量进行 Linux 提权
http://www.hackingarticles.in/linux-privilege-escalation-using-path-variable/
多种语言(php/python/perl)将 ELF 文件在内存中加载执行的方法介绍
https://blog.fbkcs.ru/en/elf-in-memory-execution/
Linux上的程序是怎样运行的
https://mp.weixin.qq.com/s/5cMFr7fAUsBPEWNi9bqtvg
`
reader_loop
-> execute_command
–> execute_command_internal
—-> execute_simple_command
——> execute_disk_command
——–> shell_execve
众所周知,Linux的实现语言是c,shell也是其一个应用,也有自己的main函数。进入main函数后,在基本的初始化操作之后,最终进入reader_loop函数。reader_loop会调用execute_command来等待用户输入命令行参数,在用户输入参数之后,将调用execute_command_internal函数。execute_command_internal函数是shell源码中执行命令的实际操作函数。他需要对作为操作参数传入的具体命令结构的value成员进行分析,并针对不同的value类型,再调用具体类型的命令执行函数进行具体命令的解释执行工作。
具体来说:如果value是simple,则直接调用execute_simple_command函数进行执行,execute_simple_command再根据命令是内部命令或磁盘外部命令分别调用execute_builtin和execute_disk_command来执行,其中,execute_disk_command在执行外部命令的时候调用make_child函数fork子进程执行外部命令。
如果value是其他类型,则调用对应类型的函数进行分支控制。举例来说,如果是value是for_commmand,即这是一个for循环控制结构命令,则调用execute_for_command函数。在该函数中,将枚举每一个操作域中的元素,对其再次调用execute_command函数进行分析。即execute_for_command这一类函数实现的是一个命令的展开以及流程控制以及递归调用execute_command的功能。在上述整个调用流程串的最后一步是shell_execve。该函数最终会调用系统函数execve。
`