江苏省赛2021题解

2021领航杯APK逆向

没见过这么急促、简陋的比赛

言归正传

本题反思:

反码,补码这些要会算,不能只会正数,负数也要会。

基础不牢啊,这道题如果算对了-16的二进制码就出了,300分可惜可惜

题目简单,但还是要以此为戒增强基础

一定要明白运算的含义,不能马虎

知识提要:

APK文件要了解的基础

  • 每一个apk文件都是可以解压的

解压之后一般会出现以下文件(纯APK,无添加别的引擎)

AndroidManifest.xml

该文件是每个应用都必须定义和包含的,它描述了应用的名字、版本、权限、引用的库文件等等信息

META-INF目录

1.META-INF目录下存放的是签名信息,用来保证apk包的完整性和系统的安全

2.保证了apk包里的文件不能被随意替换。如果想要替换里面的一幅图片,一段代码, 或一段版权信息,想直接解压缩、替换再重新打包,基本是不可能的。

3.软件修改后需要将里面的证书文件删除(.RSA、.SF、.MF三个文件)再重新签名,否则软件无法安装

res目录

1.res目录存放资源文件。包括图片,字符串等等。

2.res文件夹里存放的大部分是软件所需的资源及布局文件(drawable存放资源、layout、xml存放布局文件.xml),部分需要汉化的单词、语句会在这些.xml文件里

lib目录

存放一些so文件,有的可能没有

assets目录

存放一些配置文件,这些文件的内容在程序运行过程中可以通过相关的API获得

classes.dex文件

classes.dex是java源码编译后生成的java字节码文件(比如这个题就可以直接分析这个文件)

resources.arsc

编译后的二进制资源文件。resources.arsc文件是编译后的资源文件,大多数情况下,需要汉化的单词、语句绝大多数都在这个文件里,汉化的时候首先就要看这个文件。

解题思路

这类简单apk首先就需要找到MainActive函数,这里我直接将class.dex文件用jadx打开了,不要管androidx开头的文件和google开头的文件,找最不一样的、出现次数最少的文件

image-20210901225721143

这里轻松获得源码

看到最下面,找到关键判断

1
2
3
4
5
6
if (check.m39check_final_GBYM_sE(r1)) 
{
Toast.makeText(this.this$0.this$0.getApplicationContext(),"Rightflag!",0).show();
} else
{
Toast.makeText(this.this$0.this$0.getApplicationContext(), "Wrong flag!", 0).show() }

这里需要知道参数的含义

判断关键的“r1”就是由函数

1
check.m40enc_Fz0kQmc(substring, r4, r1);

得到的

该函数的三个参数很分析得到就是

输入的flag中间的内容;从一个文件中读取的几个字符;一个空字符

跳到这个函数的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final void m40enc_Fz0kQmc(String str, byte[] bArr, byte[] bArr2) {
Intrinsics.checkNotNullParameter(str, "input");
Intrinsics.checkNotNullParameter(bArr, "key");
Intrinsics.checkNotNullParameter(bArr2, "enc");
byte[] r0 = UByteArray.m107constructorimpl(36);
for (int i = 0; i <= 35; i++) {
UByteArray.m118setVurrAj0(r0, i, UByte.m64constructorimpl((byte) (str.charAt(i) ^ i)));
}
for (int i2 = 0; i2 <= 35; i2++) {
UByteArray.m118setVurrAj0(bArr2, i2, UByte.m64constructorimpl((byte) (UByteArray.m113getw2LRezQ(r0, 35 - i2) ^ UByteArray.m113getw2LRezQ(bArr, i2 % 16))));
}
for (int i3 = 0; i3 <= 35; i3++) {
byte b = UByteArray.m113getw2LRezQ(bArr2, i3);
UByteArray.m118setVurrAj0(bArr2, i3, UByte.m64constructorimpl((byte) UnsignedUtils.m361uintDivideJ1ME1BU(UInt.m132constructorimpl(UByte.m64constructorimpl((byte) (UByteArray.m113getw2LRezQ(bArr2, i3) & -16)) & UByte.MAX_VALUE), 16)));
UByteArray.m118setVurrAj0(bArr2, i3 + 36, UByte.m64constructorimpl((byte) (b & 15)));
}
}


看到主函数判断的函数

1
2
3
4
5
6
7
8
9
10
public final boolean m39check_final_GBYM_sE(byte[] bArr) {
Intrinsics.checkNotNullParameter(bArr, "enc");
for (int i = 0; i <= 71; i++) {
if ("abcdefgh13462579".charAt(UByteArray.m113getw2LRezQ(bArr, i) & UByte.MAX_VALUE) != "ccccebeebbeafbeeeabefabfaffffafaafaaea4b292he31922g6d54a62hchf2bb9ehagdc".charAt(i)) {
return false;
}
}
return true;
}

根据这个函数,可以直接求出r1的值(判断里的参数),但是这里需要注意的是,jadx不会对参数进行重命名,所以会出现参数名重复的现象,大坑

1
2
3
4
5
6
7
8
9
10
11
12
string_2="ccccebeebbeafbeeeabefabfaffffafaafaaea4b292he31922g6d54a62hchf2bb9ehagdc"
string_1="abcdefgh13462579"
bArr='123456789getflag'

barr2=[]
for i in range(len(string_2)):
barr2.append((string_1.index(string_2[i]))&255)
barr2_0=[]
print(barr2)
# print(barr2_0)
for i in range(36):
barr2_0.append(barr2[i]*16+barr2[i+36])

得到r1,也就是上面函数bArr2参数

这样就可以求出flag了,三个循环,第一个第二个没啥好分析的,直接逆向就行

1
2
3
4
5
6
7
8
9
#前两个循环
r0=[0 for i in range(36)]
for i in range(36):
r0[35-i]=barr2_0[i]^ord(bArr[i%16])
print(r0)

flag=[]
for i in range(36):
flag.append(chr(r0[i]^i))

看到第三个循环

1
2
3
4
5
6
7
8
9
10
for (int i3 = 0; i3 <= 35; i3++) {
byte b = UByteArray.m113getw2LRezQ(bArr2, i3);
#这里首先把数组里的i位保存给b了
UByteArray.m118setVurrAj0(bArr2, i3, UByte.m64constructorimpl((byte) UnsignedUtils.m361uintDivideJ1ME1BU(UInt.m132constructorimpl(UByte.m64constructorimpl((byte) (UByteArray.m113getw2LRezQ(bArr2, i3) & -16)) & UByte.MAX_VALUE), 16)));
#这里可以简化的看成i位&-16之后又÷16
#-16的二进制表示是:1111 1111 0000,这里可以看作是保留了这个第i位的前4位
UByteArray.m118setVurrAj0(bArr2, i3 + 36, UByte.m64constructorimpl((byte) (b & 15)));
#15的二进制是:1111,这就是保留后四位
#总体来看就是将第i位拆开来算了,大坑啊我日
}

这段加密的大体意思就是:

1
2
3
4
5
mov EAX, [ESP+I]
and [ESP+I], FFFFFFF0
sal [ESP+I], 4
and EAX, 1111
mov [ESP+I+0x24], EAX

看似对对称的两个数进行操作,实际上还是一个数,给出脚本

1
2
for i in range(36):
barr2_0.append(barr2[i]*16+barr2[i+36])

组合起来就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
string_2="ccccebeebbeafbeeeabefabfaffffafaafaaea4b292he31922g6d54a62hchf2bb9ehagdc"
string_1="abcdefgh13462579"
bArr='123456789getflag'

barr2=[]
for i in range(len(string_2)):
barr2.append((string_1.index(string_2[i]))&255)
barr2_0=[]
print(barr2)
# print(barr2_0)
for i in range(36):
barr2_0.append(barr2[i]*16+barr2[i+36])

r0=[0 for i in range(36)]
for i in range(36):
r0[35-i]=barr2_0[i]^ord(bArr[i%16])
print(r0)

flag=[]
for i in range(36):
flag.append(chr(r0[i]^i))
print(''.join(flag))

APK例外

也不算是例外,只是我觉得是例外,我做的题太少了……

apk游戏,buu的PixelShooter

这是一个由unity引擎支持的一个打飞机游戏,他的flag存在于

assets\bin\Data\Managed目录下的

Assembly-CSharp.dll文件中