Week1#
SignSign#
打开程序,直接送上flag,但只输出了flag的后一半??
为了找到完整的flag,我们用ida打开程序,进行静态分析。
正常情况下,一开始能看到一个写满汇编指令的窗口。这个就是程序对应主函数的反汇编结果。
但是主函数中并没有另一半flag。
这个时候我们的思路应该是去搜集一下这个程序的相关信息,尤其是字符串信息很重要。
打开字符串窗口(快捷键Shift+F12),查看程序的所有字符串。
能看到两段flag都在其中,拼接一下即可提交。
BinaryMaster#
有两种思路:
- 按照题目提示,八进制04242424转成十六进制为0x114514。把这个结果输入程序后拿到flag。
- IDA反编译主函数,可以直接看到flag明文。
对于第二种思路:在反汇编结果页面,按F5(或者Tab)就可以生成反编译的伪C代码。这一功能大大降低了逆向分析的难度。反编译成功后,程序的主要逻辑就很清晰明白了。
BabyBase#
按照上面两题的方法反编译程序主函数。
发现调用了两个可能和flag相关的函数:encode()
和 check_flag()
,后续要着重分析。
如果继续收集信息,查看字符串,能够发现一个比较特殊的字符串:(下图蓝色高亮)
有经验的师傅一眼就能看出是base64标准索引表,然后直接base64decode一把梭了。不过没看出也不要紧。
按照常规思路,逆向分析上面提到的两个关键函数:
check_flag
:检查编码后的结果是否正确,其实就是给出了编码结果(在上面字符串窗口也能看到)encode
:Base64编码的具体实现,如果要彻底看懂这个函数,最好提前了解一下Base64的原理。不过看到一些关键特征也能大概推测出来,比如3字节输入变成4字节输出、以及添加到末尾的等号。
最后拿着结果找个在线网站解码就可以,非常推荐: CyberChef
Xor-Beginning#
反编译后,可以暂时忽略主函数中常见的IO函数和数据赋值等部分,以便快速找到核心逻辑。
可以发现关键代码是下图的一段循环。涉及到运算符^
,对应的运算是异或。
异或(xor)是逆向中很常见的一种位运算,它的逆运算就是自身,也就是 (A ^ B) ^ B == A
由此推导出解密方法和加密方法相同。
回到前面提取出赋值给数组 v5
的密文,可以写出如下解密脚本:
data=[0x7E, 0x35, 0x0B, 0x2A, 0x27, 0x2C, 0x33, 0x1F, 0x76, 0x37,
0x1B, 0x72, 0x31, 0x1E, 0x36, 0x0C, 0x4C, 0x44, 0x63, 0x72,
0x57, 0x49, 0x08, 0x45, 0x42, 0x01, 0x5A, 0x04, 0x13, 0x4C]
for i in range(len(data)):
data[i] = data[i]^(78-i)
print(bytes(data).decode("utf-8"))
Xor-Endian#
c语言中int类型是32位数据,即4字节。
由于数据在内存中以字节为单位存储,int类型也可以看作长度为4的char数组。
多字节类型的数据,各个字节按照 小端序(Little Endian)存储,这一顺序与我们习惯的阅读顺序相反。
本题的文件格式是ELF,也就是Linux下的可执行程序。
可以在Linux系统下用 file
命令查一下:
ELF在Windows系统下不能直接运行,但并不妨碍用IDA进行静态分析。
和上一题类似,程序同样使用了xor进行加密,但是密文是比较大的整数,有点奇怪。
出题时这里其实是char数组强制类型转换成了int数组,每一个数字实际上对应着四个字符。
在提取密文进行解密的时候,要把这些数字重新转换成字符串或者char数组。过程中一定注意小端序的处理,才能让最后得到的字符串顺序正确。例如下面python脚本使用的函数 to_bytes(4,"little")
data = [1363025275,253370901,1448151638,1415391232,91507463,139743552,
1450318164,1985283101,1465125718,1934953223,84430593]
source = b''.join([element.to_bytes(4,"little") for element in data])
key = b'Key0xGame2024'
for i in range(len(source)):
flag = chr(source[i] ^ key[i % len(key)])
print(flag,end="")
当然也可以巧妙利用IDA规避这一问题。IDA自动按照小端序解析字符串,但是注意伪代码中字符串用双引号标注,有的师傅遇到下图的情况:
仔细观察能发现是用的单引号,这个应该理解为字节序列,依然按照大端的顺序从左到右解析的。
这个时候按一下F5重新反编译就能看到正确顺序的字符串。最后解密思路和上面脚本相同。
Week2#
BabyUPX#
根据题目的提示,可以猜到本题加了UPX壳。UPX是一种常见的压缩壳,可以使用查壳工具进行检测。常用的有以下两种:
- Exeinfo PE (Github:exeinfo)
- Detect-It-Easy (比较推荐,Github:DIE)
压缩壳对exe文件进行压缩,在执行原程序代码前率先取得控制权,自动对原始程序解压还原,以达到保护程序不被静态反编译的目的。如果直接把程序扔进IDA,就会看到警告和一个明显不正常的start
函数,而我们想要的主函数是无法找到的。
这里就要考虑如何脱壳。UPX能够压缩的同时自带解压功能,所以先去下载 UPX- the Ultimate Packer for eXecutables(命令行工具)。然后一把抓住程序,输入脱壳命令,顷刻炼化。
upx -d <filename>
脱壳后能用IDA正常打开。分析encode
函数,可以看出每个字节高4位和低4位交换,实际上就是16进制逆序。
FirstSight-Pyc#
.pyc
是Python源代码经过编译生成的字节码文件。可以选择pycdc
或者uncompyle6
进行反编译。
要注意pyc文件对应的python版本可能会影响反编译的效果,本题使用的是较低的3.8版本,经过测试上述两个反编译引擎都能还原代码。
以uncompyle6
为例进行分析,反编译结果如下:
加密逻辑是先对输入取md5得到十六进制摘要,之后仅对数字部分进行rot5,最后套上0xGame{}
输出。
直接运行pyc程序即可得到flag,但是需要用3.8版本的python解释器才能正常运行。
当然也可以把反编译的代码copy下来运行。
FirstSight-Jar#
Java源文件编译后生成.class
文件,之后打包成可执行的jar
包。
针对jar的反编译工具有很多,例如Procyon、CFR、jd、jadx等,一抓一大把,找一款适合的就可以。
以jadx为例:
encrypt
实现了一个简单的仿射密码,只不过字母表改成了十六进制字符。
加密函数的数学形式为 \(E(x) = 5x+3(mod\ 16)\),求解同余式就可以反解出x。注意解密时不能用除法,在模运算下要改用乘法逆元。
import gmpy2
Alphabat = "0123456789abcdef"
enc = "ab50e920-4a97-70d1-b646-cdac5c873376"
inverse = int(gmpy2.invert(5,16))
for i in enc:
if i not in Alphabat:
print(i,end="")
else:
index = ((Alphabat.index(i) - 3)*inverse)%16
print(Alphabat[index],end="")
另外值得一提,仿射密码作为单表代换密码,明文和密文之间存在一一映射关系。有不少师傅先计算出了明文字符与密文字符的对应关系,然后逐一替换来解密。这个思路也是可行的。
Xor::Random#
c++程序,反编译后看起来有一点丑陋,涉及到大量使用的模板,类,对象,还有构造析构函数。
函数名比较长,不过都是标准库的函数,不难根据名字推测函数功能。
主函数大概可以划分成三部分:先检查了flag的格式和长度,之后生成随机数v21
,最后一边用v21
异或加密一边检查。注意密钥从v21
和v21+3
中交替选择。
重点说一下中间随机数的部分。有两种解法:
- 解法一:常规静态分析
结合密码学的相关知识,可以知道种子能够控制伪随机数生成。程序首先在init_random
函数中设置了种子0x77
。后面if
语句中又设置了种子0x1919810
。到底用了哪个种子呢?
回过头跟踪v6的赋值,查看check()
,不难发现这个函数的返回值一定为0,那么v6的值也为0。上图绿色方框内的srand()
压根不会执行。由此可以确定正确的种子,进而得到正确的随机数值。等效的简化代码如下:
srand(0x77);
v22 = rand();
v21 = rand();
不同编程语言的伪随机数生成算法存在差异,甚至c标准库在windows和linux系统下的rand
实现也不一样。本题的随机数应在windows下调用stdlib.h
的rand
。
#include<stdio.h>
#include<stdlib.h>
int main()
{
unsigned char num,key;
unsigned char *array;
unsigned long long v13[4];
v13[0] = 0x1221164E1F104F0C;
v13[1] = 0x171F240A4B10244B;
v13[2] = 0x1A2C5C2108074F09;
v13[3] = 99338668810000;
srand(0x77);
rand();
num = rand() & 0xff;
array = (unsigned char*)v13;
for(int i = 0; i <= 29; i++)
{
key = (i % 2 !=0? num: num+3);
array[i] ^= key;
}
printf("0xGame{%s}",array);
return 0;
}
- 解法二:动态调试直接拿密钥
由于check()
只检查长度不检查内容,而且随机数种子只可能二选一,所以直接在v21最后一次赋值处设置断点。当程序运行到此处时种子一定已经被确定下来,不用关心到底用了哪个种子,我们只需要拿v21这个随机数的具体值即可。
调试可以选择IDA自带的Local Windows debugger调试器,断点处的语句运行后,查看内存中的随机数值。
脚本参考解法一。
ZzZ#
本题是Visual Studio 2022生成的Debug程序,vs生成程序的一大特征就是把函数名、变量名等符号全部拿出来放到pdb文件中。如果没有符号文件,在静态分析时就看不到这些信息。
无符号的情况下,IDA会自动把函数命名为其起始地址。start代表是整个程序的入口点,与main略有区别。
首先要想办法找到main()
。不难发现程序运行时会输出enter flag
的提示,而main()
一定会调用io函数来输出这一字符串。所以可以从字符串入手,通过字符串窗口找到相关的字符串,进行交叉引用,定位到主函数。
交叉引用快捷键:X 或 ctrl+X
之后就成功来到了主函数,反编译后分析加密逻辑:
flag内的uuid共有5段,其中首尾两段直接给出,中间三段从字符数组转unsinged int
后代入方程组验证。
接下来用Z3 Solver解方程,因为涉及位运算,所以用BitVec
这种类型,长度至少为32bit。
有的师傅在解出方程后发现结果有问题,基本上都是在数据类型上出了问题。举例来说:%4s
向(const char *)v5
这个地址写入连续的4个字节,但v5
本身是unsigned int
数组,所以这4个字节会被按小端序解析成数组的第一个unsigned int
元素,之后赋值给v10
进行方程的运算。
所以解出来的值应该是一个4字节也就是32bit的整数,之后按小端转成字节再转字符串。
from z3 import *
v13 = 3846448765
v14 = 0xD085A85201A4
v10,v11,v12 = BitVecs("v10 v11 v12",32)
s = Solver()
s.add(11 * v11 + 14 * v10 - v12 == 0x48FB41DDD)
s.add(9 * v10 - 3 * v11 + 4 * v12 == 0x2BA692AD7)
s.add(((v12 - v11) >> 1) + (v10 ^ 0x87654321) == 3451779756)
if s.check()==sat:
result = s.model()
form = "0xGame{%8x-%4s-%4s-%4s-%12x}"
args = [result[var].as_long().to_bytes(4,"little").decode() for var in (v10,v11,v12)]
print(form % (v13,args[0],args[1],args[2],v14))
Week3#
BabyASM#
一段比较精简的Intel x86汇编指令,除去数据之外大概50多行,主要考察一些常见汇编指令的功能。
同时也是提醒下不要太过于依赖IDA反编译的伪码,F5虽好但也可能出现反编译错误甚至完全用不了的情况,所以还是有必要掌握汇编的知识。
jle
向回跳转表示循环,程序的两段循环分别在L4
和L5
部分,对应两段加密。
data = [20, 92, 43, 69, 81, 73, 95, 23, 72, 22, 24, 69, 25, 27, 22, 17, 23, 29, 24, 73, 17,
24, 85, 27, 112, 76, 15, 92, 24, 1, 73, 84, 13, 81, 12, 0, 84, 73, 82, 8, 82, 81, 76, 125]
for i in range(22):
data[i] += 28
for j in range(22,43,1):
data[j] ^= data[j-22]
print(bytes(data).decode())
非预期:把文件.txt
改成.s
后缀,用Linux直接gcc编译链接成32位程序,运行出flag
LittlePuzzle#
和上周差不多的java逆向。反编译后看到一个 \(9\times 9\) 的二维数组board
,之后分析下面的check
函数可以确定是数独的逻辑。把board
提取出来整理成数独的格式,如下图:
数独本身难度不大,可以自行推理求解,也可以找数独求解器一类的工具去解。
最后应该能求出唯一的一个解(下图白色部分)
通过主函数的代码,能够分析出只需要输入(白色部分的)解即可,程序调用flag
函数输出flag。
Tea#
程序没有符号信息,但是结合上一周的经验,可以通过字符串来定位主函数。
代码量并不多,比较难受的是不能根据函数名推测功能,有耐心的话挨个函数点进去看也是可以的。
好一点的方法是,关注对明文(传入的flag)和密文进行操作的函数,从这些函数入手分析。
传入的明文是Buf1
,根据memcmp
推测绿色框中的数据是密文,中间for
循环调用的可以猜到是加密函数,红色框的数据对应密钥。
细心观察可以发现密文被用了两次,除去最后的check,前面还有一次对密文进行了逆序(sub_1400113B6
)
接下来分析加密函数,是经典的TEA加密算法,题目中也有提示。算法的具体原理可以自行搜索了解(这里贴一个b站视频【动画密码学】TEA ), 一个很显著的识别特征是0x9E3779B9
,也就是题目中的v5 -= 0x61C88647
最后有个小坑还需要说一下,Buf1
是unsigned int
数组而不是char
数组,每两次加密的数据间隔了8*4 = 0x20字节。实际上只加密了flag的最前面8个字节和最后面8个字节。中间的部分没有变化,能够看出来还是可读的十六进制明文。解密只需调用两次tea解密。
(至于为什么for循环中j<5
进行了5次加密,其实算是一个干扰项。但是Buf1
数组开的空间不够大,导致加密时会越界把栈上的一些无关数据也加密了,可能影响到一些师傅的分析思路,在这里给各位师傅们磕一个)
#include<stdio.h>
void tea_decry(unsigned int data[2], unsigned int key[4])
{
unsigned int d1 = data[0], d2 = data[1];
unsigned int delta = 0x9e3779b9, number = delta * 32;
for (int i = 0; i < 32; i++)
{
d2 -= ((d1<<4) + key[2]) ^ ((d1>>5) + key[3]) ^ (d1 + number);
d1 -= ((d2<<4) + key[0]) ^ ((d2>>5) + key[1]) ^ (d2 + number);
number -= delta;
}
data[0] = d1;
data[1] = d2;
}
void reverse(unsigned char *data,unsigned int len)
{
unsigned char *pchar = data;
unsigned char *bchar = data + len - 1;
unsigned char bufchar;
while(pchar<bchar)
{
bufchar = *pchar;
*pchar = *bchar;
*bchar = bufchar;
pchar++;
bchar--;
}
}
int main()
{
unsigned char data[] = {
0xC9, 0xB6, 0x5C, 0xCE, 0xF8, 0xEE, 0x8E, 0xA2, 0x33, 0x36,
0x34, 0x63, 0x37, 0x32, 0x36, 0x64, 0x38, 0x37, 0x65, 0x33,
0x62, 0x33, 0x63, 0x64, 0x36, 0x39, 0x64, 0x34, 0x64, 0x30,
0x62, 0x38, 0x2A, 0x7A, 0x7C, 0x3B, 0x85, 0x33, 0x6D, 0xD3};
reverse(data,40);
unsigned int key[] = {2,0,2,4};
unsigned int *u32data = (unsigned int*)data;
tea_decry(&u32data, key);
tea_decry(&u32data[8], key);
printf("%s",data);
return 0;
}
The Matrix#
本题的矩阵使用了结构体进行存储,由于IDA不能识别自定义的结构体,所以会看到有大量形式相似的赋值语句,其实是结构体元素的初始化。 由于内存对齐的原则,结构体大小为
0x10
字节,其中element_array
的指针占用最后8字节。struct Matrix_m_n{ unsigned char m unsigned char n, unsigned int* element_array }
程序混淆了几个关键的函数名,不过好在大多数函数的名称是正常的,只需要对这几个函数单独分析即可。
调用了4次hello(data,Matrix)
初始化4个key矩阵元素的值。要留意一下这几个key在栈上的顺序,后面加密时会用到。
Morpheus(Matrix,length,flag)
检查输入flag长度,之后初始化矩阵元素。
之后分析下面的加密逻辑,guess(keyMatrix,sourceMatrix,outputMatrix)
实现矩阵乘法,也就是key矩阵右乘flag明文的矩阵。
根据线性代数知识,\(KA =B\) 这一矩阵方程的解为 \(A = K^{-1}B\)
利用numpy可以比较方便的求出逆矩阵(但是求出来的有点精度误差,需要手动修正下,题目给的key矩阵求逆后元素依然都是整数,所以四舍五入一下),进而还原加密前的矩阵,得到flag。
import numpy as np
k1=[1,3,1,0,2,1,1,2,0]
k2=[1,2,2,3,5,6,0,2,1]
k3=[0,4,3,1,2,1,2,3,1]
k4=[1,2,3,3,5,6,1,4,10]
data = [
0x1e8,0x1c0,0x181,0x557,0x4d3,0x41e,0x13d,0x111,0x102,
0x26c,0x140,0x145,0x5b7,0x2ec,0x2f3,0x5e9,0x31d,0x336,
0x14d,0x10a,0x192,0xbd,0x9f,0xf5,0xbd,0xa1,0x101,
0x162,0x147,0x223,0xfb,0xc0,0x126,0x191,0x123,0x1b7,
0xf0,0xfd,0x10d,0x29e,0x2c0,0x2f1,0x91,0x9f,0xa4,
0x229,0x13b,0x12e,0x4e4,0x2d8,0x2c7,0x5bd,0x325,0x2e4,
0x1c7,0x151,0xd5,0xfe,0xe5,0x6e,0x12c,0xa0,0x9e]
# 注意key矩阵的顺序
keyMat = [np.array(var).reshape(3,3) for var in (k2,k4,k1,k3)]
dataMat = [np.array(data[i*9:(i+1)*9]).reshape(3,3) for i in range(len(data)//9)]
flag = []
for i in range(7):
inv = np.linalg.inv(keyMat[i%4])
arr = np.matmul(inv,dataMat[i]).flatten().tolist()
if i < 6:
arr = arr[:6] # 相邻矩阵的元素重叠
flag.extend([round(num) for num in arr])
print(bytes(flag).decode())
FirstSight-Android (Mobile)#
安卓apk反编译,比较好用的工具是GDA和jadx,更推荐后者。
程序位置在com.example.ezandroid
,不难发现有一个Base62编码类,主活动调用了其中的编码函数,然后和secret
字符串进行比较。
Android程序的各种资源都被保存到xml文件中,字符串也不例外,所以secret字符串的具体内容要去value/string.xml
查看。可以在jadx中搜索资源中的文本。
之后拿去解码即可。
Week4#
PyPro#
根据题目提示推测本题的程序由python打包而来,先 pyinstaller 解包(这一步也可以参考后面Misc)
解包结果可以看到pyc版本:3.12,很不幸 uncompyle6 不支持反编译3.9版本以上的 pyc,这里可以用 pycdc。( https://github.com/zrax/pycdc,官方版本是源代码,需要手动cmake一下,pycdc可能遇到反编译了一半之后报错罢工的情况,参照 Issue #307 改一下pycdc源码之后重新编译即可解决,当然也可以用pycdas看字节码)
PyLingual这个网站也支持反编译3.12版本,效果还不错。
贴个源码:
import base64
from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes
key = 0x554b134a029de539438bd18604bf114
def PKCS5_pad(data):
if len(data)<48:
length = 48 - len(data)
return data.ljust(48,length.to_bytes())
else:
return None
def main():
enc = input("在这里输入你的flag:\n").encode("utf-8")
if len(enc)!=44:
print("length error!")
for i in range(len(enc)):
exit(123)
elif enc[6] != ord("{") or enc[-1] != ord("}"):
print("format error")
exit(1)
chiper = AES.new(long_to_bytes(key),AES.MODE_ECB)
enc = chiper.encrypt(PKCS5_pad(enc))
result = base64.b64encode(enc)
data = '2e8Ugcv8lKVhL3gkv3grJGNE3UqkjlvKqCgJSGRNHHEk98Kd0wv6s60GpAUsU+8Q'
if result.decode() == data:
print("flag正确")
else:
print("错误")
return None
if __name__ == "__main__":
main()
先AES之后Base64,解密就不难了。从反编译结果直接抄点代码下来,或者Cyberchef一把梭。
import base64
from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes
key = 0x554b134a029de539438bd18604bf114
data = '2e8Ugcv8lKVhL3gkv3grJGNE3UqkjlvKqCgJSGRNHHEk98Kd0wv6s60GpAUsU+8Q'
enc = base64.b64decode(data)
chiper = AES.new(long_to_bytes(key),AES.MODE_ECB)
flag = chiper.decrypt(enc)
print(flag)
MineSweeper#
Unity游戏题,选择C#反编译工具DnSpy或者ILSpy。一般情况下(没有IL2CPP),游戏代码编译后保存在Assembly-CSharp.dll
文件中,预期是对这个文件进行逆向。
题目解法比较开放,有师傅直接硬扫70个雷通关的,但是难度太大不推荐硬玩。
先说常规思路:静态分析
- 找到
Game
类,分析Update
函数,发现通关后会调用crypt
函数解密出flag然后输出。 - 继续分析
crypt
,函数导入了Resources/enc.bytes
的前44字节作为密文。这个文件用AssetStudio
可以导出,之后用十六进制编辑器查看。 - 接下来的解密部分有点类似RC4,
Game.key
先打乱传入的参数Key
也就是Game.haha
,得到的结果作为真正的密钥进行异或。 - 仿照
crypt
函数搓个解密脚本。
更推荐的方法:patch修改游戏逻辑。DnSpy中可以编辑反编译的代码,只要想办法触发通关条件就能拿flag,修改后记得重新编译运行。
- 最直接的改法,把开始时的雷数改成0,点两下就通关。或者
this.gamewin
直接改成true
wincheck
函数中修改语句win = true
- 改
Update
函数对this.gamewin
的判断,对条件取反。
当然改法还有很多,甚至可以重写整个函数,在刚进入游戏的时候就弹出flag。
Register#
一个简易的crackme,需要逆向破解序列号保护。
打开运行,发现要输入用户名和对应的序列号。用户名只能是题目描述中提到的0xGameUser
,否则警告用户名错误,这也是为了避免多解。
使用DIE进一步查信息,32位GUI
运行时注意观察字符串信息,之后在IDA通过字符串定位关键函数
- 如果交叉引用主窗口标题 “0xCrackMe”,定位到
WinMain
,之后找回调函数lpfnWndProc
- 交叉引用子窗口的相关字符串直接定位到回调函数
sub_411B10
在整个逆向过程中会碰到很多windows API函数,可以去Microsoft官方文档搜索查询。
进入sub_4112DA
继续分析,v9这里是对时间戳右移28位,结合动态调试也能发现v9
是一个固定值。之后先对用户名异或,再调用sub_1011041
生成SHA1哈希值。
SHA1的十六进制值套上0xGame{}
就是flag
Tea2.0#
同样是32位程序,反编译后定位主函数。代码只有短短几行,逻辑还是比较清晰的。
sub_41116D
是加密函数,分析一下发现和上周的TEA很像,通过网络搜索或者问AI能够分析出是XTEA加密。不过要注意轮数改成了64轮。
之后按常规思路就是拿unk_41A020
密文进行解密,不出意外会得到一串乱码)
第四周的题目当然不会白送分。为了找到问题出在哪里,借助动态调试对加密过程和密文密钥进行检查,最后发现密文和静态分析时看到的硬编码的数据不同。把这段新的密文dump下来拿去解密才会得到flag。
如果纯静态做要麻烦的多。交叉引用unk_41A020
,可以看到TlsCallback_1_0
函数修改了密文(注意Tlscallback
部分的代码先于主函数执行),具体原理是对硬编码的密文进行了一次TEA加密,其密钥又经过TlsCallback_0_0
修改。理清这些后可以写出如下脚本:
#include<stdio.h>
void tea_encry(unsigned int* data, unsigned int* key)
{
unsigned int d1 = data[0], d2 = data[1];
unsigned int delta = 0x9e3779b9, sum = 0;
for (int i = 0; i < 32; i++)
{
sum += delta;
d1 += ((d2 << 4) + key[0]) ^ ((d2 >> 5) + key[1]) ^ (d2 + sum);
d2 += ((d1 << 4) + key[2]) ^ ((d1 >> 5) + key[3]) ^ (d1 + sum);
}
data[0] = d1;
data[1] = d2;
}
void xtea_decry(unsigned int *data, unsigned int *key)
{
int round = 64;
unsigned int d1 = data[0], d2 = data[1];
unsigned int delta = 0x9e3779b9, number = delta * round;
for (int i = 0; i < round; i++)
{
d2 -= ( ((d1<<4) ^ (d1>>5)) + d1) ^ (number + key[(number>>11) & 3]);
number -= delta;
d1 -= ( ((d2<<4) ^ (d2>>5)) + d2) ^ (number + key[number & 3]);
}
data[0] = d1;
data[1] = d2;
}
int main()
{
unsigned int data[] ={
0x18dc360,0xd5835457,0x8bee2dcb,0x92bb2dee,0xfdf4ad54,0x43f8c2d,
0x61a232a9,0xf15f4d1,0x16ea4979,0x7c2bf6da,0xdcd5fa32,0x76450819 };
unsigned int TEAkey[] = { 0x1245,0x3298,0x4756,0x1463 };
unsigned int XTEAkey[] = { 0x4512,0x9832,0x5647,0x6314 };
for (int i = 0; i < 4; i++) {
TEAkey[i] ^= 0xABCD;
}
for(int j = 0; j < 6; j++)
{
tea_encry(data+2*j, TEAkey);
}
for (int k = 0; k < 6; k++)
{
xtea_decry(data+2*k, XTEAkey);
}
printf("%s",(char*)data);
return 0;
}
JustSoSo (Mobile)#
jadx反编译后能看到Java层有一个ReversC4
类,简单分析下可以看出是修改过的RC4加密。只是在生成密钥流(KSA)的过程中把初始的S-box倒序排列了一下。
之后来到MainActivity
,发现加载了外部库secret
,并且声明了native层的方法getKey()
。
后面调用了ReversC4.encrypt(flagInput.getBytes(), getKey())
,由此可以推测RC4的密钥要从getKey()
得到。这个函数的实现并不在java层,而是之前提到的secret
库中。也就是lib
目录下的 libsecret.so
文件。
分析so动态库,arm
和x86/64
架构任选其一分析即可,后者相对简单一些。IDA打开后一眼看到要找的函数Java_com_ctf_justsoso_MainActivity_getKey
。
这里的xmm是Intel SSE指令集的寄存器,可以存放128bit的数据类型__m128i
,事实上这个数据类型是由若干个基本类型打包而来。比如本题的_mm_cvtepu8_epi32
函数先把4个uint8
拓展为4个uint32
(高位用0填充),再把这4个uint32
打包成一个m128i
。xmmword
也是一样的道理,做题时完全可以把它当成长度为4的uint数组。
理解了这一点再去看伪代码就比较清楚了,source
中每个字符乘2再异或0x7F
得到密钥,之后回到java层RC4解密即可。