Linux系统上的可执行文件/动态库


=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
LD_PRELOAD
$ cd ~/download_s/exploit-database-2015-10-28
$ ./searchsploit "ld_"

==

ldd
ldconfig
strace
/proc/$pid/maps
objdump
readelf
lsof
nm
strings
strip
pldd
pmap

==

参考链接

=EOF=


《“Linux系统上的可执行文件/动态库”》 有 12 条评论

  1. 在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. 当你接管了用户的会话或进程后,你可以用于安装恶意软件(比如:他们忘了注销计算机且你有对设备的物理访问权限 或者 你利用精心构造的内容获取了他们的应用程序的权限时);
    `

  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指定动态库搜索路径时,都可指定多个动态库搜索路径,其搜索的先后顺序是按指定路径的先后顺序搜索的。

  3. 静态库和动态库的优缺点 #详细
    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

  4. 用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 #显示相对时间
    `

  5. 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。
    `

回复 hi 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注