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反编译后罗列的资源文件相同,查阅资料后发现这些至少在我目前的阶段用不到,具体用途见以下博客。

Android 解读.apk解压后文件详细说明

再看源代码部分,文件夹中包含的代码可能的用途如下(省流:一般只用看com文件夹下的代码):

  1. android.support.v4:包含Android Support库中的v4版本的代码。Support库是用于向后兼容旧版本的Android系统的一套库,Android Support 库提供了对新 API 的访问权限,这些 API 在旧版本的 Android 中可能不存在,这个库使得开发者能够编写一次代码,可以在各种版本的 Android 上运行,而无需担心特定 API 的可用性。

  2. androidx:AndroidX相当于是对Android Support Library的升级,个人理解它跟android.support.v4差不多。

  3. com:是一个顶级包名(Top-Level Package),通常包含应用程序的自定义代码。在这个包下,可能会有多个子文件夹,每个子文件夹代表一个子包,通常与应用程序的功能模块相对应。

  4. example.re11113:应用程序的包名,example.re11113 是这个特定应用的Java包名。在这个文件夹下,可能会找到所有的自定义类、活动(Activity)、服务(Service)、广播接收器(BroadcastReceiver)、适配器(Adapter)等。

  5. 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_KeyYourRC4Key ,代码调用了 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 App之动态调试

出于安全考虑,Android系统并不允许应用被随意调试,官方文档称需要满足二者之一的条件:
1.App的AndroidManifest.xml中Application标签必选包含属性android:debuggable=“true”
2./default.prop中ro.debuggable的值为1;


可以看到这道题直接就是可调试的。

放入雷电模拟器直接动态调试,输入flag{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa},发现会闪退


没配置好frida环境,文章看的脑壳疼,这两天是配不下去了,以后有时间继续写


crypto

posted @ 2024-05-21 13:08  skdtxdy  阅读(420)  评论(7编辑  收藏  举报