ciscn2024初赛部分题目复现
misc
Power_Trajectory_Diagram
测信道攻击,参考文章:https://zhuanlan.zhihu.com/p/157585244
python代码读取文件数据:
import numpy as np
import pandas as pd
path = r"attachment.npz"
data = np.load(path)
index = data['index']
inputs = data['input']
outputs = data['output']
traces = data['trace']
print("Index shape:", index.shape)
print("Inputs shape:", inputs.shape)
print("Traces shape:", traces.shape)
print(index)
print(inputs)
print(outputs)
print(traces)
读取到的数据如下:
Index shape: (520,)
Inputs shape: (520,)
Traces shape: (520, 5000)
index
[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
3 3 ... 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11
11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12
12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12
12 12 12 12 12 12 12 12 12 12 12 12 12 12]
inputs
['a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r'
's' 't' 'u' 'v' 'w' 'x' 'y' 'z' '0' '1' '2' '3' '4' '5' '6' '7' '8' '9'
'_' '!' '@' '#' 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n'
'o' 'p' 'q' 'r' 's' 't' 'u' 'v' 'w' 'x' 'y' 'z' '0' '1' '2' '3' '4' '5'
'6' '7' '8' '9' '_' '!' '@' '#' 'a' 'b' ... '!' '@' '#' 'a' 'b' 'c' 'd'
'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 'u' 'v'
'w' 'x' 'y' 'z' '0' '1' '2' '3' '4' '5' '6' '7' '8' '9' '_' '!' '@' '#'
'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r'
's' 't' 'u' 'v' 'w' 'x' 'y' 'z' '0' '1' '2' '3' '4' '5' '6' '7' '8' '9'
'_' '!' '@' '#']
outputs
[]
traces
[[ 0.11914062 -0.0390625 -0.04101562 ... -0.03613281 -0.01464844
-0.06640625]
[ 0.11425781 -0.04492188 -0.04101562 ... -0.03808594 -0.01367188
-0.06640625]
[ 0.125 -0.04296875 -0.04101562 ... -0.04101562 -0.01757812
-0.06933594]
...
[ 0.11816406 -0.05175781 -0.04296875 ... -0.09082031 -0.10449219
-0.13476562]
[ 0.12207031 -0.046875 -0.04394531 ... -0.10351562 -0.10449219
-0.1328125 ]
[ 0.12304688 -0.0390625 -0.04296875 ... -0.09082031 -0.10058594
-0.12792969]]
同一个index有40个字符,每个index包含的字符都相同,猜测一个index中可以得到flag的一个字符。数据存入excel,画出功率图的图像,找不同,也就是找有突出的点。
发现前面的突出固定为#,后面跟着的突出会发生改变,手动收集数据,依次为_ciscn_2024_,index为12对应的功率中没有除#以外突出的字符,忽略不计,加上格式得到flag{_ciscn_2024_}
。
reverse
asm_re
题目给出了一个txt文件,里面是用ida反编译过的汇编代码以及十六进制,ARM架构小端存储。
放进chatgpt反汇编,写出C++代码,以下为大致反汇编结果
#include <iostream>
#include <cstring>
#include <cstdio>
extern char aFlagXxxxxxxxxx[]; // flag
extern char unk_100003F10[]; // 对比数据
char __result_array[1024]; // 结果数组
int main() {
char stack_buffer[256];
// 栈溢出保护检查(不涉及主要逻辑,省略)
strcpy(stack_buffer + 192, aFlagXxxxxxxxxx);
strcpy(stack_buffer + 256, unk_100003F10);
memcpy(__result_array, stack_buffer + 192, 152);
int len = strlen(stack_buffer + 192);
int index = 0;
int temp = 0;
while (index < len) {
char c = stack_buffer[192 + index];
temp = (c * 80 + 20) ^ 'M';
temp += 30;
__result_array[index] = temp;
index++;
}
bool result_matches = true;
for (int i = 0; i < len; ++i) {
if (__result_array[i] != stack_buffer[256 + i]) {
result_matches = false;
break;
}
}
if (result_matches) {
printf("The result array matches the expected array.\n");
} else {
printf("The result array does not match the expected array.\n");
}
return 0;
}
有三个字符数组变量,分别用于存储flag、对比数据(猜测为正确flag经过处理后的数据串)和结果数组(传入的flag经过处理后的数据串)。使用strcpy函数将flag和对比数据复制到栈空间的特定位置,flag在前(192以后),对比数据在后(256以后),使用memcpy将输入的flag复制到结果数组中。循环遍历输入的flag中的每个字符,对每个字符执行一系列计算操作,(c * 80 + 20) ^ 'M'+30,然后将计算结果存储在结果数组中。循环结束后,程序通过另一个循环比较结果数组和未知数据数组,检查它们是否匹配。根据比较结果,判断结果数组是否与对比数据相同。
现在已知对比数据为unk_100003F10中的数据,手动将这部分代码删除多余的注释,放到txt文件中用代码进行数据提取,遍历ascii所有可打印字符串的范围,暴力破解flag。
exp如下:
import re
path = r"asmre_data.txt"
# 从文件中读取数据
with open(path, 'r') as file:
data = file.read()
# 使用正则表达式提取DCB后面的十六进制值或0
pattern = re.compile(r'DCB\s+(?:0x([0-9A-Fa-f]+)|(\d+))')
matches = pattern.findall(data)
hex_values = []
for hex_val, dec_val in matches:
if hex_val: # 如果匹配到十六进制值
hex_values.append(int(hex_val, 16))
elif dec_val: # 如果匹配到0
hex_values.append(int(dec_val, 10))
# 小端存储
combined_hex_values = []
for i in range(0, len(hex_values), 4):
combined_value = 0
for j in range(4):
if i + j < len(hex_values):
combined_value |= hex_values[i + j] << (j * 8)
combined_hex_values.append(combined_value)
string_array = [f'0x{value:08X}' for value in combined_hex_values]
print("Result Array:")
print(string_array)
hex_array = [int(s, 16) for s in string_array]
result_string = ""
for i in range(len(hex_array)):
for j in range(33, 128):
tmp = (((j * 0x50) + 0x14) ^ 0x4d) + 0x1e
if tmp == hex_array[i]:
result_string += chr(j)
break
print(result_string)
# flag{67e9a228e45b622c2992fb5174a4f5f5}
whereThel1b
给出的代码中调用了so文件中的whereistheflag和trytry函数。
so文件放入ida反编译,找到这两个函数。
由于ida反汇编后太过复杂,放入chatgpt重写成更容易理解的代码:
def whereistheflag(pla):
encoded_pla = base64.b64encode(pla)
encoded_list = list(encoded_pla)
result_list = []
length = len(encoded_pla)
for i in range(length):
random_int = random.randint(0, length - 1)
add_result = encoded_list[i] + random_int
result_list.append(add_result)
print(result_list)
return result_list
def whereistheflag1(pla):
encoded_pla = base64.b64encode(pla)
result_list = []
length = len(encoded_pla)
for _ in range(length):
random_int = random.randint(0, length - 1)
xor_result = encoded_pla[random_int] ^ random_int
result_list.append(xor_result)
return result_list
def trytry(pla):
random.seed(0)
result = whereistheflag1(pla)
return result
根据以上大致思路的代码分析so文件的这三个函数,因为ida中反编译的代码太过复杂,选择直接调用so文件中的函数进行爆破。给出的代码中只使用了trytry函数的返回值,因此只需要调用trytry。
给出的密文encry长度56字节,因为代码中使用了base64编码,而后一系列操作的处理并没有改变base64编码后的flag的长度,Base64编码过程是将3个字节的数据转换为4个Base64字符,猜测原flag长度为(56/4)*3,也就是42字节,因此使用所有可见字符进行三字节爆破。
exp如下:
import whereThel1b
encry = [108, 117, 72, 80, 64, 49, 99, 19, 69, 115, 94, 93, 94, 115, 71, 95, 84, 89, 56, 101, 70, 2, 84, 75, 127,
68, 103, 85, 105, 113, 80, 103, 95, 67, 81, 7, 113, 70, 47, 73, 92, 124, 93, 120, 104, 108, 106, 17, 80,
102, 101, 75, 93, 68, 121, 26]
flag = ''
for i in range(14):
for a in range(32, 126):
for b in range(32, 126):
for c in range(32, 126):
abc = chr(a) + chr(b) + chr(c)
flag_test = (flag + abc).ljust(42, '1')
result = whereThel1b.trytry(flag_test.encode())
position = i * 4
ret_substring = result[position:position + 4]
encry_substring = encry[position:position + 4]
if ret_substring == encry_substring:
flag += abc
print(flag)
# flag{7f9a2d3c-07de-11ef-be5e-cf1e88674c0b}
发现提示python版本不对,重新下载python3.10运行代码,得到flag。
gdb_debug
64位ida反编译,分析代码发现加密步骤一共包括三个异或和一个置换,最终运算结果是s2。
异或
置换
异或
异或
将主要加密部分使用chatgpt写成更容易理解的python形式如下:
def encrypt_string(s):
v17 = []
for i in range(len(s)):
v17.append(ord(s[i]) ^ rand_1[i])
ptr = list(range(len(s)))
for k in range(len(s) - 1, 0, -1):
v18 = rand_2[len(s)-k-1]
ptr[k], ptr[v18] = ptr[v18], ptr[k]
v31 = []
for m in range(len(s)):
v31.append(v17[ptr[m]])
for n in range(len(s)):
v31[n] = rand_3[n] ^ v31[n]
for n in range(len(s)):
v31[n] = asc_55AE330010A0[n] ^ v31[n]
return v31
ida配合kali远程动态调试,扒下来随机数。
第三个异或产生的随机数无法直接得到,只能使用操作前后的数值再次异或得到,其他部分的随机数都有变量存储,可以直接得到数值。
异或前:
异或后:
获得该部分随机数的代码如下:
hex_pairs = [
"06^d8", "4a^e0", "5b^19", "14^e8", "c4^cd", "77^9f", "df^6d", "63^65",
"b5^b8", "82^11", "e0^81", "3c^c8", "4a^6e", "99^d0", "ce^db", "f9^f8",
"bc^6b", "52^f9", "79^7d", "ca^d2", "19^d6", "3c^d5", "da^0f", "1f^89",
"2d^1e", "fe^34", "93^6a", "ef^c5", "a3^fd", "2b^c1", "c4^e9", "1a^26",
"44^d0", "d5^ba", "c2^fa", "04^99", "bf^e7", "ec^06"
]
results = []
for pair in hex_pairs:
hex1, hex2 = pair.split('^')
dec1 = int(hex1, 16)
dec2 = int(hex2, 16)
result = dec1 ^ dec2
results.append(hex(result)[2:].upper())
print(results)
这里其实不用一个一个扒下来,随机数种子的设置如下:
v3 = time(0LL);
srand(v3 & 0xF0000000);
也就是说在 2^28 秒内,随机数都会维持一个相同的序列,时长换算成年,最高位是十万,有生之年都不会变。另外因为windows和linux下的随机数生成过程不一样,一定要使用linux运行才能得到相同数值。
比赛做题的时候人傻了,没注意到这部分,以为出题人用什么神奇的方法把随机数固定住了,硬是要把随机数扒下来,也没想去看看ida反汇编的代码,等我扒完随机数,黄花菜都凉了(QAQ)。另外还眼花看错了最后异或的变量,加密最后异或asc_55AE330010A0,我看成了异或s2,结束了才发现居然还有一个变量。
exp如下:
rand_1 = [
0xd9, 0x0f, 0x18, 0xbd, 0xc7, 0x16, 0x81, 0xbe, 0xf8, 0x4a,
0x65, 0xf2, 0x5d, 0xab, 0x2b, 0x33, 0xd4, 0xa5, 0x67, 0x98,
0x9f, 0x7e, 0x2b, 0x5d, 0xc2, 0xaf, 0x8e, 0x3a, 0x4c, 0xa5,
0x75, 0x25, 0xb4, 0x8d, 0xe3, 0x7b, 0xa3, 0x64
]
rand_2 = [
0x21, 0x00, 0x0a, 0x00, 0x20, 0x1f, 0x0a, 0x1d, 0x09, 0x18,
0x1a, 0x0b, 0x14, 0x18, 0x15, 0x03, 0x0c, 0x0a, 0x0d, 0x02,
0x0f, 0x04, 0x0d, 0x0a, 0x08, 0x03, 0x03, 0x06, 0x00, 0x04,
0x01, 0x01, 0x05, 0x04, 0x00, 0x00, 0x01
]
rand_3 = [
0xDE, 0xAA, 0x42, 0xFC, 0x09, 0xE8, 0xB2, 0x06, 0x0D, 0x93,
0x61, 0xF4, 0x24, 0x49, 0x15, 0x01, 0xD7, 0xAB, 0x04, 0x18,
0xCF, 0xE9, 0xD5, 0x96, 0x33, 0xCA, 0xF9, 0x2A, 0x5E, 0xEA,
0x2D, 0x3C, 0x94, 0x6F, 0x38, 0x9D, 0x58, 0xEA
]
asc_55AE330010A0 = [
0xBF, 0xD7, 0x2E, 0xDA, 0xEE, 0xA8, 0x1A, 0x10, 0x83, 0x73,
0xAC, 0xF1, 0x06, 0xBE, 0xAD, 0x88, 0x04, 0xD7, 0x12, 0xFE,
0xB5, 0xE2, 0x61, 0xB7, 0x3D, 0x07, 0x4A, 0xE8, 0x96, 0xA2,
0x9D, 0x4D, 0xBC, 0x81, 0x8C, 0xE9, 0x88, 0x78
]
def decrypt_string(s):
v31 = []
for n in range(len(s)):
v31.append(asc_55AE330010A0[n] ^ ord(s[n]))
for n in range(len(s)):
v31[n] = rand_3[n] ^ v31[n]
ptr = list(range(len(s)))
for k in range(len(s) - 1, 0, -1):
v18 = rand_2[len(s)-k-1]
ptr[k], ptr[v18] = ptr[v18], ptr[k]
v17 = [None] * len(s)
for m in range(len(s)):
v17[ptr[m]] = v31[m]
result = []
for i in range(len(s)):
result.append(v17[i] ^ rand_1[i])
return result
s2 = "congratulationstoyoucongratulationstoy"
s2_list = list(s2)
result = decrypt_string(s2_list)
flag = ''.join(chr(result[j]) for j in range(len(result)))
print(flag)
# flag{78bace5989660ee38f1fd980a4b4fbcd}
rust_baby
查壳,无壳
放入ida,看到一个极其抽象的main函数,也没看见exe运行输出的字符串在哪。
下断点单步调试,发现进入这个a1函数之后代码就找不到了,尝试查看sub_7FF78C9E298A
函数。
发现在运行过sub_7FF78C9FC570
的时候,界面出现字符,然后就看不懂了。
找到sub_7FF78C9E298A
函数中看起来像是base64加密的字符串,进行解密。
密文:
ewogICAgICAgICJoZW5oZW5hYWEhIjpbMSwxLDQsNSwxLDQsMSw5LDEsOSw4LDEsMF0sCiAgICAgICAgImNyeWZvcmhlbHAiOiJpZ2R5ZG8xOVRWRTEzb2dXMUFUNURnalB6SHdQRFFsZTFYN2tTOFR6SEs4UzVLQ3U5bW5KMHVDbkFRNGFWM0NTWVVsNlF5Y3BpYldTTG1xbTJ5L0dxVzZQTkpCWi9DMlJadXUrRGZRRkN4dkxHSFQ1Z29HOEJObDFqaTJYQjN4OUdNZzlUOENsYXRjPSIsCiAgICAgICAgIndoYXRhZG9vciI6IjExNDUxNDE5MTk4MTBXVEYiLAogICAgICAgICJpd2FudG92aXNpdCI6Ik8wUFN3YW50ZjFhZ25vdzEiCiAgICAgICAgfQ==ewogICAgIndoZXJlIjoid2hlcmUgaXMgeW91ciBmbGFnPzoiLAogICAgIm9tZyI6ImNvcnJlY3QgZmxhZyIsCiAgICAibm9ub25vIjoibm9wZSwgd3JvbmcgZmxhZyIKfQ==
base64解密后:
{
"henhenaaa!":[1,1,4,5,1,4,1,9,1,9,8,1,0],
"cryforhelp":"igdydo19TVE13ogW1AT5DgjPzHwPDQle1X7kS8TzHK8S5KCu9mnJ0uCnAQ4aV3CSYUl6QycpibWSLmqm2y/GqW6PNJBZ/C2RZuu+DfQFCxvLGHT5goG8BNl1ji2XB3x9GMg9T8Clatc=",
"whatadoor":"1145141919810WTF",
"iwantovisit":"O0PSwantf1agnow1"
}{
"where":"where is your flag?:",
"omg":"correct flag",
"nonono":"nope, wrong flag"
}
密文:
ewogICAgIndoZXJlIjoid2hlcmUgaXMgeW91ciBmbGFnPzoiLAogICAgIm9tZyI6ImNvcnJlY3QgZmxhZyIsCiAgICAibm9ub25vIjoibm9wZSwgd3JvbmcgZmxhZyIKfQ==
base64解密后:
{
"where":"where is your flag?:",
"omg":"correct flag",
"nonono":"nope, wrong flag"
}
cryforhelp对应的数据无法直接用base64解密。
然后看不懂了,找到了B站的视频 ciscn24-rust_baby 逆向速览
打断点,调试找输入数据在哪出现。
可以看到有部分输入的数据被写入到变量内存中,一次只处理8个字节,不满8个字节就在后面补E
。
后面是一堆加密,最终字符串长度13*8=104。
输入flag{aaaaaaaaaaaaaaaaaa}
,第一阶段存储在v165,第二阶段存储在v195,第三段flag存储在v22。
flag{aaa ==> ekag|bcc ==> VXRTOQPP
aaaaaaaa ==> ``aabbcc ==> SSRRQQPP
aaaaaaa} ==> ``aabbc 7Fh ==> SSRRQQPL
EEEEEEEE ==> DDEEFFGG ==> wwvvuutt
猜测第一阶段到第二阶段规则如下:
0~1 -1
2~3 +0
4~5 +1
6~7 +2
猜测第二阶段到第三阶段规则是按字节异或0x33。
接下来经过第一层加密后的字符串出现在v91里面,v96是用来异或的数组,加密后的数据如下:
部分异或数组如下:
提取所有用来异或的数据,一共104个,可以将输入数据全部设置为0,0异或任何数都是它本身。
如上图,在进行异或之前,将数据全部设置为0,运行完成之后,v97中的数据为要找的用来异或的密钥
3F8gIsJ5GVY12otH0xn8VRTN0ntYWQlC3iy0SNnyG6lA4ab7/zjB1eLod3hvIgTmFj4MNVJc/cHlWRzQrlqy3Rn4QuYsiVnlEZzIe4Fwf2+8bwKP9/TIcK4C+FvicggJb79LObXQHqM=
继续向后调试代码,发现下一次用到加密后的输入数据的代码如下。
让chatgpt分析后,知道这段代码是base64加密。
对比字符串的代码为v86 = v135[*((_QWORD *)&Src[0] + 1)] == v135[v106];
。
查看用来对比的数据,发现最终对比的字符串是igdydo19TVE13ogW1AT5DgjPzHwPDQle1X7kS8TzHK8S5KCu9mnJ0uCnAQ4aV3CSYUl6QycpibWSLmqm2y/GqW6PNJBZ/C2RZuu+DfQFCxvLGHT5goG8BNl1ji2XB3x9GMg9T8Clatc=
。
分析完毕,写逆向代码,首先将最终对比的字符串base64解码,再异或密钥,接着异或0x33,最后按规则进行反向加减,ASCII解码。
exp如下:
import base64
encoded_string = "igdydo19TVE13ogW1AT5DgjPzHwPDQle1X7kS8TzHK8S5KCu9mnJ0uCnAQ4aV3CSYUl6QycpibWSLmqm2y/GqW6PNJBZ/C2RZuu+DfQFCxvLGHT5goG8BNl1ji2XB3x9GMg9T8Clatc="
xor_key = "3F8gIsJ5GVY12otH0xn8VRTN0ntYWQlC3iy0SNnyG6lA4ab7/zjB1eLod3hvIgTmFj4MNVJc/cHlWRzQrlqy3Rn4QuYsiVnlEZzIe4Fwf2+8bwKP9/TIcK4C+FvicggJb79LObXQHqM="
# 步骤1: base64解码
decoded_bytes = base64.b64decode(encoded_string)
decoded_keys = base64.b64decode(xor_key)
xor_bytes = []
# 步骤2: 异或密钥
for i in range(len(decoded_bytes)):
xor = decoded_bytes[i] ^ decoded_keys[i]
xor_bytes.append(xor)
# 步骤3: 异或0x33
xor_0x33_bytes = bytes([b ^ 0x33 for b in xor_bytes])
# 8个字符一组进行加减运算
processed_bytes = []
for i in range(0, len(xor_0x33_bytes), 8):
group = list(xor_0x33_bytes[i:i+8])
for j in range(-1, 3):
processed_group1 = group[(j + 1) * 2] - j
processed_group2 = group[(j + 1) * 2 + 1] - j
processed_bytes.append(processed_group1)
processed_bytes.append(processed_group2)
processed_bytes = bytes(processed_bytes)
flag = processed_bytes.decode('ascii', errors='ignore')
print(flag)
# flag{6e2480b3-4f02-4cf1-9bc0-123b75f9a922}EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
得到flag为flag{6e2480b3-4f02-4cf1-9bc0-123b75f9a922}
。
androidso_re(动态分析环境配不下去了,有时间再写)
第一次仔细研究安卓逆向题目,争取用静态和动态两种方法解决问题。
放入jadx反编译,定位关键代码,本人习惯一开始定位MainActivity。
我之前是直接打开源代码中的com文件夹,找到MainActivity开始审计代码,但是不知道这些文件夹具体有什么说法。
apk文件修改后缀名作为zip压缩包打开也会看到一些文件,跟jadx反编译后罗列的资源文件相同,查阅资料后发现这些至少在我目前的阶段用不到,具体用途见以下博客。
再看源代码部分,文件夹中包含的代码可能的用途如下(省流:一般只用看com文件夹下的代码):
-
android.support.v4:包含Android Support库中的v4版本的代码。Support库是用于向后兼容旧版本的Android系统的一套库,Android Support 库提供了对新 API 的访问权限,这些 API 在旧版本的 Android 中可能不存在,这个库使得开发者能够编写一次代码,可以在各种版本的 Android 上运行,而无需担心特定 API 的可用性。
-
androidx:AndroidX相当于是对Android Support Library的升级,个人理解它跟android.support.v4差不多。
-
com:是一个顶级包名(Top-Level Package),通常包含应用程序的自定义代码。在这个包下,可能会有多个子文件夹,每个子文件夹代表一个子包,通常与应用程序的功能模块相对应。
-
example.re11113:应用程序的包名,example.re11113 是这个特定应用的Java包名。在这个文件夹下,可能会找到所有的自定义类、活动(Activity)、服务(Service)、广播接收器(BroadcastReceiver)、适配器(Adapter)等。
-
google:这个文件夹可能包含与Google服务相关的代码。
也就是说,只用分析 example.re11113 文件夹下的代码。
开始分析,MainActivity代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private EditText editText;
private boolean legal(String paramString) {
return paramString.length() == 38 && paramString.startsWith("flag{") && paramString.charAt(paramString.length() - 1) == '}' && !inspect.inspect(paramString.substring(5, paramString.length() - 1));
}
/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.Button);
this.editText = (EditText) findViewById(R.id.edit_text);
button.setOnClickListener(this);
}
@Override // android.view.View.OnClickListener
public void onClick(View v) {
Log.d("Button ID", "Button ID: " + v.getId());
switch (v.getId()) {
case R.id.Button /* 2131230723 */:
String inputstr = this.editText.getText().toString();
if (legal(inputstr)) {
Toast.makeText(this, "You are right.", 0).show();
return;
} else {
Toast.makeText(this, "You are wrong.", 0).show();
return;
}
default:
return;
}
}
}
legal函数判断字符串,长度为38,以flag{开头,最后一个字符是},除去首尾的 "flag{" 和 "}" 后的34个字符需要通过inspect类的inspect方法的验证,才会返回true。
onCreate函数初始化活动设置,onClick在按钮被点击时调用。onClick函数当用户点击按钮时,程序会获取文本框中的输入,检查这个输入是否符合legal函数的规则,返回输入是否正确。
双击查看inspect类的inspect方法:
public class inspect {
public static boolean inspect(String input_str) {
try {
byte[] input_flag = input_str.getBytes(StandardCharsets.UTF_8);
byte[] str2 = jni.getkey().getBytes(StandardCharsets.UTF_8);
Arrays.copyOf(str2, 8);
SecretKeySpec key = new SecretKeySpec(str2, "AES");
byte[] ivBytes = jni.getiv().getBytes(StandardCharsets.UTF_8);
IvParameterSpec iv = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
cipher.init(1, key, iv);
byte[] encryptedBytes = cipher.doFinal(input_flag);
String encryptedFlag = Base64.encodeToString(encryptedBytes, 0).trim();
boolean bool = encryptedFlag.equals("JqslHrdvtgJrRs2QAp+FEVdwRPNLswrnykD/sZMivmjGRKUMVIC/rw==");
if (!bool) {
return true;
}
return false;
} catch (Exception exception) {
exception.printStackTrace();
return true;
}
}
}
utf-8解码输入的字符串input_str;getkey函数生成密钥存储在str2中,取前8个字节,创建一个SecretKeySpec对象key,指定str2为密钥,指定是AES算法加密,其结果作为key;getiv函数生成初始化向量IV存储在ivByte中,创建一个IvParameterSpec对象iv,指定ivByte作为初始化向量;创建一个Cipher对象,指定使用DES算法、CBC模式和PKCS5Padding填充机制,初始化Cipher对象,使用加密模式(1代表加密),指定key作为密钥,iv作为初始化向量;对input_flag进行加密操作,得到加密后的字节数组;将解密后的字节数组编码为Base64字符串,并去除两端的空白字符;判断最终得到的字符是否与JqslHrdvtgJrRs2QAp+FEVdwRPNLswrnykD/sZMivmjGRKUMVIC/rw==
相等,相等则返回true。
其中有key算法为AES,加密却是DES的矛盾,查阅资料后有以下分析:
public SecretKeySpec(byte[] key, String algorithm)
从给定的字节数组构造一个密钥。此构造函数不检查给定的字节是否确实指定了指定算法的密钥。例如,如果算法是DES,则此构造函数不检查key是否为8字节长,并且也不检查弱键或半弱键。
也就是说对后面的加密没有太大影响。
逆向分析,JqslHrdvtgJrRs2QAp+FEVdwRPNLswrnykD/sZMivmjGRKUMVIC/rw==
先base64解码,然后对得到的数据使用key作为密钥,iv作为初始化向量进行DES算法、CBC模式和PKCS5Padding填充机制的解密,得到的就是原始flag。
这里有两种方法得到key和iv,我在这里分别进行静态分析和动态分析。
静态分析
双击getiv函数,发现getiv和getkey函数都来自于Secret_entrance文件,这是一个.so文件,找出来放入ida进行逆向。
public class jni {
public static native String getiv();
public static native String getkey();
static {
System.loadLibrary("Secret_entrance");
}
}
找到这两个需要分析的函数。
F5反汇编显示以上弹窗,是因为文件是ARM架构,需要对应的插件才能反汇编,但是这个ida里面没有。换了一个版本的ida(应该是在吾爱破解下载的7.6版本,时间太久记不清在哪下载的了),F5反汇编成功。
__int64 __fastcall Java_com_example_re11113_jni_getiv(__int64 a1)
{
//定义变量,省略
v8 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
getiv_fixed();
if ( (v5 & 1) != 0 )
v2 = v7;
else
v2 = v6;
v3 = (*(__int64 (__fastcall **)(__int64, _BYTE *))(*(_QWORD *)a1 + 1336LL))(a1, v2);
if ( (v5 & 1) != 0 )
operator delete(v7);
return v3;
}
有一个get_fixed函数未知,继续找。
void getiv_fixed(void)
{
//定义变量,省略
v29 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v26 = 24;
strcpy(v27, "F2IjBOh1mRW=");
std::string::basic_string(&v22, &v26);
v1 = v24;
v0 = v25;
v2 = v22 & 1;
v3 = (unsigned __int64)v22 >> 1;
if ( (v22 & 1) != 0 )
v4 = v24;
else
v4 = (unsigned __int64)v22 >> 1;
if ( (v22 & 1) != 0 )
v5 = v25;
else
v5 = v23;
if ( !v4 )
goto LABEL_22;
v6 = v5;
if ( v4 == 1 )
goto LABEL_33;
v7 = v5 + 1;
v6 = &v5[v4 & 0xFFFFFFFFFFFFFFFELL];
v8 = v4 & 0xFFFFFFFFFFFFFFFELL;
do
{
v9 = (unsigned __int8)*(v7 - 1);
v10 = (unsigned __int8)*v7;
v11 = v10 - 65;
if ( (unsigned int)(v9 - 65) <= 0x19 )
*(v7 - 1) = (char)(v9 - 49) % 26 + 65;
if ( v11 <= 0x19 )
*v7 = (char)(v10 - 49) % 26 + 65;
v12 = (unsigned int)(v10 - 97) < 0x1A && v11 > 0x19;
if ( (unsigned int)(v9 - 97) <= 0x19 && (unsigned int)(v9 - 65) >= 0x1A )
*(v7 - 1) = (v9 - 81) % 26 + 97;
if ( v12 )
*v7 = (v10 - 81) % 26 + 97;
v8 -= 2LL;
v7 += 2;
}
while ( v8 );
if ( v4 != (v4 & 0xFFFFFFFFFFFFFFFELL) )
{
LABEL_33:
v18 = &v5[v4];
do
{
v21 = (unsigned __int8)*v6;
if ( (unsigned int)(v21 - 65) < 0x1A )
{
v19 = 65;
v20 = -49;
}
else
{
if ( (unsigned int)(v21 - 97) > 0x19 )
goto LABEL_36;
v19 = 97;
v20 = -81;
}
*v6 = v19 + (v20 + v21) % 26;
LABEL_36:
++v6;
}
while ( v18 != v6 );
}
v1 = v24;
v0 = v25;
v2 = v22 & 1;
v3 = (unsigned __int64)v22 >> 1;
LABEL_22:
if ( v2 )
v13 = v0;
else
v13 = v23;
if ( v2 )
v14 = v1;
else
v14 = v3;
v15 = (_QWORD *)std::__put_character_sequence<char,std::char_traits<char>>(&std::cout, v13, v14);
std::ios_base::getloc((std::ios_base *)((char *)v15 + *(_QWORD *)(*v15 - 24LL)));
v16 = std::locale::use_facet(v28, &std::ctype<char>::id);
v17 = (*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)v16 + 56LL))(v16, 10LL);
std::locale::~locale((std::locale *)v28);
std::ostream::put(v15, v17);
std::ostream::flush(v15);
didi(&v22);
if ( (v22 & 1) != 0 )
operator delete(v25);
if ( (v26 & 1) != 0 )
operator delete(*(void **)&v27[15]);
}
代码中出现一个字符串F2IjBOh1mRW=
。
放入chatgpt分析代码,如果字符是大写字母(ASCII 在 65 到 90 之间),使用 ((字符 - 49) % 26 + 65) 进行转换;如果字符是小写字母(ASCII 在 97 到 122 之间),使用 ((字符 - 81) % 26 + 97) 进行转换。猜测这部分是在对字符串F2IjBOh1mRW=
进行操作,相当于对每个字母进行移位加密,移位量-16。
然后是一些输出操作,再调用 didi
函数,这个函数经过分析发现是base64解码函数。
根据以上分析,将字符串F2IjBOh1mRW=
移位-16加密,得到V2YzREx1cHM=
,再base64解码得到最终的iv,Wf3DLups
。
接下来分析key的获取方法。
__int64 __fastcall Java_com_example_re11113_jni_getkey(__int64 a1)
{
_BYTE *v2; // x8
bool v3; // zf
_BYTE *v4; // x8
_BYTE *v5; // x8
_BYTE *v6; // x8
_BYTE *v7; // x8
_BYTE *v8; // x8
_BYTE *v9; // x8
_BYTE *v10; // x8
_QWORD *v11; // x20
__int64 v12; // x0
unsigned int v13; // w21
char *v14; // x1
__int64 v15; // x19
char v17; // [xsp+8h] [xbp-58h] BYREF
char v18[23]; // [xsp+9h] [xbp-57h] BYREF
char v19; // [xsp+20h] [xbp-40h] BYREF
_BYTE v20[15]; // [xsp+21h] [xbp-3Fh] BYREF
void *v21; // [xsp+30h] [xbp-30h]
char v22; // [xsp+38h] [xbp-28h] BYREF
char v23[23]; // [xsp+39h] [xbp-27h] BYREF
char v24[8]; // [xsp+50h] [xbp-10h] BYREF
__int64 v25; // [xsp+58h] [xbp-8h]
v25 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v22 = 24;
strcpy(v23, "TFSecret_Key");
v17 = 20;
strcpy(v18, "YourRC4Key");
jiejie(&v22, &v17);
if ( (v19 & 1) != 0 )
v2 = v21;
else
v2 = v20;
*v2 ^= 3u;
v3 = (v19 & 1) == 0;
if ( (v19 & 1) != 0 )
v4 = v21;
else
v4 = v20;
v4[1] ^= 0x89u;
v5 = v21;
if ( v3 )
v5 = v20;
v5[2] ^= 0x33u;
v6 = v21;
if ( v3 )
v6 = v20;
v6[3] ^= 0xB8u;
v7 = v21;
if ( v3 )
v7 = v20;
v7[4] ^= 0x54u;
v8 = v21;
if ( v3 )
v8 = v20;
v8[5] ^= 0xCu;
v9 = v21;
if ( v3 )
v9 = v20;
v9[6] ^= 0x20u;
v10 = v21;
if ( v3 )
v10 = v20;
v10[7] ^= 0x6Au;
std::string::basic_string(&v17, &v19, 0LL, 8LL, &v19);
v11 = (_QWORD *)std::__put_character_sequence<char,std::char_traits<char>>(&std::cout, "Keys match!", 11LL);
std::ios_base::getloc((std::ios_base *)((char *)v11 + *(_QWORD *)(*v11 - 24LL)));
v12 = std::locale::use_facet(v24, &std::ctype<char>::id);
v13 = (*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)v12 + 56LL))(v12, 10LL);
std::locale::~locale((std::locale *)v24);
std::ostream::put(v11, v13);
std::ostream::flush(v11);
if ( (v17 & 1) != 0 )
v14 = *(char **)&v18[15];
else
v14 = v18;
v15 = (*(__int64 (__fastcall **)(__int64, char *))(*(_QWORD *)a1 + 1336LL))(a1, v14);
if ( (v17 & 1) == 0 )
{
if ( (v19 & 1) == 0 )
goto LABEL_24;
LABEL_28:
operator delete(v21);
if ( (v22 & 1) == 0 )
return v15;
goto LABEL_25;
}
operator delete(*(void **)&v18[15]);
if ( (v19 & 1) != 0 )
goto LABEL_28;
LABEL_24:
if ( (v22 & 1) != 0 )
LABEL_25:
operator delete(*(void **)&v23[15]);
return v15;
}
发现字符串 TFSecret_Key
和 YourRC4Key
,代码调用了 jiejie
函数,该函数复杂不好分析,根据字符串的提示猜测是RC4加密,密钥和加密的字符串已知;猜测代码中的异或部分是在异或加密后的数据,根据变量的存储位置,猜测是数据从前往后异或0x03,0x89,0x33,0xB8,0x54,0x0C,0x20,0x6A。
写出代码获取密钥:
from Crypto.Cipher import ARC4
def rc4_encrypt(data, key):
cipher = ARC4.new(key)
return cipher.encrypt(data)
def xor_with_bytes(data, bytes_sequence):
result = bytearray()
for i in range(len(data)):
result.append(data[i] ^ bytes_sequence[i % len(bytes_sequence)])
return bytes(result)
plaintext = b'TFSecret_Key'
key = b'YourRC4Key'
encrypted_data = rc4_encrypt(plaintext, key)
xor_sequence = bytes([0x03, 0x89, 0x33, 0xB8, 0x54, 0x0C, 0x20, 0x6A])
final_result = xor_with_bytes(encrypted_data, xor_sequence)
print(final_result)
运行代码得到b'A8UdWaeq\xc1\xb2k\x02'
,根据inspect.inspect方法的key获取过程,取前8个字节作为密钥key。
进行DES解密,密钥A8UdWaeq
,ivWf3DLups
,解密得到188cba3a5c0fbb2250b5a2e590c391ce
,代码如下:
from Crypto.Cipher import DES
from Crypto.Util.Padding import unpad
import base64
def decrypt_des(ciphertext, key, iv):
cipher = DES.new(key.encode('utf-8'), DES.MODE_CBC, iv.encode('utf-8'))
ciphertext = base64.b64decode(ciphertext)
plaintext = cipher.decrypt(ciphertext)
plaintext = unpad(plaintext, DES.block_size)
return plaintext.decode('utf-8')
ciphertext = "JqslHrdvtgJrRs2QAp+FEVdwRPNLswrnykD/sZMivmjGRKUMVIC/rw=="
key = "A8UdWaeq"
iv = "Wf3DLups"
plaintext = decrypt_des(ciphertext, key, iv)
print(plaintext)
flag为flag{188cba3a5c0fbb2250b5a2e590c391ce}
。
静态分析的脑壳疼,太复杂的代码能动调还是动调。
动态分析(未完成)
动态分析获取key和iv。
安卓逆向动态分析的学习,有一篇很好的文章,解答了我很多的疑惑。
出于安全考虑,Android系统并不允许应用被随意调试,官方文档称需要满足二者之一的条件:
1.App的AndroidManifest.xml中Application标签必选包含属性android:debuggable=“true”
;
2./default.prop中ro.debuggable的值为1;
可以看到这道题直接就是可调试的。
放入雷电模拟器直接动态调试,输入flag{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa},发现会闪退
没配置好frida环境,文章看的脑壳疼,这两天是配不下去了,以后有时间继续写