前言
好累啊,这几天都是比赛,也没学到多少
不过vn的wp写都写了,干脆水一篇博客吧
拼尽全力写出两题简单的,我好菜我好菜我好菜我好菜我好菜我好菜……..
Login
APK逻辑
一开始接受一个key,调用native函数setkey设置
username和passwd组成payload和一个字符传入native函数encryptd反回enc
enc+payload传入native函数sign生成签名
然后全部发给服务端
坑
android:extractNativeLibs=”false”要改为true才能正常调试
反调试
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
__int64 sub_775313A7F8()
{
__int64 result; // x0
pthread_t newthread_[2]; // [xsp+0h] [xbp-10h] BYREF
newthread_[1] = *(_ReadStatusReg(TPIDR_EL0) + 40);
if ( pthread_create(newthread_, 0, sub_775313BFAC, &arg_)
|| pthread_create(newthread_, 0, sub_775313C060, &arg__0)
|| (result = pthread_create(newthread_, 0, sub_775313C114, &arg__1), result) )
{
perror("Failed to create thread");
_ReadStatusReg(TPIDR_EL0);
exit(1);
}
_ReadStatusReg(TPIDR_EL0);
return result;
}
void __fastcall __noreturn sub_775313C114(__int64 a1)
{
FILE *stream; // [xsp+8h] [xbp-248h]
_BYTE v2[512]; // [xsp+10h] [xbp-240h] BYREF
__int64 v3; // [xsp+210h] [xbp-40h]
__int64 v4; // [xsp+218h] [xbp-38h]
__int64 n512_2; // [xsp+220h] [xbp-30h]
FILE *stream_1; // [xsp+228h] [xbp-28h]
int n512_1; // [xsp+234h] [xbp-1Ch]
__int64 n512; // [xsp+238h] [xbp-18h]
_BYTE *v9; // [xsp+240h] [xbp-10h]
__int64 s_chk; // [xsp+248h] [xbp-8h]
v4 = a1;
v3 = a1;
while ( 1 )
{
stream = fopen("/proc/self/maps", "r");
if ( stream )
{
do
{
v9 = v2;
n512 = 512;
n512_1 = 512;
stream_1 = stream;
n512_2 = 512;
s_chk = __fgets_chk(v2, 512, stream, 512);
}
while ( s_chk
&& !sub_775313BEB0(v2, "frida")
&& !sub_775313BEB0(v2, "gadget")
&& !sub_775313BEB0(v2, "/data/local/tmp")
&& !sub_775313BEB0(v2, "frida-agent") );
fclose(stream);
}
sleep(1u);
}
}
会创建两个线程去一直close ida和firda的服务端,只要换个端口就行
sub_775313C114函数会查map里的特征,换个运行目录+使用去特征的frida客户端即可
so分析
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
jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
__int64 v3; // [xsp+8h] [xbp-68h]
jint n65542; // [xsp+24h] [xbp-4Ch]
__int64 v5; // [xsp+28h] [xbp-48h] BYREF
_OWORD v6[3]; // [xsp+30h] [xbp-40h] BYREF
__int64 v7; // [xsp+68h] [xbp-8h]
v7 = *(_ReadStatusReg(TPIDR_EL0) + 40);
v5 = 0;
if ( sub_775313A6F0(vm, &v5, 65542) )
{
n65542 = -1;
}
else
{
__android_log_print(4, "main", "JNI_OnLoad");
v3 = sub_7753139C68(v5, "com/britney/login/util/NativeBridge");
if ( v3 )
{
v6[2] = *&off_7753170E28; // "(Ljava/lang/String;Ljava/lang/String;Landroid/content/Context;)Ljava/lang/String;"
v6[1] = *&off_7753170E18;
v6[0] = *off_7753170E08; // "encrypt"
if ( sub_775313A72C(v5, v3, v6, 2) )
n65542 = -1;
else
n65542 = 65542;
}
else
{
n65542 = -1;
}
}
_ReadStatusReg(TPIDR_EL0);
return n65542;
}
这里动态注册了两个函数,一个是encrypt,一个是sign
encryped分析
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
__int64 __fastcall encrypt(JNIEnv *a1, jclass a2, __int64 a3, __int64 a4)
{
unsigned int v5; // [xsp+68h] [xbp-3B8h]
const char *v6; // [xsp+78h] [xbp-3A8h]
_OWORD android_id[22]; // [xsp+A0h] [xbp-380h] BYREF
_BYTE v11[256]; // [xsp+200h] [xbp-220h] BYREF
_BYTE v12[256]; // [xsp+300h] [xbp-120h] BYREF
_QWORD v13[2]; // [xsp+400h] [xbp-20h] BYREF
int v14; // [xsp+410h] [xbp-10h]
__int64 v15; // [xsp+418h] [xbp-8h]
v15 = *(_ReadStatusReg(TPIDR_EL0) + 40);
v6 = sub_7753139F50(a1, a3, 0);
v14 = 0;
v13[1] = 0;
v13[0] = 0;
sub_7753139B5C(a1, a4, v13);
combine_str(v12, 256, 256, "%s:%s", v6, v13);
sub_775313A274(a1, a3, v6);
v5 = sub_775313A2B0(v12, 0x100u);
memset(v11, 0, sizeof(v11));
sub_775313B4D0(&unk_77531761A0, 16, v12);
memset(android_id, 0, 350);
sub_775313B728(v11, v5);
return sub_7753139E20(a1, android_id);
}
换了sbox的aes,丢ai就行
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
import struct
import base64
# --- Custom Constants Extracted from IDA ---
# The Custom S-Box dumped from memory (0x70A9D67040)
SBOX = [
0x20, 0x7b, 0x18, 0xa7, 0x42, 0x44, 0xd7, 0x4a, 0xcd, 0x32, 0xd1, 0xec, 0xf3, 0x81, 0xa5, 0x89,
0x0e, 0x91, 0x4b, 0xf0, 0xe9, 0x5d, 0x8d, 0xf5, 0x46, 0xfc, 0x31, 0x36, 0xb6, 0xac, 0x9b, 0xb9,
0x26, 0x09, 0xe6, 0x40, 0xd4, 0xb0, 0x51, 0x4f, 0x9c, 0x3e, 0xe7, 0x79, 0x30, 0x88, 0xb1, 0x3c,
0x7a, 0x5c, 0xd3, 0x14, 0x5a, 0xab, 0x56, 0xc0, 0x04, 0x29, 0xd0, 0x3b, 0x1f, 0xf9, 0xa3, 0x57,
0x00, 0x8a, 0x84, 0x16, 0xf4, 0x1a, 0xea, 0x64, 0xa6, 0xd6, 0x2e, 0xbe, 0x2f, 0x17, 0xc4, 0xe0,
0x1e, 0x02, 0x3a, 0x22, 0x8f, 0x9f, 0xcb, 0xa8, 0x2c, 0x67, 0x34, 0x25, 0xd5, 0xff, 0xef, 0xf6,
0xe2, 0xaa, 0xd9, 0x72, 0xfe, 0xce, 0xa1, 0x78, 0x85, 0x96, 0x2a, 0x77, 0xca, 0xc1, 0x37, 0x74,
0xa2, 0x5e, 0x6c, 0xfd, 0xb8, 0x4d, 0x7d, 0x70, 0xb3, 0xdd, 0xcf, 0x71, 0x73, 0x61, 0xf8, 0x19,
0x48, 0xe3, 0x63, 0x33, 0x3d, 0x15, 0xae, 0x98, 0xe5, 0x80, 0xbd, 0xbc, 0x82, 0xc6, 0x94, 0x01,
0xe4, 0xde, 0x06, 0x50, 0x95, 0xdf, 0x47, 0xf7, 0x90, 0x8b, 0x45, 0x9a, 0x6e, 0x07, 0xad, 0x1c,
0x35, 0x83, 0x68, 0x03, 0x6f, 0x5b, 0xb7, 0xfb, 0x1d, 0xc5, 0x10, 0x7c, 0xd8, 0x6a, 0xcc, 0x69,
0x8e, 0x24, 0x4c, 0x39, 0xb4, 0xa0, 0x0b, 0x52, 0xe8, 0xa9, 0xb2, 0x8c, 0x0a, 0xbf, 0x28, 0x86,
0x6d, 0xaf, 0xda, 0x41, 0xfa, 0x75, 0xb5, 0x43, 0xc3, 0x60, 0x62, 0x2b, 0x55, 0xf2, 0x9e, 0x2d,
0x12, 0x23, 0x0d, 0xdb, 0x6b, 0xc7, 0x38, 0x7f, 0x5f, 0x97, 0x08, 0xed, 0xe1, 0xbb, 0xee, 0x9d,
0xd2, 0x92, 0x49, 0x3f, 0xdc, 0x58, 0x87, 0xc2, 0xba, 0x99, 0xc9, 0x4e, 0xf1, 0x21, 0xeb, 0x13,
0x65, 0x59, 0x76, 0x0c, 0xc8, 0x05, 0xa4, 0x54, 0x93, 0x1b, 0x66, 0x11, 0x27, 0x53, 0x7e, 0x0f
]
# Generate Inverse S-Box
INV_SBOX = [0] * 256
for i in range(256):
INV_SBOX[SBOX[i]] = i
# Rcon
RCON = [0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]
# --- AES Helper Functions ---
def xtime(a):
return (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)
def multiply(x, y):
a = x
b = y
p = 0
for i in range(8):
if (b & 1):
p ^= a
hi_bit_set = (a & 0x80)
a = (a << 1) & 0xFF
if hi_bit_set:
a ^= 0x1B
b >>= 1
return p
def mix_single_column(a):
t = a[0] ^ a[1] ^ a[2] ^ a[3]
u = a[0]
a[0] ^= t ^ xtime(a[0] ^ a[1])
a[1] ^= t ^ xtime(a[1] ^ a[2])
a[2] ^= t ^ xtime(a[2] ^ a[3])
a[3] ^= t ^ xtime(a[3] ^ u)
def inv_mix_single_column(a):
u = xtime(xtime(a[0] ^ a[2]))
v = xtime(xtime(a[1] ^ a[3]))
a[0] ^= u
a[1] ^= v
a[2] ^= u
a[3] ^= v
mix_single_column(a)
def key_expansion(key):
# key: 16 bytes
nb = 4
nk = 4
nr = 10
# Convert key to words
w = []
for i in range(nk):
w.append([key[4*i], key[4*i+1], key[4*i+2], key[4*i+3]])
for i in range(nk, nb * (nr + 1)):
temp = w[i-1][:]
if i % nk == 0:
# RotWord
temp = temp[1:] + temp[:1]
# SubWord (Use CUSTOM SBox)
temp = [SBOX[b] for b in temp]
# Xor Rcon
temp[0] ^= RCON[i // nk]
w.append([w[i-nk][0]^temp[0], w[i-nk][1]^temp[1], w[i-nk][2]^temp[2], w[i-nk][3]^temp[3]])
return w
def add_round_key(state, round_key):
for c in range(4):
for r in range(4):
# *** CUSTOM MODIFICATION: XOR 0x91 ***
state[r][c] ^= (round_key[c][r] ^ 0x91)
def inv_shift_rows(s):
# Standard InvShiftRows
s[1][0], s[1][1], s[1][2], s[1][3] = s[1][3], s[1][0], s[1][1], s[1][2]
s[2][0], s[2][1], s[2][2], s[2][3] = s[2][2], s[2][3], s[2][0], s[2][1]
s[3][0], s[3][1], s[3][2], s[3][3] = s[3][1], s[3][2], s[3][3], s[3][0]
def inv_sub_bytes(s):
for r in range(4):
for c in range(4):
s[r][c] = INV_SBOX[s[r][c]]
def decrypt_block(ciphertext, expanded_key):
# ciphertext: 16 bytes
# expanded_key: list of words
# State is column-major: state[row][col]
state = [[0]*4 for _ in range(4)]
for c in range(4):
for r in range(4):
state[r][c] = ciphertext[r + 4*c]
nr = 10
# Initial Round (AddRoundKey with last round key)
# The last round key is at index Nr*4
rk = expanded_key[nr*4 : (nr+1)*4]
add_round_key(state, rk)
for round in range(nr - 1, 0, -1):
inv_shift_rows(state)
inv_sub_bytes(state)
# AddRoundKey
rk = expanded_key[round*4 : (round+1)*4]
add_round_key(state, rk)
# InvMixColumns
for c in range(4):
col = [state[r][c] for r in range(4)]
inv_mix_single_column(col)
for r in range(4):
state[r][c] = col[r]
inv_shift_rows(state)
inv_sub_bytes(state)
rk = expanded_key[0:4]
add_round_key(state, rk)
# Output to bytes
out = []
for c in range(4):
for r in range(4):
out.append(state[r][c])
return bytes(out)
def solve():
print("[*] Starting Custom AES Decryption...")
# Key
key_str = "g3Jb3tu4njIVK0Q9"
print(f"[*] Using Key: {key_str}")
key_bytes = [ord(c) for c in key_str]
# Ciphertext found in pcap
custom_table = "RSTUVWLbcdefghiMNOPrstuvQXYZajCklmnEFGHIJKwxyz01ABD234opq56789+/"
std_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
cipher_b64 = "vzz4ieKilkvCy/T2vMoTZTgViYCFAvZ/s2JGrV+Emfl="
trans_table = str.maketrans(custom_table, std_table)
std_b64 = cipher_b64.translate(trans_table)
# Pad base64
if len(std_b64) % 4 != 0:
std_b64 += '=' * (4 - len(std_b64) % 4)
try:
encrypted_bytes = base64.b64decode(std_b64)
print(f"[+] Encrypted Bytes (Hex): {encrypted_bytes.hex()}")
# 2. Key Expansion
w = key_expansion(key_bytes)
# 3. Decrypt
#print(encrypted_bytes)
#encrypted_bytes = b'^\xdbu8\xaaN\x81\xf5\xde\xb3\xf0\xb3\\\xfd\x82l#\x049\xa7\xa4\xc1v\xffS:%LO\xa3\x84\xb8'
decrypted_data = b""
for i in range(0, len(encrypted_bytes), 16):
block = encrypted_bytes[i:i+16]
#print(block)
if len(block) == 16:
decrypted_data += decrypt_block(block, w)
print(f" Decrypted Raw (Hex): {decrypted_data.hex()}")
# 4. Remove Padding (PKCS7)
try:
pad_len = decrypted_data[-1]
if pad_len > 0 and pad_len <= 16:
plain_bytes = decrypted_data[:-pad_len]
try:
plain_text = plain_bytes.decode('utf-8')
# Force print
print(f" [+] SUCCESS! Plaintext: {plain_text}")
except:
print(f" [-] Decode UTF-8 error. Raw: {plain_bytes}")
else:
# Check if zero padding? Or no padding?
try:
plain_text = decrypted_data.split(b'\0')[0].decode('utf-8')
print(f" [?] Possible Plaintext (Zero Padding): {plain_text}")
except:
print(f" [-] Invalid padding length: {pad_len}")
except Exception as e:
print(f" [-] Decode error: {e}")
except Exception as e:
print(f"[-] Base64 decode failed: {e}")
if __name__ == "__main__":
solve()
VNCTF2026:Vv&nN_W3lC0me!!:b2e90a5f379ea4db
VNCTF2026是账号
Vv&nN_W3lC0me!!密码
sub_7753139B5C函数会生成一串硬件唯一的hid,由combine_str进行拼接
如果只提交账号密码,会显示请在同一台设备上登入,所以到时候要手动替换hid
sign函数
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
__int64 __fastcall sign(JNIEnv *a1, jclass a2, __int64 a3, __int64 a4, __int64 a5)
{
__int64 v6; // [xsp+18h] [xbp-1C8h]
int n15; // [xsp+40h] [xbp-1A0h]
int v8; // [xsp+44h] [xbp-19Ch]
const char *v9; // [xsp+48h] [xbp-198h]
const char *v10; // [xsp+50h] [xbp-190h]
_OWORD android_id[2]; // [xsp+80h] [xbp-160h] BYREF
char v15; // [xsp+A0h] [xbp-140h]
_BYTE v16[16]; // [xsp+B0h] [xbp-130h] BYREF
_BYTE v17[256]; // [xsp+C0h] [xbp-120h] BYREF
_QWORD v18[2]; // [xsp+1C0h] [xbp-20h] BYREF
int v19; // [xsp+1D0h] [xbp-10h]
__int64 v20; // [xsp+1D8h] [xbp-8h]
v20 = *(_ReadStatusReg(TPIDR_EL0) + 40);
v10 = sub_7753139F50(a1, a3, 0);
v9 = sub_7753139F50(a1, a4, 0);
v19 = 0;
v18[1] = 0;
v18[0] = 0;
sub_7753139B5C(a1, a5, v18);
v8 = combine_str(v17, 256, 256, "VNCTF:%s:%s:%s:%d:%d:%d", v10, v18, v9, arg_ & 1, arg__0 & 1, arg__1 & 1);
sub_775313BA48(v17, v8, v16);
v15 = 0;
memset(android_id, 0, sizeof(android_id));
for ( n15 = 0; n15 <= 15; ++n15 )
{
*(android_id + 2 * n15) = a0123456789abcd[v16[n15] >> 4];
*(android_id + ((2 * n15) | 1)) = a0123456789abcd[v16[n15] & 0xF];
}
v6 = sub_7753139E20(a1, android_id);
_ReadStatusReg(TPIDR_EL0);
return v6;
}
动态调试发现,sign也会把hid组到里面去,然后生成签名
我们提交正确的账号密码,在combine_str函数下断点,把合并后的字符串hid部分替换成b2e90a5f379ea4db即可,服务端会返回flag
VNCTF{e2_7RA1Fic_log1n_lacnJ61z}
ez_maze
脱壳
x64dbg 跑到一堆pop的地方,然后下面有个jmp,跳转过去就是真实oep
用scylla去dump
分析
通过messagebox函数定位到sub_7FF650B0192函数,发现是主逻辑
动态调试的时候去dump一下迷宫的数据,然后丢ai
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
# # #
######### # ### # #
# # # #
######## # ### ### #
# # # # # #
## # ### # # ### ###
# # # # #
# ### ##### ##### #
# # # # # # #
# # ##### # # # # #
# # # # # # #
####### ##### # # #
# # #
##### ########### #
# # # #
## # ### ####### # #
# # # # # # #
# # # ### ### # # #
# # # #
##################
class MSVCRand:
def __init__(self, seed=1):
self.holdrand = seed
def srand(self, seed):
self.holdrand = seed
def rand(self):
self.holdrand = (self.holdrand * 214013 + 2531011) & 0xFFFFFFFF
return (self.holdrand >> 16) & 0x7FFF
def solve():
width, height = 20, 20
maze = [1] * (width * height)
maze[0] = 0
rnd = MSVCRand()
rnd.srand(100) # 关键种子
stack_x, stack_y = [0]*400, [0]*400
stack_count = 1
# Directions: 0:(+2,0), 1:(-2,0), 2:(0,+2), 3:(0,-2)
dx_list = [2, -2, 0, 0]
dy_list = [0, 0, 2, -2]
while stack_count > 0:
curr_idx = stack_count - 1
cx, cy = stack_x[curr_idx], stack_y[curr_idx]
candidates = []
for i in range(4):
nx, ny = cx + dx_list[i], cy + dy_list[i]
if 0 <= nx < 20 and 0 <= ny < 20:
if maze[nx + 20 * ny] == 1:
candidates.append(i)
if candidates:
pick = candidates[rnd.rand() % len(candidates)]
nx, ny = cx + dx_list[pick], cy + dy_list[pick]
mx, my = cx + dx_list[pick]//2, cy + dy_list[pick]//2
maze[nx + 20 * ny] = 0
maze[mx + 20 * my] = 0
stack_x[stack_count], stack_y[stack_count] = nx, ny
stack_count += 1
else:
stack_count -= 1
# 后处理逻辑
maze[399] = 0 # (19,19)
if maze[379] == 1: maze[398] = 0
else: maze[379] = 0
return maze
def bfs(maze):
q = [(0, 0, "")]
visited = {(0,0)}
while q:
x, y, path = q.pop(0)
if x == 19 and y == 19: return path
# a:x+1, d:x-1, w:y+1, s:y-1
moves = [('a', 1, 0), ('d', -1, 0), ('w', 0, 1), ('s', 0, -1)]
for char, dx, dy in moves:
nx, ny = x + dx, y + dy
if 0 <= nx < 20 and 0 <= ny < 20 and maze[nx + 20*ny] == 0:
if (nx, ny) not in visited:
visited.add((nx, ny))
q.append((nx, ny, path + char))
print(bfs(solve()))
wwaaaaaaaawwwwddwwddwwaaaawwaaaaaassssaawwwwaawwwwwwwa