0%

两个ldd与动态链接器中的漏洞

我从 ByteCTF 的比赛 qq 群里得知中国科学技术大学的第七届信息安全大赛(Hackergame 2020)正在进行,于是昨天晚上吃完饭马上就开始干(我也只能搞搞新生赛了。。。),感觉题很有意思。这里写一写我做一道 binary 题的过程,以及后续更多的分析。

题目分析

上传文件后服务器会使用 ldd 命令行工具处理,并输出结果。不是传统的 Glibc pwn (其实后面发现也算是属于 “Glibc” pwn),那怎么办?先上传几个文件试试再说!

无修改 ELF

1
2
3
4
5
6
// gcc test.c -o test
#include <stdio.h>
int main(void)
{
puts("hhhh");
}

远程与本地使用的动态链接器和 libc 库相似(版本未知),一切正常。然后试一些奇怪的 ELF 。

添加动态库

仍使用上面的源文件,用 patchelf 工具添加 ELF 依赖的库。

1
2
gcc test.c -o test_2
patchelf --add-needed ./flag ./test_2

有趣的结果。远程的 ldd 版本为 ldd (Debian GLIBC 2.28-10) 2.28 ,下载后在本地测试:

1
2
3
4
5
6
7
8
9
10
$ cp ../test_2 .
$ ll
total 32
drwxrwxr-x 2 jkilopu jkilopu 4096 Nov 1 15:02 ./
drwxrwxr-x 5 jkilopu jkilopu 4096 Nov 1 14:52 ../
-rw-rw-r-- 1 jkilopu jkilopu 7 Nov 1 15:00 flag
-rwxrw-r-- 1 jkilopu jkilopu 5388 Nov 1 15:02 ldd*
-rwxrwxr-x 1 jkilopu jkilopu 10408 Nov 1 15:02 test_2*
$ ./ldd ./test_2
./test_2: error while loading shared libraries: ./flag: file too short

通过对比可以推测:我们上传的文件在远程“变”成了 a.out ,且 flag 文件与 a.out 在同一个目录 /dev/ 中。

那接下来呢?做过 Linux pwn 的话应该知道,一般来说我们都要 get shell ,再不济也要 orw。如何让 ldd 执行我们想要的命令呢?

Google!

当时我没有先去看 ldd 的源码,而是 google ldd execute arbitrary code - Google Search( google 的智能匹配加上了 “arbitary” ,说明 ldd 的漏洞还算出名)。

发现了两篇相关的 Blog 文章:ldd arbitrary code executionArbitrary Code Execution with ldd ,它们利用的是同一个漏洞,但利用方法有一些差异。建议先去读原文,这里非常简短的描述一下:

第一个漏洞

ldd 实际上是一个 shell script ,真正干活的是 linux 中的动态链接器(默认为 /lib/ld-linux.so.2 /lib64/ld-linux-x86-64.so.2 /libx32/ld-linux-x32.so.2)。在调用动态链接器前,ldd 设置了一个重要的环境变量 LD_TRACE_LOADED_OBJECTS=1 ,“让”动态链接器列出 ELF 依赖的库。因此,对于一般的情况, ldd ./test 可以简化为:

1
2
3
4
$ LD_TRACE_LOADED_OBJECTS=1 /lib64/ld-linux-x86-64.so.2 ./test
linux-vdso.so.1 => (0x00007ffd54797000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5f051f6000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5f055c0000)

有一般就有意外,这里必须要满足两点:

  1. 如果找不到“可用的”动态链接器,ldd 会尝试直接执行该文件以获得信息(这样就会调用 ELF 中 .interp 段对应的动态链接器)。对应下面这种情况:
1
2
3
4
5
6
7
8
9
10
$ LD_TRACE_LOADED_OBJECTS=1 ./test
linux-vdso.so.1 => (0x00007fff6cb92000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7be32ce000)
/lib64/ld-linux-x86-64.so.2 (0x00007f7be3698000)
$ readelf -l ./test
...
INTERP 0x0000000000000200 0x0000000000400200 0x0000000000400200
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
...
  1. 如果没有设置 LD_TRACE_LOADED_OBJECTS ,默认的动态链接器会直接执行该文件:
1
2
3
$ /lib64/ld-linux-x86-64.so.2 ./test
hhhh
$

两种利用

下面是非常简短的叙述:

  1. 第一篇博客:

    编译新的动态链接器,它除了不处理 LD_TRACE_LOADED_OBJECTS 环境变量以外,与默认的动态链接器的行为没什么不同,然后“让” ELF Executable 使用它作为动态链接器(可以用 patchelf --set-interpreter 或者编译时加上 -dynamic-linker 选项指定动态链接器)。 ldd 时会用该链接器执行 ELF ,达成任意代码执行 (arbitrary code execute )。

  2. 第二篇博客:

    写一个程序 fake_interp 并静态编译, 它会执行 execve("/lib64/ld-linux-x86-64.so.2", argv, NULL) (argv 的第二个元素是 ELF Executable 的名称),即调用真正的动态链接器,但不带 LD_TRACE_LOADED_OBJECTS 环境变量。然后将 fake_interp 设为 ELF Executable 的动态链接器。 ldd 时同样也能达成任意代码执行。

两种利用都触发了第一种意外: ldd 找不到“可用的”动态链接器,所以去调用 ELF Executable 的 .interp 指定的动态链接器。由于调用了“外部的”动态链接器,我们能做的事就很多了。“外部的”动态链接器可以与默认的差异很小(第一篇博客),也可以伪装成正常的动态链接器,输出正常的信息,再做一些 “evil things” 。在远程服务器上利用时,要上传“外部的”动态链接器和 ELF Executable ,确保两者共存且路径正确。

利用失败

想法很美好,我在本地和远程测试时都失败了。原因在 ldd 和默认的动态链接器上,要搞清楚必须要看与题目版本对应的 ldd 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
RTLDLIST="/lib/ld-linux.so.2 /lib64/ld-linux-x86-64.so.2 /libx32/ld-linux-x32.so.2"
...
for rtld in ${RTLDLIST}; do
if test -x $rtld; then
▸ dummy=`$rtld 2>&1`
if test $? = 127; then
▸ verify_out=`${rtld} --verify "$file"`
▸ ret=$?
case $ret in
▸ [02]) RTLD=${rtld}; break;;
esac
fi
fi
done
case $ret in
1)
# This can be a non-ELF binary or no binary at all.
nonelf "$file" || {
echo $"not a dynamic executable"
▸ result=1
}
;;
0|2)
try_trace "$RTLD" "$file" || result=1
;;
*)
echo 'ldd:' ${RTLD} $"exited with unknown exit code" "($ret)" >&2
exit 1
;;
esac
  1. 第一个坎:默认动态链接器

    前面提到了 ldd 会去找“可用的”的动态链接器,对应着verify_out=${rtld} --verify "$file"。下面就用默认的动态链接器测试一下已经修改了 .interp 段的 ELF Executable:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $ readelf -l test_3
    ...
    INTERP 0x0000000000000190 0x0000000000400190 0x0000000000400190
    0x000000000000000e 0x000000000000000e R 1
    [Requesting program interpreter: ./fake_interp]
    ...
    $ /lib64/ld-linux-x86-64.so.2 --verify ./test_3
    $ echo $?
    0

    不知道为什么,/lib64/ld-linux-x86-64.so.2 认为它能处理 test_3 。这就麻烦了,“外部的”动态链接器不会被调用,第一个意外不能被触发。

  2. 第二个坎:patched ldd

    第一个坎没有跨过,两种利用方法就都嗝屁了。而第二个坎从 ldd 源码的层面修复了漏洞,在这个 patch 中有一个 comment:

    Tomas Hoger 2011-02-02 14:19:20 UTC
    Created attachment 476574 [details]
    local-ldd.diff

    ldd patch from the Debian eglibc packages. It avoids this issue by ensuring that ldd always does:

    LD_TRACE_LOADED_OBJECTS=1 /lib/ld-linux.so.2 /path/to/ELF-lib-or-binary

    rather than:

    LD_TRACE_LOADED_OBJECTS=1 /path/to/ELF-lib-or-binary

    This change is not part of upstream glibc. I don’t seem to be able to find any reference to this change on sourceware.org, so I’m not sure if it was ever proposed upstream. Does it this change break any expected use to explain why this fix should not make it upstream?

    对比上面的 ldd 源码,发现题目中的 ldd 确实解决了第一个意外。 ldd 永远不会执行 LD_TRACE_LOADED_OBJECTS=1 /path/to/ELF-lib-or-binary

    这就等于说,该漏洞不存在于题目中的 ldd 和默认动态链接器。放弃。

    小结

    我花了好一段时间才搞明白为什么利用会失败。我忽视了博客的时效性,再加上两个博客中并没有提及 ldd 和默认动态链接器的版本,我拿着已经修复了漏洞的 ldd 和 verify 总是能成功的默认动态链接器搞,当然会失败。

    其实 ldd 的 man page 中已经此漏洞进行了警告:

    Security

    In  the  usual  case, ldd invokes the standard dynamic linker (see ld.so(8)) with the LD_TRACE_LOADED_OBJECTS environment variable set to 1, which causes the linker to display the library dependencies.  Be aware, however, that in some circumstances,  some  versions  of  ldd  may attempt to obtain the dependency information by directly executing the program.  Thus, you should never employ ldd on an untrusted executable, since this may result in the execution of arbitrary code.  A safer alternative when  dealing  with untrusted executables is:

    据此,论坛上的一些人不认为这是 ldd 的安全问题: ldd 本就不应处理不被信任的 ELF 。

    这条路走不通了,继续 google 吧。。。

CVE-2019-1010023

ldd execute arbitrary code - Google Search 下已经找不到有用的信息了。我想了想应该把版本也加入搜索关键词中,于是 google ldd vulnerability 2.28 - Google Search ,缩小了版本的搜索范围,扩大了漏洞利用手法的搜索范围,只要有 vulnerability 就行。

看起来最顶端条目的相关性很大,点进去看看。

琳琅满目的 cve ,但我以前从未接触过。找到唯一一条摘要中有 “ldd” 的条目:CVE-2019-1010023 , 也是一个任意代码执行漏洞,点击 Reference 中的 https://sourceware.org/bugzilla/show_bug.cgi?id=22851

然后看作者发的贴子,里面有 PoC (Proof of Concept,前几天才知道这个词),发现能完美解决题目,然后大致浏览一遍评论,最后复制、粘贴(源码)、复制、粘贴(shellcode)、复制、粘贴(makefile),改一下 shellcode (从 cat /etc/passwd 改为 cat ./flag),编译,上传,成功拿到 flag。。。整个过程十分流畅,除了装了个什么 nasm ,从茫然无措到拿到二血,只有二十分钟的距离。。。

那这个漏洞到底是什么?作者又是怎么利用的?要搞明白可不止二十分钟了。。。

分析PoC