逆向

1.kotlindroid

先在手机上运行一下

发现字符串check your flag here

因为用jadx打开这个apk,然后传统方法找到MainActivity发现根本看不懂。就直接搜字符串吧

然后跳过去查看代码,虽然说 Kotlin 代码 不好看懂,不过这里大致看出什么逻辑

再结合手机上,我输错了就是wrong字符串看出,这里的check应该就是检查逻辑,前面有一大段代码都是什么初始化界面,这里的密文发现是base64编码,加密也 可以看出是AES/GCM,GCM的加密需要密钥,IV,tag

提示说tag就是密文的一部分,先不管

1.求密钥

这里就是两个keybyte数组,分别经过异或23,和8 然后拼接组成密钥,这里看出byte2来自Config类的方法

def generate_key():
key1 = [118, 99, 101, 126, 124, 114, 110, 100]
key2 = [123, 113, 109, 99, 97, 122, 124, 105]


modified_key1 = bytes([b ^ 23 for b in key1])


modified_key2 = bytes([b ^ 8 for b in key2])


final_key = modified_key1 + modified_key2

return final_key.decode()

# 生成密钥
key = generate_key()
print("密钥:", key)
#atrikeyssyekirta

2.IV

这里已经给了IV,是字符串114514,然后我就去套脚本发现不行,卡了很久,后面才发现AES的这个IV必须是bytes类型而不是字符串,包括那个key,这里的byte [] bytes也已经很明显了

有意思的是,我在用密文解密base的时候发现了密文前面就是那个IV

最后发现这个密文其实是由iV+密文+tag组成的,好像都是这样组成的,tag连同密文一起,不用管,后面就在脚本中分离IV

import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

key="atrikeyssyekirta"
key=key.encode('utf-8')

iv = "114514".encode('utf-8')
print(iv)

# 预设的 Base64 编码的加密字符串
encrypted_base64 = "MTE0NTE0HMuJKLOW1BqCAi2MxpHYjGjpPq82XXQ/jgx5WYrZ2MV53a9xjQVbRaVdRiXFrSn6EcQPzA=="

def decrypt(encrypted_base64, key, iv):
try:

encrypted_bytes = base64.b64decode(encrypted_base64)
# 分离出 IV 和密文,这里假设编码时 IV 在前

encrypted_data = encrypted_bytes[len(iv):]


cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
# 解密密文
decrypted_bytes = cipher.decrypt(encrypted_data)

try:
# 尝试将解密后的字节数据解码为 UTF-8 字符串
decrypted_text = decrypted_bytes.decode('utf-8')
return decrypted_text
except UnicodeDecodeError:
print(f"解密后的字节数据: {decrypted_bytes}")
return None

except Exception as e:
print(f"解密出错: {e}")
return None

# 调用解密函数
decrypted_text = decrypt(encrypted_base64, key, iv)
if decrypted_text:
print(f"解密后的文本: {decrypted_text}")

2.fish

不知道是不是手机的原因,无法打开这个应用,只好拿出以前手机来开开

根据提示和这个输出的字符串发现,如果输入错误,鱼就会跑

这里的onclick的逻辑跟我们应用的时候一样

这里就是对输入进行加密,感觉类似于凯撒加密,逆向的时候可以对字符串先进行+’1‘-i2%4,推测是在哪个分支过来的

这里的大致逻辑就是通过输入的字符串进行encode加密,然后调用这个下载的dex文件里面的加密再次进行验证,密文就在dex文件里面,既然要下载这个密文,那么肯定有相应的地址,

网页搜索后就下载成功了,之后我们去用jadx打开这个dex文件

大致逻辑就算,输入的字符串先经过encrypt加密,再经过这个dex文件的encode加密,最终对比里面的字符串strr,这里的逆向就是通过一个decode,然后进行encrypy逆向,这里的逆向两个交换位置用的是分别异或

import binascii

# 定义解码映射
fish_dcode = {
"iiijj": 'a', "jjjii": 'b', "jijij": 'c', "jjijj": 'd', "jjjjj": 'e',
"ijjjj": 'f', "jjjji": 'g', "iijii": 'h', "ijiji": 'i', "iiiji": 'j',
"jjjij": 'k', "jijji": 'l', "ijiij": 'm', "iijji": 'n', "ijjij": 'o',
"jiiji": 'p', "ijijj": 'q', "jijii": 'r', "iiiii": 's', "jjiij": 't',
"ijjji": 'u', "jiiij": 'v', "iiiij": 'w', "iijij": 'x', "jjiji": 'y',
"jijjj": 'z', "iijjl": '1', "iiilj": '2', "iliii": '3', "jiili": '4',
"jilji": '5', "iliji": '6', "jjjlj": '7', "ijljj": '8', "iljji": '9',
"jjjli": '0'
}
def decode_fish(encoded_str):
"""解密 hook_fish.dex 里的密文"""
plaintext = ""
for i in range(0, len(encoded_str), 5): # 5个字符一组
chunk = encoded_str[i:i + 5]
plaintext += fish_dcode.get(chunk) # 解密失败返回 '?'
return plaintext
def reverse_branch_transform(ciphertext):
result_chars = []
for i, c in enumerate(ciphertext):
F = ord(c)
# 尝试分支1(原始字符在 'a'-'f' 范围)
candidate1 = F - (i % 4) + ord('1')
if ord('a') <= candidate1 <= ord('f'):
result_chars.append(chr(candidate1))
else:
# 分支2(原始字符为数字 '0'-'9')
candidate2 = F - ord('7') - (i % 10)
# 我们希望 candidate2 落在 '0'-'9' 的范围内
if ord('0') <= candidate2 <= ord('9'):
result_chars.append(chr(candidate2))
else:
# 如果都不符合,则默认采用 candidate1(理论上不会发生)
result_chars.append(chr(candidate1))
return ''.join(result_chars)


def reverse_swap(s):
"""
逆向 code() 中的交换操作。
加密时,对十六进制字符串的字符数组每两个字符进行了互换,
由于交换是自反操作,交换两次就能还原原序。
"""
chars = list(s)
for i in range(0, len(chars) - 1, 2):
# 交换相邻两个字符
chars[i], chars[i + 1] = chars[i + 1], chars[i]
return ''.join(chars)


def decrypt_encrypt(ciphertext):
"""
完整的解密流程,逆向 encrypt() 的操作:
1. 对最终密文的每个字符进行逆向“分支变换”
2. 逆向相邻字符的交换(code()函数)
3. 得到原始十六进制字符串,将其转换为字节
4. 对每个字节减去 68,恢复原始明文字节
5. 将字节转换为字符串返回
"""
# 步骤①:逆向最后一步变换
after_branch = reverse_branch_transform(ciphertext)
# 步骤②:逆向交换操作
hex_str = reverse_swap(after_branch)
# 步骤③:将十六进制字符串转换为字节数组

enc_bytes = binascii.unhexlify(hex_str)
# 步骤④:每个字节减去 68
original_bytes = bytes([b - 68 for b in enc_bytes])
# 步骤⑤:转换为字符串(假设使用 UTF-8 编码)
try:
plaintext = original_bytes.decode('utf-8')
except Exception:

plaintext = original_bytes.decode('latin-1')
return plaintext


if __name__ == "__main__":
strr = "jjjliijijjjjjijiiiiijijiijjiijijjjiiiiijjjjliiijijjjjljjiilijijiiiiiljiijjiiliiiiiiiiiiiljiijijiliiiijjijijjijijijijiilijiijiiiiiijiljijiilijijiiiijjljjjljiliiijjjijiiiljijjijiiiiiiijjliiiljjijiiiliiiiiiljjiijiijiijijijjiijjiijjjijjjljiliiijijiiiijjliijiijiiliiliiiiiiljiijjiiliiijjjliiijjljjiijiiiijiijjiijijjjiiliiliiijiijijijiijijiiijjjiijjijiiiljiijiijilji"
decrypted_text = decode_fish(strr)

ciphertext = decrypted_text.strip()
plaintext = decrypt_encrypt(ciphertext)
print(plaintext)

3.fuko’s starfish

这个题的流程是进行了三个游戏,前两个都不是特别重要,主要是调用了dll里面的函数,不过我一调试就反调试了

1. _initterm_e (反调试)

假如有TLS回调函数,静态调试下修改没有用(可能在导出表看不到,可以在进_initterm_之前下断点,再动态中修改)

这个函数会在主函数之前加载,具体作用就是初始化一系列函数,参数是函数指针数组的起始地址和结尾地址,遍历一系列函数

这里对main函数进行交叉应用,再找到这个函数之后,去看它的参数的函数是不是有反调试函数

我在dll里面也看到过如上图的情形,当时以为没什么用就直接走了,结果看汇编才发现它竟然是花指令

2.花指令

这里的call $+5,值得注意,把下一个代码的地址压入栈中,再跳转到下一个代码执行,而且没有弹出,堆栈不平衡,而且看图也可以发现流程应该在右边,这里直接删除push eax到pop eax的所有代码

之后看到就是一个反调试代码,修改一下跳转就行,重要的是记得保存,包括在exe里面也要保存

apply patches

3.dll里面的反调试

这个dll里面的反调试也挺新颖的

这里我已经修改过了,如果程序没有被调试,dwFlags的值一般是1,记得保存

后面还有一个检测进程的反调试,出题人的解决方式是,nop掉要退出的代码

之后通过findcript发现是标准的AES代码

srand(114514);
for (int i = 0; i < 16; i++) {
key[i] = 0x17^(rand()%0xff);
if (key[i] < 16) printf("0");
printf("%x", key[i]);
}
from Crypto.Cipher import AES  
import binascii

hex_key = "09e5fdeb683175b6b13b840891eb78d2"
hex_ciphertext = "3d011c190ba090815f672731a89aa47497362167ab2eb4a09418d37d93e646e7"

key = binascii.unhexlify(hex_key)
ciphertext = binascii.unhexlify(hex_ciphertext)

cipher = AES.new(key, AES.MODE_ECB)

decrypted = cipher.decrypt(ciphertext)
print(decrypted)

#b"VNCTF{W0w_u_g0t_Fuk0's_st4rf1sh}"

4.幸运转盘

对于这种鸿蒙逆向的,都是把.hap文件解压之后去找ets目录的.abc文件拖进反编译工具,之后去找index.js(如果实在找不到逻辑可以 DevEco Studio模拟器安装并运行得到界面 )

**pages/index.ets** 或 **pages/index.js** 通常是鸿蒙应用的默认入口页面,相当于 Android 的 MainActivity。

寻找主要逻辑,可以一边找一边喂给AI,

这段代码主要用于页面跳转,同时调用了一个外部库函数处理数据,并将处理结果传递到目标页面

传入的参数就是输入和固定值24,去找so文件

跟普通的so文件不同,鸿蒙逆向的so找主要逻辑是找RegisterModule,相当于找Java的注册JNi一样了

找到主要逻辑

这个逻辑比较重要,刚才说传入的是输入和一个数字(应该是输入的长度)和一个固定的数,这调用了固定的c语言API进行平方和求根判断是不是等于40来判断长度,然后调用这个rc4的加密

这个多了一个异或40

之后就是一个标准的base64加密,返回结果,最后去js层看看代码

发现一个类似于密文的东西,找找调用看有没有判断

这里应该是判断,但是直接解密发现不成功

这里还有一个加密的操作,但是解密还是不对,

这里可能对它进行了加3,再减少3

import base64


def base64_decode(encoded_data):
try:
decoded_data = base64.b64decode(encoded_data)
return decoded_data
except Exception as e:
print(f"Base64解码失败: {e}")
return None


def rc4_ksa(key):
# RC4的密钥调度算法 (KSA)
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]
return S


def rc4_prga(S, data_len):
# 伪随机生成算法 (PRGA)
i = 0
j = 0
key_stream = []
for _ in range(data_len):
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
key_stream.append(S[(S[i] + S[j]) % 256])
return key_stream


def rc4_decrypt(key, data):
key = [ord(c) for c in key]
S = rc4_ksa(key)
key_stream = rc4_prga(S, len(data))
decrypted_data = bytes([(data[i] ^ key_stream[i])-3 for i in range(len(data))])
return decrypted_data


def main():
base64_input = [101, 74, 76, 49, 101, 76, 117, 87, 55, 69, 118, 68, 118, 69, 55, 67, 61, 83, 62, 111, 81, 77, 115, 101, 53, 73, 83, 66, 68, 114, 109, 108, 75, 66, 97, 117, 93, 127, 115, 124, 109, 82, 93, 115]
key = "Take_it_easy"
what=[]
for i in base64_input:
i=(i^7)-1
what.append(i)
# Base64解码
decoded_data = base64_decode(bytes(what))
if decoded_data is None:
return
yes=[]
for i in decoded_data:
i^=40
yes.append(i)
# RC4解密
decrypted_data = rc4_decrypt(key, yes)
print("解密后的数据: ", decrypted_data.decode(errors='ignore'))


if __name__ == "__main__":
main()
#VNCTF{JUst_$ne_Iast_dance_2025!}

密码学

1.easyMath

from sympy import symbols, solve, Poly
from Crypto.Util.number import long_to_bytes
from sympy.ntheory.modular import crt
from itertools import product

# 已知的 c 和多项式
c = 24884251313604275189259571459005374365204772270250725590014651519125317134307160341658199551661333326703566996431067426138627332156507267671028553934664652787411834581708944
polynomial_str = "x**3 - 15264966144147258587171776703005926730518438603688487721465*x**2 + 76513250180666948190254989703768338299723386154619468700730085586057638716434556720233473454400881002065319569292923*x - 125440939526343949494022113552414275560444252378483072729156599143746741258532431664938677330319449789665352104352620658550544887807433866999963624320909981994018431526620619"

# 定义符号变量
x = symbols('x')

# 将多项式字符串转换为 SymPy 多项式对象
poly = Poly(polynomial_str, x)

# 求解多项式的根
roots = solve(poly, x)

# 提取三个素数
p1, p2, p3 = [int(root) for root in roots]

def sqrt_mod(c, p):
""" 计算模 p 的平方根(p ≡ 3 mod 4)"""
exponent = (p + 1) // 4
root = pow(c % p, exponent, p)
return root, p - root # 返回两个可能的解

# 计算每个素数下的平方根组合
roots = [
sqrt_mod(c, p1),
sqrt_mod(c, p2),
sqrt_mod(c, p3)
]

# 生成所有可能的根组合(共 2^3 = 8 种)
all_combinations = product(*roots)

# 遍历所有组合并应用 CRT
solutions = []
for comb in all_combinations:
m, _ = crt([p1, p2, p3], comb)
solutions.append(m)

# 添加模 N 下的互补解(因为 x 和 N - x 可能均为有效解)
N = p1 * p2 * p3
solutions += [N - x for x in solutions] # N = p1 * p2 * p3

# 去重并排序
unique_solutions = list(set(solutions))

# 转换为字节并输出所有可能结果
print("所有可能的解(数值 -> 字节):")
for idx, m in enumerate(unique_solutions):
try:
# 尝试 UTF-8 解码
flag_utf8 = long_to_bytes(m).decode('utf-8')
printable = all(32 <= ord(c) <= 126 for c in flag_utf8) # 检查是否为可打印 ASCII
print(f"[解{idx + 1}] 数值: {m}")
print(f" UTF-8: {flag_utf8 if printable else '非打印字符'}")
except UnicodeDecodeError:
pass

# 显示原始字节
raw_bytes = long_to_bytes(m)
print(f" 原始字节: {raw_bytes.hex()}")
print("-" * 60)

print("请检查输出中符合语义的 flag")