攻防世界 Android逆向合集

记录一下在攻防世界Mobile方向中做过的题。

1. app1(版本号)

这道题的一个知识点就是程序自身的版本号、版本名在BuildConfig.class中存储。这里的版本号、版本名与我们在应用程序上看到的版本号、版本名不是同一个东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package v5le0n9;
public class demo {
public static void main(String[] args){
String v3 = "X<cP[?PHNB<P?aj";
int len = v3.length();
int v4 = 15;
int v0;
int[] v5 = new int[len];
for(v0=0; v0<len; v0++)
{
v5[v0] = v3.charAt(v0) ^ v4;
System.out.printf("%c",v5[v0]);
}
}
}
/**
W3l_T0_GAM3_0ne
*/

另外,版本号、版本名还能通过以下方式查看:

2. app2(广播接收器)

程序一进去就到了MainActivity,输入一点东西后点击登陆去到SecondActivity。用jadx查看一下这两个Activity都做了什么。

在SencondActivity中又启动了MoniterInstallService。

有点混乱了,可以先求出口令和用户名试试。将x86目录下的so文件载入IDA32,在Exports表里找到doRawData()函数,查看伪代码。

python3解决from Crypto.Cipher import AES报错问题

1
2
3
4
5
6
7
8
9
10
import base64
from Crypto.Cipher import AES
cipher=base64.b64decode("VEIzd/V2UPYNdn/bxH3Xig==")
key = "thisisatestkey==".encode("utf-8")
aes = AES.new(key,AES.MODE_ECB)
msg = aes.decrypt(cipher)
print(msg.decode("utf-8"))
'''
aimagetencent
'''

发现这个也不是flag。当我们把用户名tencent,口令aimage输入进去,点击登陆发现崩溃了。查看一下XML文件,还有FileDataActivity我们还没有分析。

1
2
3
4
5
6
7
8
9
10
import base64
from Crypto.Cipher import AES
cipher=base64.b64decode("9YuQ2dk8CSaCe7DTAmaqAA==")
key = "thisisatestkey==".encode("utf-8")
aes = AES.new(key,AES.MODE_ECB)
msg = aes.decrypt(cipher)
print(msg.decode("utf-8"))
'''
Cas3_0f_A_CAK3
'''

其实还有更简便的方法,直接去到FileDataActivity页面查看。

这道题就没想着要我们跟着代码分析App去到FileDataActivity,不过也算收获到一点知识,显式Intent和隐式Intent,广播自定义意图动静态注册广播接收器

3. app3(备份文件ab)

下载下来是一个.ab文件,.ab文件是 Android 系统的备份文件格式,它分为加密和未加密两种类型。.ab文件的前 24 个字节是类似文件头的东西,如果是加密的,在前 24 个字节中会有 AES-256 的标志,如果未加密,则在前 24 个字节中会有 none 的标志。安卓备份文件.AB的解包方法

下载abe.jar,将.ab文件解压。在abe目录下运行cmd。

1
java -jar abe.jar unpack C:\Users\dell\Desktop\app3.ab app3.tar

.tar文件在Windows也可解压。找到.apk文件安装到模拟器,又运行不了。

因为没有跳转语句,无论输入什么都是出来这句话。再看看有什么隐藏的<activity>,发现也只有我们分析过的那两个。

再去那两个<activity>仔细分析我们刚才没有分析的,肯定是有什么遗漏了。

SHA1=”Stra1234” + “44e2e4457d4e252ca5b9fe9d20b3fea5” + “yaphetshan”= ae56f99638285eb0743d8bf76d2b0c80e5cbb096,取前7位就是ae56f99。

我们刚才说的有加密,是用SqlCipher加密的,而这个就是数据库的密码。

解压包除.apk文件之外,还有两个.db文件,一个Demo.db,另一个是Encrypt.db。两个都试试。

安装SQLite数据库用来解密数据库,上面分析写着版本为3.4.0,Linux注意对应版本,Windows下载最新也就3.0.1,所以没问题。

两个加密的.db文件使用SQLCipher打开。发现Encryto.db中有东西。

很明显的Base64,拿去解码得:

1
2
3
VGN0ZntIM2xsMF9Eb19ZMHVfTG92M19UZW5jM250IX0=

Tctf{H3ll0_Do_Y0u_Lov3_Tenc3nt!}

4. easy_apk(变种base64)

载入AK没发现lib文件,载入jeb查看源码:

天真的我就拿去Base64解码了,结果解码失败。回来再看,它是新Base64,点进去查看它的算法,发现它是把索引表给替换了。

拿Base64变种脚本替换索引表即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import base64
import string
# base 字符集
# base64_charset = string.ascii_uppercase + string.ascii_lowercase + string.digits + '+/'
base64_charset = "vwxrstuopq34567ABCDEFGHIJyz012PQRSTKLMNOZabcdUVWXYefghijklmn89+/"

def encode(origin_bytes):

# 将每⼀位bytes转换为⼆进制字符串,用bin转换后是0b开头的,所以把0b替换了,首位补0补齐8位
base64_bytes = ['{:0>8}'.format(str(bin(b)).replace('0b', '')) for b in origin_bytes]

resp = ''
nums = len(base64_bytes) // 3
remain = len(base64_bytes) % 3
integral_part = base64_bytes[0:3 * nums]

while integral_part:
# 取三个字节,以每6⽐特,转换为4个整数
tmp_unit = ''.join(integral_part[0:3])
tmp_unit = [int(tmp_unit[x: x + 6], 2) for x in [0, 6, 12, 18]]
# 取对应base64字符
resp += ''.join([base64_charset[i] for i in tmp_unit])
integral_part = integral_part[3:]

if remain:
# 补⻬三个字节,每个字节补充 0000 0000
remain_part = ''.join(base64_bytes[3 * nums:]) + (3 - remain) * '0' * 8
# 取三个字节,以每6⽐特,转换为4个整数
# 剩余1字节可构造2个base64字符,补充==;剩余2字节可构造3个base64字符,补充=
tmp_unit = [int(remain_part[x: x + 6], 2) for x in [0, 6, 12, 18]][:remain + 1]
resp += ''.join([base64_charset[i] for i in tmp_unit]) + (3 - remain) * '='

return resp

def decode(base64_str):
if not valid_base64_str(base64_str):
return bytearray()

base64_bytes = ['{:0>6}'.format(str(bin(base64_charset.index(s))).replace('0b','')) for s in base64_str if s != '=']
resp = bytearray()
nums = len(base64_bytes) // 4
remain = len(base64_bytes) % 4
integral_part = base64_bytes[0:4 * nums]

while integral_part:
# 取4个6位base64字符,作为3个字节
tmp_unit = ''.join(integral_part[0:4])
tmp_unit = [int(tmp_unit[x: x + 8], 2) for x in [0, 8, 16]]
for i in tmp_unit:
resp.append(i)
integral_part = integral_part[4:]

if remain:
remain_part = ''.join(base64_bytes[nums * 4:])
tmp_unit = [int(remain_part[i * 8:(i + 1) * 8], 2) for i in range(remain - 1)]
for i in tmp_unit:
resp.append(i)
return resp

def valid_base64_str(b_str):
if len(b_str) % 4:
return False
for m in b_str:
if m != "=" and m not in base64_charset:
return False
return True

if __name__ == '__main__':
local_base64 = "5rFf7E2K6rqN7Hpiyush7E6S5fJg6rsi5NBf6NGT5rs="
print('使用本地base64解密:', decode(local_base64).decode())
'''
使用本地base64解密: 05397c42f9b6da593a3644162d36eb01
'''
1
flag{05397c42f9b6da593a3644162d36eb01}

5. RememberOther(MD5)

运行一下程序。

拿去AK,没有看到需要动态调试的so文件。

拿去jeb分析代码:

那我们试试直接点注册。

MD5的奇数位为b216ebb92fa5caf6,再将MD5值解密,解出来为YOU_KNOW_(究竟哪个网站可以免费解MD5,我还是看牛牛们的wp才知道是这个答案)。拿到程序去验证没错。YOU_KNOW_很明显是flag形式,后面还缺了些东西,出题人给了剩下的线索。

你懂!你懂安卓!

1
YOU_KNOW_ANDROID

6. easyjni(算法分析&变种base64)

这道题看名字就知道考察jni。

载入AK,看到lib目录下有libnative.so文件,是armeabi-v7a架构的。载入jeb,代码混淆了,没关系一个个方法分析。

在判断语句,调用了MainActivity.a方法,其中一个实参就是我们输入的内容。MainActivity.a方法返回到私有a方法里,形参是我们输入的内容。再看私有a方法,调用了ncheck()方法。

调用ncheck()方法前,还创建了一个a类对象,先进去看看a类的a方法。发现是变种Base64加密。

将apk文件解包,将里面的so文件载入IDA。

1
2
3
4
5
6
7
8
9
10
11
12
C:\Users\dell\Desktop>apktool d easyjni.apk
I: Using Apktool 2.6.1 on easyjni.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: C:\Users\dell\AppData\Local\apktool\framework\1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...

在Export模块找到ncheck()方法。

哇这就有点恶心人了哈,我从没见过等号在中间的。仔细分析一下IDA源码,原来不过如此。

1
2
3
4
5
6
7
MbT3sQgX039i3g==AQOoMQFPskB1Bsc7

# i和i+1对换
bM3TQsXg30i9g3==QAoOQMPFks1BsB7c

# 前16位与后16位对换
QAoOQMPFks1BsB7cbM3TQsXg30i9g3==

再用4中的变种Base64脚本解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import base64
import string
# base 字符集
# base64_charset = string.ascii_uppercase + string.ascii_lowercase + string.digits + '+/'
base64_charset = "i5jLW7S0GX6uf1cv3ny4q8es2Q+bdkYgKOIT/tAxUrFlVPzhmow9BHCMDpEaJRZN"

def encode(origin_bytes):

# 将每⼀位bytes转换为⼆进制字符串,用bin转换后是0b开头的,所以把0b替换了,首位补0补齐8位
base64_bytes = ['{:0>8}'.format(str(bin(b)).replace('0b', '')) for b in origin_bytes]

resp = ''
nums = len(base64_bytes) // 3
remain = len(base64_bytes) % 3
integral_part = base64_bytes[0:3 * nums]

while integral_part:
# 取三个字节,以每6⽐特,转换为4个整数
tmp_unit = ''.join(integral_part[0:3])
tmp_unit = [int(tmp_unit[x: x + 6], 2) for x in [0, 6, 12, 18]]
# 取对应base64字符
resp += ''.join([base64_charset[i] for i in tmp_unit])
integral_part = integral_part[3:]

if remain:
# 补⻬三个字节,每个字节补充 0000 0000
remain_part = ''.join(base64_bytes[3 * nums:]) + (3 - remain) * '0' * 8
# 取三个字节,以每6⽐特,转换为4个整数
# 剩余1字节可构造2个base64字符,补充==;剩余2字节可构造3个base64字符,补充=
tmp_unit = [int(remain_part[x: x + 6], 2) for x in [0, 6, 12, 18]][:remain + 1]
resp += ''.join([base64_charset[i] for i in tmp_unit]) + (3 - remain) * '='

return resp

def decode(base64_str):
if not valid_base64_str(base64_str):
return bytearray()

base64_bytes = ['{:0>6}'.format(str(bin(base64_charset.index(s))).replace('0b','')) for s in base64_str if s != '=']
resp = bytearray()
nums = len(base64_bytes) // 4
remain = len(base64_bytes) % 4
integral_part = base64_bytes[0:4 * nums]

while integral_part:
# 取4个6位base64字符,作为3个字节
tmp_unit = ''.join(integral_part[0:4])
tmp_unit = [int(tmp_unit[x: x + 8], 2) for x in [0, 8, 16]]
for i in tmp_unit:
resp.append(i)
integral_part = integral_part[4:]

if remain:
remain_part = ''.join(base64_bytes[nums * 4:])
tmp_unit = [int(remain_part[i * 8:(i + 1) * 8], 2) for i in range(remain - 1)]
for i in tmp_unit:
resp.append(i)
return resp

def valid_base64_str(b_str):
if len(b_str) % 4:
return False
for m in b_str:
if m != "=" and m not in base64_charset:
return False
return True

if __name__ == '__main__':
local_base64 = "QAoOQMPFks1BsB7cbM3TQsXg30i9g3=="
print('使用本地base64解密:', decode(local_base64).decode())
'''
使用本地base64解密: flag{just_ANot#er_@p3}
'''

7. easy-so(算法分析)

运行程序。

看题目,肯定有so文件,载入AK发现4种架构都有,可以快乐地玩耍了。

载入jeb分析源码。

我们需要做的就是将CheckString方法的返回值为1。解包,用x86架构的so文件载入IDA。因为我用的模拟器是x86架构的。

这个与6的题目考点几乎一模一样,只是没了变种Base64的过程。

1
2
3
4
5
6
7
f72c5a36569418a20907b55be5bf95ad

# i与i+1对换
7fc2a5636549812a90705bb55efb59da

# 前16位与后16位对换
90705bb55efb59da7fc2a5636549812a

拿去程序里运行验证一下,验证通过。

1
flag{90705bb55efb59da7fc2a5636549812a}

8. easyjava(算法分析)

运行一下程序。

载入jeb分析:

将b类和a类所有方法都分析一遍,反正后面也要用到。

回到MainActivity类,b.b和a.a的作用一样,都是以参数为边界,对换各自的c整形列表,所以v4和v5都应该是对换后的列表。经过分析a类和b类发现并没有增加或减少原本的长度,所以v2的长度就应该等于v3的长度,v3的长度从字符串中可以计算到是12,所以循环要经历12次。

以v2索引值为0的值为例,将v2索引值为0的值传入b.a方法,如果这个值可以转换成小写字母,则把b.b小写字母表中相同的小写字母的索引值取出,与a列表(也就是v4)的数组元素对比,如果取出的索引值与a列表中某个元素相同,则返回元素的索引值。更新b.a列表和b.b小写字母表,都是首位放最末位。将返回的a列表元素的索引值作为参数传入a.a方法,如果这个形参与a列表中(也就是v5)的元素相同,取出这个元素的索引值,作为a.b小写字母表中的索引,返回该索引的元素,也就是“w”。

用Python写出来相当于a_alphabet[v5.index(v4.index(b_alphabet.index(ans[0])))] = w

现在我们要做的就是把这个过程逆回来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
v4 = [17,23,7,22,1,16,6,9,21,0,15,5,10,18,2,24,4,11,3,14,19,12,20,13,8,25]
v5 = [21,4,24,25,20,5,15,9,17,6,13,3,18,12,10,19,0,22,2,11,23,1,8,7,14,16]
flag = "wigwrkaugala"
ans = ""
b_alphabet = ['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']
a_alphabet = ['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']
for i in flag:
halflag = a_alphabet.index(i)
ans += b_alphabet[v4[v5[halflag]]]
temp = v4[0]
v4.pop(0)
v4.append(temp)
temp = b_alphabet[0]
b_alphabet.pop(0)
b_alphabet.append(temp)
print(ans)
'''
venividivkcr
'''

嘤嘤嘤终于成功了,在做的时候看漏了很多细节,所以正确的flag一直出不来,还是要耐心一点!

1
flag{venividivkcr}

9. Ph0en1x-100(动态调试so)

载入AK,看到有lib文件。载入jeb分析源码。

而getFlag和encrypt都是Native方法,需要在so文件查看详细内容。将程序解包,拿x86目录下的so文件载入IDA,查看getFlag和encrypt方法。

encrypt方法就是将输入的字符串中的每个字符的ASCII码都减1。

getFlag方法看得就有点懵逼了,如果静态分析实在困难的话,可以考虑动态调试。

动态调试需要满足android:debuggable="true",没有就要在AK中编辑添加,重新编译签名。

动态调试方法可看 https://v5le0n9.github.io/posts/15be101a.html?highlight=and#9-IDA%E5%8A%A8%E6%80%81%E7%A0%B4%E8%A7%A3%E7%99%BB%E5%BD%95%E9%AA%8C%E8%AF%81 我就在这偷下懒了。

刚才静态分析中,很明显看到一个循环,那我们就在循环的下一句下断点,在程序中输入字符串点击按钮,IDA停在断点处。此时,查看寄存器窗口EDI的值,点击小箭头,跟随地址。

到了这里还没行,循环解决了但getFlag方法还未结束。F8步过观察字符串的变化。运行到快要结束时字符串已经不再变化了。在F8步过的过程中,发现.变成了e。所以正确的getFlag的返回值应该为:

1
ek`fz@q2^x/t^fn0mF^6/^rb`qanqntfg^E`hq|

所以我们输入的字符串中每个字符-1就是上面这个字符串。编写脚本:

1
2
3
4
5
6
7
8
flag = "ek`fz@q2^x/t^fn0mF^6/^rb`qanqntfg^E`hq|"
ans = ""
for i in flag:
ans += chr(ord(i) + 1)
print(ans)
'''
flag{Ar3_y0u_go1nG_70_scarborough_Fair}
'''

10. 黑客精神(JNI_OnLoad)

运行程序,注册码保存后直接退出。

载入AK,有so文件,因为没有x86目录,如果要动态调试的话就要开我那个慢得要死的AS原生模拟器了。

载入jeb,分析源码:

看到有Log语句,赶紧打开AS连接上模拟器运行一下看看Log。程序一进去就显示m=0,然后再m=Xman。

说明程序在打开窗口前就已经载入so文件读取m的值,但它是静态的,如果我们注册过的话,程序会读取m=1,不会让我们再次注册。so文件的onCreate方法中,第一句就是初始化SN,所以m的值应该是在native方法initSN中存着。

将so文件载入IDA,在导出表里找不到initSN方法,也找不到所有native方法,但看到了JNI_Onload,说明整个过程都是在JNI_Onload里动态完成的。

点进JNI_Onload函数,查看源码:

所以EoPAoY62@ElRD应该就是注册码,但输入发现不对…Logcat窗口还是显示m=0。

那继续分析saveSN。

现在可以捋一下思路了,W3_arE_whO_we_ARE是要加密的字符串,EoPAoY62@ElRD相当于密钥,作为参数传入saveSN。v7存着的就是密钥。

把这个加密算法用Python写一遍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
flag = "W3_arE_whO_we_ARE"
flagarr = list(flag)
key = "EoPAoY62@ElRD"
keyarr = list(key)
ans = ""
v9 = 2016
for i in range(13):
if (i%3 == 1):
v9 = (v9 + 5) % 16
v11 = flagarr[v9 + 1]
elif i%3 == 2:
v9 = (v9 + 7) % 15
v11 = flagarr[v9 + 2]
else:
v9 = (v9 + 3) % 13
v11 = flagarr[v9 + 3]
ans += chr(ord(keyarr[i]) ^ ord(v11))
print(ans)
'''
201608Am!2333
'''

最后一个work方法,查看它的内存,里面包含着flag的格式。

1
xman{201608Am!2333}

11. APK逆向(算法分析)

同Bugku的mobile1

12. 人民的名义-抓捕赵德汉1-200(jar包)

下载下来的是jar包,使用命令jar -xvf 1-200.jar解压jar包。

1
2
3
4
5
6
7
8
C:\Users\dell\Desktop>jar -xvf 1-200.jar
已解压: META-INF/MANIFEST.MF
已解压: CheckPassword.class
已解压: CheckInterface.class
已解压: ClassEnc
已解压: .project
已解压: .classpath
已解压: newClassName.class

在AS或eclipse中创建一个项目,将jar包中的.class文件放到项目的class目录下即可查看源码。(jar包可直接用jadx或jd-gui工具查看源码)

这个MD5解出来是monkey99,拿去试了一下,发现flag就是这个。

1
flag{monkey99}

13. 基础android(广播接收器)

载入AK没什么发现,载入jeb分析源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
import string
base = string.ascii_uppercase + string.ascii_lowercase + string.digits + "-+_<>?/\|{}()!@#$%^&*~`"
len = 12
ans = ""
for i in range(len):
for j in base:
s1 = 255-i-100-ord(j)
if s1 == 48:
ans += j
print (ans)
'''
kjihgfedcba`
'''

拿到密码后继续去到第二关。

那就再看看这个程序还开了哪个类我们还没有分析。GetAndChangeNextContent我们还没有分析,进去看看。

之后再也没有任何操作了,我们已经看到图片了,所以NextContent不是关键类。返回到GetAndChange类,找一下BroadcastReceiver是干什么用的。

BroadcastReceiver详解:https://blog.csdn.net/huiblog/article/details/53234544

回到Manifest发现果然有静态注册。

android.is.very.fun作为显示码输入,但我点击按钮没有任何反应…但是之前不是分析time_2.zip转换为图片嘛,直接解压不行,因为它本来就不是zip压缩包。所以将后缀名修改为jpg就可以看到图片了。

1
flag{08067-wlecome}

14. easy-dex(解密dex & Twofish算法)

在模拟器运行,点一下屏幕就一直在闪,五颜六色地闪。

载入AK,没有找到smali文件,但有一个so文件。并且在AndroidManifest.xml文件中能看到有两个丢失的但程序需要的smali文件。MainActivityNativeActivityMainActivity大家都知道啦,一般是安卓程序的入口。但我们发现这个程序的入口是NativeActivity,这个是什么呢?

写android纯C++的程序需要用到NativeActivity,这个NativeActivity就是一个一般的java类, 和普通的activity没有区别。NativeActivity 是android sdk自带的一个activity。android的纯C++的程序也是需要一个java虚拟机来运行的。NativeActivity通过native_app_glu来启动我们的C++线程,传递各种activity事件给C++代码。native_app_glu在ndk的sources\android目录里面,将native_app_glu当作我们工程的静态库,这个静态库里面封装好了,会创建一个线程,这个线程里面会调用一个android_main(android_app* pApplication)的函数,因此,我们C++这边的入口函数就是android_main()。我们在这个android_main()函数里面的任务就是进行消息循环,做各种任务。

解包,载入IDA查看so文件。找到android_main函数,点进去。

也就是说要在10s内要摇100次手机。

看了个大概,再详细分析代码。

所以我们首先取出unk_7004里面的加密数据,再将它解密,再解压缩得出dex内容将它写入文件中。

File -> Script commond,选择Python,编写ida dump脚本将数据提取出来:

1
2
3
4
5
6
from idaapi import *
addr = 0x7004
size = 0x3CA10
file2Write = '.\\cipherdata'
with open(file2Write,'wb') as f:
f.write(get_bytes(addr,size))

在so文件的同目录下就会生成一个cipherdata文件。接下来给这个文件进行解密操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import zlib
file2Read = '.\\cipherdata'
file2Write = '.\\easydex.dex'

with open(file2Read, 'rb') as f2R:
data = list(f2R.read())
size = 0x3CA10
times = 0

while True:
#10次1组
if times <= 89:
timesDivide10 = int(times / 10)

#10字节1组
if times % 10 == 9:
sizesDivide10 = int(size / 10)
i = 0
for _ in range(sizesDivide10):
data[sizesDivide10 * timesDivide10 + i] ^= times
i += 1

#次数为89次时
if times == 89:
j = (timesDivide10 + 1) * sizesDivide10
while j < size:
data[j] ^= 89
j += 1
break
times += 1

#解压数据
data = zlib.decompress(bytes(data))

#判断解压出的数据是否是dex格式
type = data[0:3].decode("utf8")
if type == 'dex':
#将数据写入文件
with open(file2Write, 'wb') as f2W:
f2W.write(data)

将dex文件载入jeb,就可以查看这个程序的MainActivity类了。

那么,先找密钥。密钥的id号为2131099683,转换成十六进制为0x7F060023。将dex重命名为classes.dex放到解包目录下,重打包。解包目录下就会生成一个bulid目录。

resources.arsc载入jadx,查找id号。

继续查找two_fish,找到一串字符串,这个就是密钥。

1
I have a male fish and a female fish.

Twofish是什么?它其实是一个分组加密算法!Twofish是布鲁斯·施奈尔带领的项目组于1998年研发的区块加密算法。美国国家标准技术研究所(NIST)公开招募的高级加密标准(AES)算法最终候选算法之一,但最终并未当选高级加密标准算法。双鱼算法的标志性特点是它采用了和密钥相关的替换盒(S盒)。密钥输入位的一半被用于“真正的”加密流程进行编排并作为Feistel的轮密钥使用,而另一半用于修改算法所使用的S盒。双鱼算法的密钥编排非常复杂。软件实现的128位双鱼算法在大多数平台上的运行速度不及最终获胜的128位的AES标准算法Rijndael,不过,256位的双鱼算法运行速度却较AES-256稍快。包括Twofish-ECB, Twofish-CBC, Twofish-CTR, Twofish-OFB, Twofish-CFB。

所以再来捋一下思路。我们输入的字符串与密钥进行Twofish加密的结果是字节数组m。密钥经过&0xFF转换不需要我们手动操作,因为网上的解密算法已经包含在内了。但网上的加密算法得出来的不是带负数的字节数组,而是Base64或hex。所以我们可以先将字节数组转换成Base64的形式,再拿去解密。

1
2
3
4
5
6
7
8
9
import base64
flag = [-120, 77, -14, -38, 17, 5, -42, 44, -32, 109, 85, 31, 24, -91, -112, -83, 64, -83, -128, 84, 5, -94, -98, -30, 18, 70, -26, 71, 5, -99, -62, -58, 117, 29, -44, 6, 112, -4, 81, 84, 9, 22, -51, 95, -34, 12, 47, 77]
data = []
for i in flag:
data.append(i&0xFF)
print(base64.b64encode(bytes(data)))
'''
b'iE3y2hEF1izgbVUfGKWQrUCtgFQFop7iEkbmRwWdwsZ1HdQGcPxRVAkWzV/eDC9N'
'''
1
qwb{TH3y_Io<e_EACh_OTh3r_FOrEUER}

15. 你是谁(算法分析)

载入AK,有so文件。载入jeb,找到MainActivity中的onCreate方法。

1
11 12 17 18 20 23 26 29 30 34 35 39 40 49 51 58 62 67 73 76 84 85

按照上面点位点好后,弹出Right design,点击按钮,弹出通过爱的验证。好像并没有什么用。再往上找找源码,发现有flag字样。

中文意思是“你获得了已经排序过的flag”。

1
2
3
4
5
6
7
8
9
10
print(chr(20667))
print(chr(25105))
print(chr(26159))
print(chr(36924))
'''




'''

结合题目和代码,这个重新排序应该是“我是傻逼”。而它说了,那个是排序过的flag,而正确的flag应该为25105 26159 20667 36924

1
flag{25105 26159 20667 36924}

16. Android2.0(算法分析)

载入AK发现有so文件,载入jeb分析源码。

解包,将so文件载入IDA,分析getResult方法。

尝试编写Python脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
v5 = "LN^dl"
v5arr = list(v5)
v6 = [0x20, 53, 45, 0x16, 97]
v7 = list("AFBo}")
ans = ""
for i in range(4):
ans += chr((ord(v5arr[i]) ^ 0x80) // 2)
ans += 'l'
for i in range(4):
ans += chr(ord(v5arr[i]) ^ v6[i])
ans += 'a'
for i in range(4):
ans += chr(ord(v7[i]) ^ v6[i])
ans += '}'
print(ans)
'''
fgorll{sraasoy}
'''

提交flag发现不对,看看哪里漏了。最后发现Init函数不是简单的平均分成3组,而是对正确的flag的每个索引除以3取余得到fgorll{sraasoy}

1
2
3
4
5
6
7
8
9
ans = "fgorll{sraasoy}"
ansarr = list(ans)
ans = ""
for i in range(5):
ans += ansarr[i] + ansarr[i+5] + ansarr[i+10]
print(ans)
'''
flag{sosorryla}
'''

17. boomshakalaka-3(共享文件)

啊好讨厌为什么它是游戏!!我已经玩了好几分钟了!

载入AK看到它有so文件,载入jeb分析源码:

这个base64解码得bazingaaaa。诶结果不是这个。进去a类看看吧。

SharedPreferences是一个轻量级的存储类,特别适合用于保存软件配置参数。使用SharedPreferences保存数据,其背后是用xml文件存放数据,文件存放在/data/data/程序包名/shared_prefs目录下。

1
public abstract SharedPreferences getSharedPreferences(String name, int mode);

第一个参数是存储时的名称,第二个参数则是文件的打开方式。

那我们先找找它的xml文件吧。这个程序的包名为com.example.plane,包名可在AndroidManifest.xmlmanifest标签中找到。

所以可以推测N0MG被写进了Cocos2dxPrefsFile.xml文件中。

但打开Cocos2dxPrefsFile.xml文件却发现不止这两个字符串。我刚才玩了两次,出现了两个极为相似的字符串。

那就再玩几次试试。我发现每次关闭程序再打开又重新写入MGN0,而不关闭程序重新玩不会写入MGN0,每次结束都会以dz99为结束标志。**里面的是每个串的区别。

1
2
3
4
5
6
7
8
9
10
MGN0ZntDMGNvUzJkX0FuRHJvMW*Rf*dz99
ZntDMGNvUzJkX0FuRHJvMW*RfRV*dz99

MGN0ZntDMGNvUzJkX0FuRHJvMW*RfRV*dz99

MGN0ZntDMGNvUzJkX0FuRHJvMW*RfRzRV*dz99

MGN0ZntDMGNvUzJkX0FuRHJvMW*RfRz*dz99
ZntDMGNvUzJkX0FuRHJvMWdz99
ZntDMGNvUzJkX0FuRHJvMW*Rf*dz99

这些星号里面的串有些区别,但又是固定出现的,比如都是RfRV等等。说明在某个内存中存有这些字符。解包将so文件载入IDA,在函数名窗口搜索score,发现有好多这些字符串。

原来MW其实也是包含在里面的。将这些字符串组合起来MWRfRzBtRV9Zb1VfS24w,再将前缀和后缀加上MGN0ZntDMGNvUzJkX0FuRHJvMWRfRzBtRV9Zb1VfS24wdz99。用Base64解码得0ctf{C0coS2d_AnDro1d_G0mE_YoU_Kn0w?}。为什么用Base64,其实题目上面的flag.xml中的Base64字符串已经暗示得很清楚了。

1
0ctf{C0coS2d_AnDro1d_G0mE_YoU_Kn0w?}

18. Illusion(ARM汇编)

运行程序。

载入AK,有一个Flag文件,打开出现一串字符串Ku@'G_V9v(yGS

载入jeb,分析源码。

将so文件载入IDA查看CheckFlag方法。

如果要将算法逆过来,就需要将Flag里的字符串减32再左移32位。

1
2
3
4
5
6
7
flag = list("Ku@'G_V9v(yGS")
for i in range(len(flag)):
flag[i] = (ord(flag[i]) - 32) << 32
print(flag)
'''
[184683593728, 365072220160, 137438953472, 30064771072, 167503724544, 270582939648, 231928233984, 107374182400, 369367187456, 34359738368, 382252089344, 167503724544, 219043332096]
'''

所以sub_10C0每次循环得出的值就是上面这一串数字。进去sub_10C0看看算法。

(说着随机选取,结果还是认真算了)Flag字符串长度为13,所以在aE116c5c66e7b37数组中只取前13个字符。

1
2
.rodata:000023C8                 ; ORG 0x23C8
.rodata:000023C8 aE116c5c66e7b37 DCB "e116c5c66e7b373d912cb9b885b48913",0

前13个字符为e116c5c66e7b3,其中最大的ASCII码为e(101),最小是1(49)。而我们可以输入的可视化字符的ASCII码范围是32~126。所以v9[i]+aE116c5c66e7b37[i]-64的范围应该在17~163。照着IDA的代码抄一遍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
for a1 in range(17,164):
a2 = 93
v2 = a1 ^ a2
v3 = 1
v4 = 0
if((a2 & 0x80000000) != 0):
a2 = -a2
if((a1 & 0x80000000) != 0):
a1 = -a1
if(a1 >= a2):
while a2<0x10000000 and a2<a1:
a2 *= 16
v3 *= 16
while a2<0x80000000 and a2<a1:
a2 *= 2
v3 *= 2
while True:
if(a1 >= a2):
a1 -= a2
v4 |= v3
if(a1 >= a2 >> 1):
a1 -= a2 >> 1
v4 |= v3 >> 1
if(a1 >= a2 >> 2):
a1 -= a2 >> 2
v4 |= v3 >> 2
if(a1 >= a2 >> 3):
a1 -= a2 >> 3
v4 |= v3 >> 3
if a1 == 0:
break
v3 >>= 4
if v3 == 0:
break
a2 >>= 4
if v2 < 0:
v4 = -v4
print(v4, end = " ")
'''
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 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 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 Press any key to continue . . .
'''

在这个范围完全没有这么大的数值…而且在这个范围要不就返回0要不就返回1。肯定是哪里出问题了。呜呜我看了牛牛们的wp说这个是假的,真的CheckFlagJNI_Onload函数里,我就知道!!我就说导出表都有CheckFlag了怎么还有个JNI_Onload!以后记住从JNI_Onload进去准没错,还是没能抵挡住CheckFlag的诱惑。

可以继续试试参数范围。aLjavaLangStrin_0存的字符串为(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;。取前13个字符为(Ljava/lang/S。最小的ASCII码为40((),最大的ASCII码为118(v)。所以v10[i]+aLjavaLangStrin_0[i]-64的范围在8~180。

1
2
3
'''
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 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 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 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 1 1 1 1 1 1 1 1 Press any key to continue . . .
'''

还是要不就是0要不就是1。这个时候就不要想是不是你的问题了!肯定是IDA反汇编的错!所以接下来要看汇编代码找到正确的逻辑。

对于栈的立即数,可以右键 -> Q算得栈的偏移值。

到这已经将所有细节都分析了,除了sub_1028,如果它反编译没错的话那答案基本就已经出来了。

1
flag = ((输入的字符串+内存字符串-64) - sub_1028(输入的字符串+内存字符串-64, 93) * 93) + 32

编写脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import string
flag = list("Ku@'G_V9v(yGS")
str = list("(Ljava/lang/S")
ans = ""
#输出可打印字符串
input = string.printable

def sub_1028(a1):
a2 = 93
v2 = a1 ^ a2
v3 = 1
v4 = 0
if((a2 & 0x80000000) != 0):
a2 = -a2
if((a1 & 0x80000000) != 0):
a1 = -a1
if(a1 >= a2):
while a2<0x10000000 and a2<a1:
a2 *= 16
v3 *= 16
while a2<0x80000000 and a2<a1:
a2 *= 2
v3 *= 2
while True:
if(a1 >= a2):
a1 -= a2
v4 |= v3
if(a1 >= a2 >> 1):
a1 -= a2 >> 1
v4 |= v3 >> 1
if(a1 >= a2 >> 2):
a1 -= a2 >> 2
v4 |= v3 >> 2
if(a1 >= a2 >> 3):
a1 -= a2 >> 3
v4 |= v3 >> 3
if a1 == 0:
break
v3 >>= 4
if v3 == 0:
break
a2 >>= 4
if v2 < 0:
return -v4
return v4

for i in range(len(flag)):
for j in input:
data = ord(j) + ord(str[i]) - 64
v4 = sub_1028(data)
if (data - v4 * 93) + 32 == ord(flag[i]):
ans += j
print(ans)
'''
CISCN{GJ5728}
'''

19. APK逆向-2(XML文件)

在模拟器上安装失败,解包不能解包,用AK不能反编译。那就把它后缀改为.zip,解压发现可以解压。没有找到smali目录,但有classes.dexresources.arsc文件。这两个文件在jadx都可以打开,但AndroidManifest.xml文件显示乱码。

其实我们解压时就可以知道是AndroidManifest.xml的问题了。

那么接下来就是要仔细分析AndroidManifest.xml文件。


AndroidManifest.xml文件采用小端模式存储,可以大体分为四个部分:

  1. Header:头文件
  1. String Chunk:存储字符串资源的程序块

  2. ResourceId Chunk:存储资源id的程序块

  3. XmlContent Chunk:存储xml内容程序块,其中包含了五个部分,Start Namespace Chunk 、End Namespace Chunk 、Start Tag Chunk 、End Tag Chunk 、Text Chunk

Header

magicnumber:魔数,固定值 0x0008003(16进制),占四个字节。

filesize:xml文件总字节数 ,占四个字节。

String Chunk

ChunkType:StringChunk类型,4个字节 ,固定值 0x001c0001

ChunkSize:StringChunk大小 ,4个字节

StringCount:StringChunk字符串的个数,4个字节

StyleCount:StringChunk样式的个数,4个字节,固定值 0x00000000

Unkown: 位置区域,4个字节,固定值 0x00000000解析时候需要略过4个字节

StringPoolOffset:字符串池偏移量,4个字节,偏移量相对StringChunk头部位置

StylePoolOffset:样式池偏移量,4个字节,偏移量相对于StringChunk头部位置,固定值 0x00000000 ,这个字段基本没用到过

StringOffsets:每个字符串在字符串池中的相对偏移量,int数组,它的大小是 StringCount*4 个字节

StyleOffsets:每个样式的偏移量,int数组,它的大小是 StyleCount*4 个字节

String Pool:字符串池,存储了所有的字符串

Style Pool:样式池,存储了所有的样式,一般为0

ResourceId Chunk

ChunkType:ResourceldChunk的类型,占4个字节,固定值 0x00080180

ChunkSize:ResourceldChunk的大小,占4个字节

ResourceIds:int数组,大小为(ChunkSize - 8) / 4 ,减 8是减去头部大小的8个字节(ChunkType和ChunkSize)

XmlContent Chunk

XmlContentChunk 这部分表示的是存储了清单文件的详细信息,包含的5项,其中Start Namespace Chunk 和End Namespace Chunk 这两个可以合并一个来说明, 因为它们的结构完全一致,解析过程也是一样的。至于End Tag Chunk一共有6个数据,也就是 Start Tag Chunk 的前 6 项,这里不做单独解析和说明。End Tag Chunk这个跟清单文件标签一样的,就是给解析出来的标签加上结束标签一样。Text Chunk这个模块在010 Editor模板里并没有用到过。

Start Namespace Chunk主要包含一个清单文件的命令空间内容

ChunkType:Chunk的类型,4个字节 ,固定值 0x00100100

ChunkSize:Chunk的大小 ,4个字节

LineNumber:清单文件中的行号, 4个字节

Unknown:未知区域, 4个字节

Prefix:命名空间的前缀, 4个字节

Uri:命名空间的URI, 4个字节

Start Tag Chunk主要存放清单文件中的标签信息

ChunkType:Chunk的类型,4个字节 ,固定值 0x00100102

ChunkSize:Chunk的大小 ,4个字节

LineNumber:清单文件中的行号, 4个字节

Unknown:未知区域, 4个字节

Namespace Uri:命名空间用到的url在字符串的索引,值为 -1 表示没有用到命名空间 uri。标签的一般都没有使用到命名空间,4个字节

Name:标签名称(在字符串中的索引值),4个字节

Flags:标签类型例如开始标签还是结束标签,固定值0x00140014,4个字节

Attribute Count :标签包含的属性个数,4个字节

Class Attribute :标签包含的类属性,此项值常为 0,4个字节

Attributes :属性内容集合,每个属性固定 20 个字节,包含 5 个字段,每个字段都是 4 字节无符号 int,解析的时候需要注意Type这个值做一次处理需要右移24位。各个字段含义如下:

  • NamespaceUri:属性的命名空间uri 在字符串池中的索引
  • Name:属性名称在字符串池中的索引
  • ValueStr:属性值
  • Type:属性类型
  • Data:属性数据

首先查看xml的固定值是否有误。ChunkType应该为01 00 1c 00。而且StylePoolOffset应该为00 00 00 00。修改完后一定要点保存,而不是另存为,因为我发现另存为后有些其它值也被修改了,导致重打包后再解包时出现错误!

忽然发现解包时异常已经告诉我有其中一个错误了…

将它重打包时出现没有apktool.yml文件错误,将其它apk里面的apktool.yml复制一份下来,将apk文件名改了即可。

这次终于成功解包了。

所以xml有什么什么神秘的呢?发现中间那个字符串就是flag。

1
8d6efd232c63b7d2

20. ill-intentions(Native hook)

翻译一下就是,选择您希望与之交互的MainActivity,待办事项:添加按钮来选择活动,现在使用的是Send_to_Activity。

载入AK看可不可以直接修改AndroidManifest.xml的入口,不行。程序还是那个界面。

那就载入jeb分析看看吧。

这些方法都是在so文件里面。解包先看看so文件。导出表刚好有这三个函数。如果导出表有native函数,说明这些是导出函数;如果没有,而只有JNI_Onload,说明那些是未导出函数。

可以确定DefinitelyNotThisOne函数肯定没有flag。剩下那两个函数看得我头大,而且传入的参数也有些是在Java层加密过的。

因为你!我去学了Frida so hook!Frida超详细安装实战教程

hook一个so方法需要知道:

  • 程序的名字(Frida-ps -U查看程序名字):CTF Application
  • so文件名:libhello-jni.so
  • so方法名:Java_com_example_application_ThisIsTheRealOne_orThat

编写hook脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import frida
import sys

jscode = """
function main(){
function getjstring(jstr) {
return Java.vm.getEnv().getStringUtfChars(jstr, null).readCString();
}
Java.perform(function(){
Interceptor.attach(Module.findExportByName("libhello-jni.so","Java_com_example_application_ThisIsTheRealOne_orThat"),{
onEnter: function(args) {
send("Hook start");
},
onLeave: function(retval){
send("orThat_result:" + getjstring(retval));
}
});
});
}
setImmediate(main);
"""
def printMessage(message,data):
if message['type'] == 'send':
print('{0}'.format(message['payload']))
else:
print(message)

process = frida.get_remote_device().attach('CTF Application')
script = process.create_script(jscode)
script.on('message',printMessage)
script.load()
sys.stdin.read()

开启Frida服务:

1
2
3
4
5
C:\Users\dell>adb shell
aosp:/ # cd /data/local/tmp
aosp:/data/local/tmp # ls
android_x86_server frida-server-15.1.17-android-x86
aosp:/data/local/tmp # ./frida-server-15.1.17-android-x86

打开另一命令窗口开启端口转发,Frida默认端口27042:

1
2
C:\Users\dell>adb forward tcp:27042 tcp:27042
27042

等等,突然想起来我们还没去到我们想要进去的Activity,修改xml文件我是不会修改了,但可以利用objection运行指定的Activity。

安装objection:pip install objection

在模拟器运行程序,在终端输入需要调试的程序的包名:objection -g com.example.hellojni explore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
C:\Users\dell>objection -g com.example.hellojni explore
Using USB device `Android Emulator 5554`
Agent injected and responds ok!

_ _ _ _
___| |_|_|___ ___| |_|_|___ ___
| . | . | | -_| _| _| | . | |
|___|___| |___|___|_| |_|___|_|_|
|___|(object)inject(ion) v1.11.0

Runtime Mobile Exploration
by: @leonjza from @sensepost

[tab] for command suggestions
com.example.hellojni on (Meizu: 7.1.2) [usb] #

列出app所有的Activity:android hooking list activities

1
2
3
4
5
6
7
com.example.hellojni on (Meizu: 7.1.2) [usb] # android hooking list activities
com.example.application.DefinitelyNotThisOne
com.example.application.IsThisTheRealOne
com.example.application.MainActivity
com.example.application.ThisIsTheRealOne

Found 4 classes

启动指定Activity:android intent launch_activity com.example.application.ThisIsTheRealOne

1
2
3
com.example.hellojni on (Meizu: 7.1.2) [usb] # android intent launch_activity com.example.application.ThisIsTheRealOne
(agent) Starting activity com.example.application.ThisIsTheRealOne...
(agent) Activity successfully asked to start.

可以看到程序页面已经变了:

运行脚本,点击程序中间的按钮,打印返回值。

1
2
3
C:\Users\dell\Desktop>python hookso.py
Hook start
orThat_result:KeepTryingThisIsNotTheActivityYouAreLookingForButHereHaveSomeInternetPoints!

它说这个不是我要找的Activity,换一个。

1
2
3
com.example.hellojni on (Meizu: 7.1.2) [usb] # android intent launch_activity com.example.application.IsThisTheRealOne
(agent) Starting activity com.example.application.IsThisTheRealOne...
(agent) Activity successfully asked to start.

修改脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import frida
import sys

jscode = """
function main(){
function getjstring(jstr) {
return Java.vm.getEnv().getStringUtfChars(jstr, null).readCString();
}
Java.perform(function(){
Interceptor.attach(Module.findExportByName("libhello-jni.so","Java_com_example_application_IsThisTheRealOne_perhapsThis"),{
onEnter: function(args) {
send("Hook start");
},
onLeave: function(retval){
send("perhapsThis_result:" + getjstring(retval));
}
});
});
}
setImmediate(main);
"""
def printMessage(message,data):
if message['type'] == 'send':
print('{0}'.format(message['payload']))
else:
print(message)

process = frida.get_remote_device().attach('CTF Application')
script = process.create_script(jscode)
script.on('message',printMessage)
script.load()
sys.stdin.read()

运行,点击按钮,打印返回值。

1
2
3
C:\Users\dell\Desktop>python hookso.py
Hook start
perhapsThis_result:Congratulation!YouFoundTheRightActivityHereYouGo-CTF{IDontHaveABadjokeSorry}
1
CTF{IDontHaveABadjokeSorry}

21. LoopCrypto

将用户输入传到a()方法中,当点击“CHECK”时执行onClick()方法。取出包中的签名并对其进行MD5加密,再转换成16进制字符串。将输入与加密后的签名一同传入到JNI层的check()方法中。

使用jeb动调可以得到签名的MD5值吗?由于APK的debuggable属性为false,那我们修改系统为全局可调试。有两个进程,选第一个进程发现变量全为空,选第二个进程发现不可调试。

行吧,真不知道什么原因,或许到时候可以hook check()函数得到签名的MD5值。进入So,没有找到check(),去看看JNI_OnLoad()

进入sub_88C4()sub_8740()调用了Decode.a()方法,将这三块内存数据解密成字符串。

所以sub_87FC()就是我们要找的check()函数。进入check()函数:

尝试使用Frida hook sub_87FC(),要先在内存中找到So的地址,再通过check()函数在So中的偏移得到它的参数。

1
2
3
4
5
function hook_check(){
var libcheck_addr = Module.findBaseAddress("libcheck.so")
console.log("libcheck_addr => ", libcheck_addr)
}
setImmediate(hook_check)

呃…竟然没有,是因为我用的是x86架构模拟器,而So是ARM架构的吗,那怎么办…使用Android 8.1.0真机启动不了这个App。

使用objection查看内存中加载的库确实没有找到libcheck.so。回看代码,发现是在Decode类中进行So加载操作,当我们点击按钮时应该会执行到Decode.check()方法,也就是会让So加载起来,所以为什么在内存中找不到呢?

将off_12004中的622个字节dump下来,File -> Script command,编写脚本以二进制的形式dump出来。

进入sub_85E0()函数:

所以APK签名的MD5值是多少呢?

F8 C4 90 56 E4 CC F9 A1 1E 09 0E AF 47 1F 41 8D

进入sub_84D0()

1.2.11算是一个提示,对加密后的数据使用zlib-1.2.11压缩算法进行压缩。

22. 从林的秘密(wasm & z3)

查看布局发现ID都对应不上,其实是在这个APK中内置了WebView页面,文字、输入框、按钮这些都不是在Java代码中定义的,而是由加载的URL显示出来的。所以这个Activity中定义的文字、输入框、按钮这些控件都是用来迷惑人的,点击事件自然也没有用。

URL在So中的sayHello()方法里,为http://127.0.0.1:8000。在浏览器中输入网址也可以得到这个HTML页面。

check_key()方法被一个不会发生的点击事件调用,所以逆向check_key()只是在浪费时间。我们知道So在加载进内存后会依次执行init()init_array()以及JNI_OnLoad()函数,查找导出表只看到JNI_OnLoad(),看看它都干了什么。

这个mm0数组大小为0x85E0,但从伪代码中可以看到还有用到下标为0x85F2的,所以取最大值0x85F3。使用IDA Python脚本将这块内存dump下来,顺便异或0x67。

1
2
3
4
5
6
7
8
9
10
static main(){
auto i,fp;
fp = fopen("C:\\Users\\Dell\\Desktop\\Dump2","wb");
auto start = 0x12008;
auto size = 34291;
for(i=start;i<start+size;i++){
fputc(Byte(i)^0x67,fp);
}
fp.close();
}

打开文件就可以看到解密后的HTML源码了。

JavaScript中有个check_flag()函数,要求输入字符串长度为32,并调用check_key()函数,可是并没有看到check_key()函数,应该是从那一大串二进制数据中动态加载出来的。而那一大串二进制数据是WebAssembly,即WASM。WebAssembly是一种新的编码格式并且可以在浏览器中运行,WASM可以与JavaScript并存,WASM更类似一种低级的汇编语言。

将二进制数据粘贴到记事本,并用010 Editor导入十六进制数据(File -> Import Hex),然后另存为check.wasm文件。由于它是汇编语言,转为C语言或直接生成可执行文件更容易理解,推荐一个工具wasm一键转c。将可执行文件载入IDA查看check_key()函数。

这些o()函数的主要作用是对memory的偏移存的值,也就是我们输入的字符串进行异或。

xxx()函数是对异或后的值进行线性运算:

复杂的线性运算、按位运算都可以使用z3库来求解。python z3库学习

由于偏移1024~1055与v2~v33并不是对应关系,所以还要调整对应关系后再进行异或运算。

得到flag:

1
K9nXu3_2o1q2_w3bassembly_r3vers3

23. 变形金刚(字符串加密 & RC4 & 变种Base64)

运行一下程序发现提示不同,程序的执行流程走到了父类AppCompiatActivity里面,分析代码可知,只要对So中的eq()方法分析得到mPassword,就相当于得到了flag。

查看So的导出表,只有以下两个函数:

datadiv_decodexxx()是So进行字符串加密的特征函数,进去发现它做了一些异或操作。

将它们拷贝出来进行异或还原字符串:

进入JNI_OnLoad()查看eq()方法:

进入eq()

继续分析代码,对输入字符串进行RC4加密后,还使用到了byte4050,byte4050是一个字符表,长度为65。根据代码中右移2、4、6可以猜测是Base64的变种,也就是将3个字符经编码后变成4个字符。byte24E8中最后两个字符是“;;”,而byte4050字符表最后一个字符也是“;”,说明这个字符相当于Base64的“=”,印证我们之前的推测。

此时我们已经得到编码后的字符串,现在要将字符串解码。byte24E8的两个占位符是最后才加上去的,所以不参与异或。

红框中,如果直接打印,可见字符会被输出;如果需要数据转成16进制形式,需要使用binascii库。现在已知key,原始S盒,加密后的16进制字符串,就可以进行RC4解密了。

这…很明显不对,原始S盒不可能出错,加密后的16进制字符串应该没错,所以可能是解key时出了错。遇到移位运算,不要使用负数,不要太相信结果,要结合上下文分析。

结果发现并没有用到“-”,所以还是不对。如果将字符串全部逆序根本不用将“-”提取出来,可能是“-”的位置不变。最后转换出来的key如下图:

更改key值再次进行RC4解密,哎这个就像样了,是正常字符串,fu0kzHp2aqtZAuY6

输入密码得到flag。

1
flag{android4-9}

24. 一统天下

在说明中可以知道加密逻辑已经被去掉了,也就是说这个APK文件无害的,需要我们研究其中的加密算法。载入jeb,加密算法如下图,但是类和方法混淆得太离谱了,如果jeb没有自动解密功能还会更夸张。

这个App名是吃鸡辅助,点进去后配置完文件自动退出,生成另一个名为QQ的App,卸载吃鸡辅助App。点进QQ图标重新进入勒索软件界面,找到关键类。

每次加载APK到jeb,this.O000O0OO = "" + 10169905;中的常量值都会改变,使用jadx工具发现这里进行了一个随机数选择。

进入new OO00O0(),找到显示主页面执行逻辑。定位关键字符串,找到与特征码、解密口令相关语句。

接下来就是找OO0OOOO()函数。我们发现lib目录下有两个So文件,libmydvp.solibmyqtest.so。主页面加载了libmyqtest.so,导出表中没有OO0OOOO()函数和JNI_OnLoad(),看到So字符串加密和一个Native层函数Java_android_support_v4_app_o000000o_o000000o0(),该函数是在主页面加载的。

猜测要找的函数应该在libmydvp.so中,程序又是如何加载libmydvp.so到内存的呢?在Java代码中查找loadLibrary()函数。

根据继承关系找到OO0OOOO0.class,它在AndroidManifest.xml中,被provider标签调用。因为provider会在入口类走之前运行,因此第一个执行的类为android.support.v4.app.OO0OOOO0,也就是这个程序先加载libmydvp.so再加载libmyqtest.so

这个So文件使用到了OLLVM混淆。

JNI_OnLoad()函数:

24.1 反字符串加密

So中的字符串解密有两种情况,一种是在执行JNI_OnLoad()前就将所有字符串解密,解密算法在.init_array中;另一种是使用时再解密。加密字符串数据存储在.data节区。

先将所有加密字符串解密,这里如何快速获得所有的解密字符串?一个个敲代码或在010 Editor中异或都太慢了。。。找到OO0OOOO()函数,并且看字符串还有TracerPid检测。

果然,被我找到一些方法。使用IDA插件uEmu对加密字符串进行解密;使用Frida hook方式得到解密字符串使用AndroidEmu或unidbg对抗字符串混淆;也可以将So加载到内存后,dump .data节区中的数据。

Android APP漏洞之战(14)——Ollvm混淆与反混淆这篇文章几乎涵盖这道题所要用到的知识。

24.1.1 IDA插件uEmu

由于我下载的IDA Pro自带Python,所以要将unicorn下载到相应的Python环境中,在载入uEmu.py脚本时才不会报没有这个模块的错:

1
pip install unicorn --target="D:\CTF\tools\IDA_Pro_v7.5\python\3\"

在解密函数的首尾下断点,在首断点右键 -> uEmu -> Start,在尾断点右键 -> uEmu -> Run,右键 -> uEmu -> Show Memory Range 查看byte_E3004解密后的结果。

24.1.2 Frida hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function hook_native(){

//ollvm默认的字符串混淆,静态的时候没法看见字符串
//执行起来之后,先调用.init_array里面的函数来解密字符串
//解密完之后,内存中的字符串就是明文状态了。

//首先找到函数的基址
var base_hello_jni = Module.findBaseAddress("libmydvp.so");
if(base_hello_jni){
//利用指针加偏移的方法找到函数地址
var addr_str = base_hello_jni.add(0xE3010);
console.log("addr_E3010:",ptr(addr_str).readCString());

var addr_str = base_hello_jni.add(0xE3037);
console.log("addr_E3037:",ptr(addr_str).readCString());
}
}

24.1.3 AndroidEmu

批量解密可以用AndroidNativeEmu:

1
pip install androidemu

采用上面文章的解密脚本,但不知道为什么出错,暂时不管了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import logging
import sys
from unicorn import UC_HOOK_MEM_WRITE
import struct

from androidemu.emulator import Emulator

# Configure logging
logging.basicConfig(
stream=sys.stdout,
level=logging.DEBUG,
format="%(asctime)s %(levelname)7s %(name)34s | %(message)s"
)

logger = logging.getLogger(__name__)
dstr_datas={}
def hook_mem_write(uc,type,address,size,value,userdata):
curdata=struct.pack("I",value)[:size]
dstr_datas[address]=curdata
# print(curdata)

emulator = Emulator(vfp_inst_set=True)
emulator.load_library("libc.so", do_init=False)
#设置内存的写入监控
emulator.mu.hook_add(UC_HOOK_MEM_WRITE, hook_mem_write)
#后面的do_init为true就会调用.init_array
modulePath="libmydvp.so"
lib_module = emulator.load_library(modulePath, do_init=True)
#获取到基址
base_addr=lib_module.base
sofile=open(modulePath,"rb")
#我们要将真实的字符串回填到sodata中。然后再保存
sodata=sofile.read()
sofile.close()
for address,v in dstr_datas.items():
#仅仅将so范围内的保存原字符串进行写回
if address > base_addr and address < base_addr+lib_module.size:
offset=address-base_addr-0x1000
print("address:0x%x data:%s offset:0x%x" % (address, v,offset))
sodata=sodata[:offset]+v+sodata[offset+len(v):]
#保存成一个新的so
savepath=modulePath+".new"
nfile=open(savepath,"wb")
nfile.write(sodata)
nfile.close()

24.1.4 从内存中dump .data节区

查看应用程序的进程与PID:

1
2
3
4
> adb shell "ps | grep {PackageName}"

u0_a160 11082 641 1689772 68260 SyS_epoll_wait 0 S com.zhuotong.kanxuectf2
u0_a160 11098 11082 1635208 14508 ptrace_stop 0 t com.zhuotong.kanxuectf2

/proc/{PID}/maps中得到libmydvp.so的内存映像地址:

1
2
3
4
5
 # cat /proc/11082/maps |grep libmydvp.so

d50d6000-d51b5000 r-xp 00000000 fd:00 261788 /data/app/com.zhuotong.kanxuectf2-sfhALQ1_qUOjlGyuyBsd8Q==/lib/arm/libmydvp.so
d51b5000-d51b9000 r--p 000de000 fd:00 261788 /data/app/com.zhuotong.kanxuectf2-sfhALQ1_qUOjlGyuyBsd8Q==/lib/arm/libmydvp.so
d51b9000-d51ba000 rw-p 000e2000 fd:00 261788 /data/app/com.zhuotong.kanxuectf2-sfhALQ1_qUOjlGyuyBsd8Q==/lib/arm/libmydvp.so

.data节区可读可写,所以将最后一段dump出来。skip是内存起始地址,count是这个节的大小:

1
2
3
4
5
# dd if=/proc/11082/mem of=/data/local/tmp/dump.so skip=3575353344 bs=1 count=4096

4096+0 records in
4096+0 records out
4096 bytes transferred in 0.065 secs (63015 bytes/sec)

dump.so赋予755权限再pull出来。使用010 Editor将适当的字符串数据拷贝到libmydvp.so中的.data节区。

重新加载libmydvp.so,就可以看到解密后的字符串。

24.2 反虚假控制流

分析完.init_array的字符串解密,在.init_array中还有一些函数。

进入sub_1AEE8(),可看到pthread_create()函数和getpid()函数,还有/proc//statusTracerPid字样,可以确定这个函数是在进行TracerPid检测。

进入sub_1ED54(),创建管道,创建线程,同样是进行了TracerPid检测。

进入JNI_OnLoad(),分析代码。

ssspbahh.so是以NISLFILE开头的,并没有这种魔数类型。但我们在解密字符串是发现NISLFILE字样,并且被JNI_OnLoad()中的sub_86128()调用。然而这个函数被混淆得离谱。

尝试通过符号执行来解决虚假控制流和控制流平坦化。Kali安装angr环境教程,在Windows安装就像shit一样。ida pro6.4 linux安装使用,用来查看函数地址。利用angr符号执行去除虚假控制流,使用deflat脚本。

进入虚拟Python环境:

1
source /root/angr/bin/activate

执行命令:

1
python3 deflat.py -f samples/bin/check_passwd_x8664_flat --addr 0x400530

退出虚拟环境:

1
deactivate

不知道为什么去不了…明明例子中的ELF文件可以去掉的。

通过native hook方式找到动态注册函数?但是有反调试,这个方法应该不可行。

25. Phishing is not a crime-2

你能用Instagram for Android移动应用程序APK v7.9.0在signed_bodyPOST参数中对传出的JSON数据进行签名吗?提示:我喜欢早上的炒蛋!

下载下来是一个伪Instagram,抓包没抓到东西,连不上网。点击登录或注册根本没请求到服务器。

26. Flag_system(备份文件ab)

下载下来的是一个安卓备份文件,具体看题3。

直接使用abe解压出来的TAR文件损坏,发现版本与是否压缩都不一样,修改一下该文件的版本与是否压缩参数,并将后缀名改为.ab。此时解压出来的TAR文件正常。

TAR文件中主要有两个APK文件和一个DB文件,DB文件被加密。其中一个APK非常简单,再看另一个。

并没有什么特殊的信息,我们知道数据库名为BOOKS.db,找到建立数据库的地方。

此时数据库密码可以通过Log打印,也可以通过动调或hook方式得到。由于程序没有设置debuggable,所以通过设置ro.debuggable=1,再进行动调比较方便。由于getSign()函数在程序运行前就执行了,所以要用debug模式才能断到getSign()函数处。

得到数据库密码:320b42d5771df37906eee0fff53c49059122eeaf。但我使用DB Browser for SQLCipher for Windows v3.12.99打不开数据库,用sqlcipher 2还是不能打开。

此时可以换一种方法解题。查看getReadableDatabase()函数,它返回的是一个SQLiteDatabase类型的对象,而这个数据类型中有query()rawQuery()方法,都是用来查询数据库内容的。

对比之下发现rawQuery()方法更符合我们编写SQL语句的习惯,我们可以用它来查询数据库中的信息。hook getReadableDatabase(),得到SQLiteDatabase对象,然后在getReadableDatabase()方法中调用rawQuery()方法来获得想要的数据。

为什么只有自己增加的数据…

27. Where(炸弹引爆 & DEX修复)

下载下来不知道什么格式文件,拿去WinHex或者010 Editor,文件头为50 4B 03 04,标准的ZIP文件格式,APK文件本质上也是个ZIP文件,所以将后缀名修改为.apk

/assets目录下还有一个abc文件,打开发现是一个DEX文件头,在/original目录下找到一个y文件,应该是DEX文件的一部分。

查看abc文件,由于DEX文件头描述了整个DEX文件的分布,所以可以看到DexClassDef与数据段是有数据的,其余都为0。

但y文件只有0x94大小,根本不能构成一个DEX文件。与它同目录的CERT.RSA文件异常的大,考虑是使用了炸弹引爆

已知DEX文件总大小为0x1549F0,DEX文件头大小为0x70,所以DEX body的大小为0x154980。DEX body埋在了CERT.RSA的末尾,它的总大小为0x154EAB,所以CERT.RSA的大小应该最大为0x52B。

但在提取前发现前面有“KEY”和“DEX”字样,后面有“aes-128-cbc”,也就是说DEX body有可能进行了AES加密。

aes-128-cbc是对称加密算法,加解密使用的KEY是一样的,明密文长度有可能不一致。将加密过后的DEX body取出,使用解密算法解密。

1
openssl enc -d -aes-128-cbc -k 'Misc@inf0#fjhx11' -nosalt -in encrypt_dex -out decrypted

解密失败,这是由于openssl高版本加密时对密码使用sha256进行加密;低版本使用md5加密。如果需要兼容,则需要指定参数-md的值,确保两边使用相同的摘要算法。使用opensll加解密压缩文件

1
openssl enc -d -aes-128-cbc -md md5 -k 'Misc@inf0#fjhx11' -nosalt -in encrypt_dex -out decrypted

decrypted中的数据追加到abc文件的尾部,大小恰好是0x1549F0。

由于现在已经有DEX body了,string_ids_size、type_ids_size等这些肯定不全为0。以string_ids_size为例,它的偏移为0x70,type_ids_size的偏移为0x91DC,这两个地址中间存放的全都是string的偏移地址。一个地址占4个字节,所以string_ids_size应该为(0x91DC - 0x70) / 4 = 0x245B。同理可得type_ids_size为0x484,proto_ids_size为0x12CF,field_ids_size为0x1BA4,method_ids_size为0x4404。

将修改后的abc文件载入jadx发现checksum错误,猜测DEX文件还没完全修复,但现在只能修改checksum查看DEX文件被我们修改成什么样了。

发现onCreate()方法还没被修复出来。查看abc文件中的onCreate()方法,刚好全0的数据大小为0x94。

将y文件中的数据复制到这个地方,最后修改checksum(和最开始的checksum也不一样)。载入jadx查看onCreate()方法,得到最终flag:

28. TryGetFlag1

看起来是函数抽取型壳,使用FART脱壳,应用没有存储权限,要在AndroidManifest.xml中增加读写存储权限。

1
2
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

在application标签中添加属性:

1
android:requestLegacyExternalStorage="true"

但我们发现重新安装APK后APK不能正常运行,也不能使用FART进行脱壳,这不死循环了吗…猜测这道题根本不用脱壳解题。

由于知道一些明显的方法和成员变量,可以使用Frida hook这些方法与成员变量查看其值。

GetFlag()方法返回的字符串提示我们这里有可利用的bug。MainActivity.class中还有一个静态成员变量,也将它hook出来看看。

将字符串作为GetFlag()的参数传入,却发现它说我们得到了一个错误的flag。

1
CISCN{You.Got.It.11a15e}

提交上去不行,我实在不知道怎么解了。

29. Load

APK安装上了但闪退。

救命真的好难