# Fuko's_starfish

题目给出一个 exe 和 dll 文件,先看看 exe

先加载这个 dll 文件,若加载成功则提示我们需要完成三个小游戏,然后进入函数 sub_140001490,那么跟进它看看

第一个小游戏就在 sub_140001270 内,是一个猜数字小游戏

原本想动调的,但这道题似乎搞了挺多反调试,找不到怎么解决,强行运行吧,前面的猜数字和贪吃蛇玩一下就过了,到最后这里

对应 dll 文件里面是

这里让我们输入密钥,下面的 sub_180001650 是个 AES 加密,跟进看可以发现

这里有一大串赋值语句,应该就是密钥,跟进这些 byte_xxxx 就可以发现这个函数:

它就是生成密钥的函数,而这个函数还有花指令,简单去花,就是这一段,它用 rax 寄存器混淆 ida,使 ida 错误判断程序执行流,全 nop 即可:

这里在中间设置了随机种子,改变了前面 16 个赋值过的 byte,把它作为密钥赋值,再回去看那个 AES 函数,它有调试检测,若检测到调试则执行流会进 if 块,那么正确解密肯定要看 else 块

这里把他们都异或了一个 0x17,那么根据上述信息就可以得到密钥了

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
int main(){
srand(114514);
for (int i = 0; i < 16; i++)
{
int v10 = rand();
printf("%02x",((unsigned char)(v10 + (v10 / 255))) ^ 0x17);
}

return 0;
}
// 09e5fdeb683175b6b13b840891eb78d2

再根据后面的比对处拿到密文,AES 解密即可

# hook_fish

apk 文件,拖进 jadx 看到 oncreate 函数

这里无论怎样也钓不到鱼,但特别强调要联网,需要注意一下。同时注意看 encrypt 和 fish 这两个方法

encrypt 这个方法里边是对我们输入的东西进行一些加密,但没有看到比对函数和密文

fish 方法则是从一个 URL 地址下载一个 dex 文件,这个地址可以在程序里面找到:

后面的这些方法也都是根据这个 dex 文件来工作的

所以应该要分析一下这个 dex 文件,里面有一些其他的信息,不过这个下载地址已经失效了,等搞到这个 dex 再复现吧(

# kotlindroid

不是常规的 apk 逆向,用 kotlin 和 Jetpack Compose 框架写的,java 层反编译的代码和正常的不太一样

从 xml 看到的 mainactivity 里面其实是应用的第一层,即

但从 mainactivity 里面慢慢找太费时间,而且也不好找,里面有很多关于 compose 框架的无用代码

但运行一下,可以直接在 jadx 里面搜字符串快速定位关键逻辑

分析这里,可以得知是 AES/GCM/NoPadding 加密方式,这里可以直接看到 iv 和密文,但还不知道 key,往前看,交叉引用 check 函数,定位到这里

那么 key 就得知了

1
2
3
4
5
6
7
key1=[118, 99, 101, 126, 124, 114, 110, 100]
key2=[123, 113, 109, 99, 97, 122, 124, 105]
for i in key1:
print(chr(i^23),end='')
for i in key2:
print(chr(i^8),end='')
# atrikeyssyekirta

信息还没完全获取,继续跟进 sec 函数里面的参数:SearchActivityKt$sec$1,找到这里

这里可以看到 base64 编码,符合密文的格式,然后再看到这一行:
string encodedefault=Base64.encodedefault = Base64.encodedefault(Base64.INSTANCE, ArraysKt.plus(generateIV, doFinal), 0, 0, 6, null);
这里的意思是,把一个经过 base64 编码的字符串和这个 iv 字符串拼接,且 iv 字符串在前面

把密文拉去 base64 解密,可以看到前面 6 个字符是 114514,就是 iv 字符串,因此解密的时候要把前 6 个字符删掉才是真正的密文

到这里如果要解密,还差两个参数:tag 和 add data

但突然发现如果用 encrypt 的话,不需要这两个参数也能解出来(

去了解了一下 AES/GCM 加密方式:

也就是说,GCM 分为两个模块:加密和校验

在解密的时候,我们必须要提供这完全的四个参数:key、iv、AAD、GCM Tag,否则这些解密工具会拒绝输出密文,以此确保被加密的数据没有被篡改,即 “要么全对,要么全错 “,而 AAD 和 GCM Tag 就是进行校验的参数,其中 GCM Tag 是根据 AAD、key、iv、密文这四个参数计算得来的

但是,我现在解密密文,不需要关心数据是不是对的(他肯定是对的),所以我只要解密就好了,不需要关心 AAD 和 Tag 这两个参数。而 GCM 方式解密的底层逻辑是 CTR 模式,这个模式的工作原理简单概述:

这里的随机数(Nonce)就是 iv 向量,而计数器是

先要知道,AES 的不同模式都不会改变 AES 本身的加密数学运算,正常的 ECB 模式是把原文简单分割成 128 位的小块,然后分组加密。而 CTR 模式则是加密 Nonce + Counter(如上图,这里的示例不太规范,AES 只会接收 128 位的原文),然后输出 128 位的密文,这 128 位的密文,再直接与原文进行异或(原文也按 128 位拆分)

所以,CTR 模式最终对原文进行的操作仅仅是一个简单的异或,那么,如果我能够提供密钥 key 和 iv 向量,GCM 模式底层的 CTR 加密运算就可以生成正确的密钥流,然后进行异或。同时异或是可逆运算,我把密文再进行异或一次相同的值,就可以恢复到原文

可是直接用 CTR 模式解密就不行,会提示 iv 向量长度不够,原因是:

# 幸运转盘

鸿蒙逆向,去华为官网下个 DevEco Studio,然后运行一下模拟器可以运行这个程序

随便输一个字符串就能进入第二页面

但这里转盘会失败,并输出一段字符串

解压 hap 文件得到 abc 文件,把 abc 文件拖入 jadx-dev-all.jar 可以分析,但很难看,搜索上面转盘提示的错误字符串也没东西

没什么好的办法,只能硬看,在 MyPage 这里找到一个数组,比较可疑,就追着这个数组看看

然后找到这两坨

这个 arg0 就是我们在上面页面输入的 flag,怎么来的?看看 index

这里的 index 和 mypage 实际上就对应了两个页面,index 是接收输入的页面,mypage 就是转盘页面

先把上面的数组密文逆向操作一下看看:

1
2
3
4
a=[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]
for i in a:
print(chr((i^7)-1),end='')
# aLJ5aJqO/ApBpA/C9S8gUIsa1MSDBtijKDeqYwsziTYs

然后反编译这个 libhello.so 并找到 MyCry 函数,定位到这里

先看前面

这里对传入的明文逐字节 + 3

这里的 v5 是根据 x、y 计算的,x 和 y 看到 java 层的调用处

一个是输入的长度,一个是 24,这个 v5 的复杂函数其实就是计算平方和开根

根据前面得到的密文,长度为 44,所以 v5 不应该等于 40,也就是说应该走下面的 sub_i111iIlii 逻辑,这个逻辑是标准的 rc4,在最后异或的地方多异或了一个 0x18

但这样的话 flag 求不对,那就只能猜测或许截断了部分密文,使得 v5=40,强制走了上面的 sub_i111iIl1i 函数,这样 flag 就对了,先把密文 base64 解码,然后 rc4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
flag=[0x68,0xb2,0x79,0x68,0x9a,0x8e,0xfc,0x0a,0x41,0xa4,0x0f,0xc2,0xf5,0x2f,0x20,0x50,0x8b,0x1a,0xd4,0xc4,0x83,0x06,0xd8,0xa3,0x28,0x37,0xaa,0x63,0x0b,0x33,0x89,0x36,0x2c]
key="Take_it_easy"
s=[]
t=[]
for i in range(256):
s.append(i)
t.append(ord(key[i % len(key)]))
j=0
for i in range(256):
j=(j+s[i]+t[i])%256
s[i],s[j]=s[j],s[i]
k=[]
i=j=0
for r in range(len(flag)):
i=(i+1) % 256
j=(j+s[i]) % 256
s[i],s[j]=s[j],s[i]
t=(s[i]+s[j]) % 256
k.append(s[t])
for i in range(len(flag)):
print(chr(((flag[i]^40)^k[i]) - 3),end='')

鸿蒙逆向就是比较恶心,java 层乱码很多,要慢慢细看

# AndroidLux

apk 文件,arm64 架构模拟器打不开,先 jadx 分析,找到主逻辑

这里接收 flag,然后通过 connectAndSendLocalSocketServer 连接一个不知道什么东西,跟进一下 connectAndSendLocalSocketServer 看到

这里有神秘数字(

应该是把我们输入的 flag 发给一个服务器,然后在那个地方进行校验。这个 apk 不联网,那服务器肯定在本地,解压 apk 乱翻一下,最后在 asset 里面发现 env 文件,这是个压缩包,解压发现

这好像就是一个小 linux 系统,去 root 文件夹里面可以找到 env 这个 elf 文件,反编译一下

有点花指令,查一下资料了解一下 arm64 汇编,可以识别出来这里是花指令:

一共有两处,全 nop 掉就可以分析

是个魔改 base64,但解出来的 flag 不对

找了点题解看看,这题好像还挺复杂的... 题解里面提到的什么 proot、rootfs 需要了解一下

简单来说这个题的 apk 文件用了一个叫”proot“的开源项目,它可以在 apk 里面模拟出来一个 linux 系统,这区别于虚拟机,相当于一个小环境。java 层的代码通过 socket 和这个小环境通信,把 flag 发过去给它校验。而 rootfs 其实就是这个小 linux 系统,也就是上面解压出来得到的那个 env 文件夹

但这个 rootfs 是出题人自己搞的,也就是说他可以在里面动些手脚。举个例子:在正常的操作系统里面用命令 ls 可以列出文件夹的内容,但在 roofs 文件夹里面可以对这个命令做一些修改,修改之后的 ls 指令可能就不是原来的功能(比如触发点后门什么的)

官方题解提到:”rootfs 一般是脚本构建的,这样才能保持软件包不会过于落后,既然如此,出题者只可
能在原本 rootfs 基础上修改 rootfs。“

也就是说,出题人只能对 rootfs 里面的文件做些手脚,不能搞一些大的操作

那么就看看哪些文件是最近被更改过的,这里面应该就有信息

而官方题解又提到:” 看到 ld.so.preload 都该有所警觉了吧,这个文件打开的内容是 /usr/libexec/libexec.so“

这个 ld.so.preload 是个特殊的文本文件,在 linux 程序链接的时候起作用,相当于告诉 linux:在运行这个程序动态链接的时候,先链接这个文本文件里面指出的那些动态链接库。因此这个东西有很强的 hook 劫持作用。这个文件打开了 libexec.so 文件,那么这个文件里面肯定有一些东西,反编译它看看

果然,它 hook 了 read 和 strncmp 两个函数

一个是异或 1,一个是根据不同的字符进行加减 13 的操作然后再比较,回到原来的主逻辑可以发现

read 出现在 base64 之前,strncmp 在 base64 之后

那么加密逻辑就是异或 1-> 魔改 base64-> 根据字符加减 13

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import sys

BASE64_REV = [-1] * 256
alphabet = "TUVWXYZabcdefghijABCDEF456789GHIJKLMNOPQRSklmnopqrstuvwxyz0123+/"
for i in range(64):
BASE64_REV[ord(alphabet[i])] = i


def decode_custom_base64(encoded_str: str) -> bytes:
if not encoded_str:
return None

encoded_len = len(encoded_str)
if encoded_len % 4 != 0:
sys.stderr.write("length error\n")
return None

pad = 0
if encoded_len >= 1 and encoded_str[encoded_len - 1] == '=':
pad += 1
if encoded_len >= 2 and encoded_str[encoded_len - 2] == '=':
pad += 1

decoded_len = (encoded_len // 4) * 3 - pad
decoded_bytes = bytearray(decoded_len)

decoded_index = 0
for i in range(0, encoded_len, 4):
ch1 = encoded_str[i]
ch2 = encoded_str[i + 1]
ch3 = encoded_str[i + 2]
ch4 = encoded_str[i + 3]

d1 = BASE64_REV[ord(ch1)] if ch1 != '=' else 0
d2 = BASE64_REV[ord(ch2)] if ch2 != '=' else 0
d3 = BASE64_REV[ord(ch3)] if ch3 != '=' else 0
d4 = BASE64_REV[ord(ch4)] if ch4 != '=' else 0

if ((ch1 != '=' and d1 < 0) or \
(ch2 != '=' and d2 < 0) or \
(ch3 != '=' and d3 < 0) or \
(ch4 != '=' and d4 < 0)):
sys.stderr.write("NO!\n")
return None

if ch3 == '=':
a = (d1 << 2) d2
decoded_bytes[decoded_index] = a
decoded_index += 1
elif ch4 == '=':
a = (d1 << 2) (d2 & 3)
b = ((d2 >> 2) << 4) d3

decoded_bytes[decoded_index] = a
decoded_index += 1
if decoded_index < decoded_len:
decoded_bytes[decoded_index] = b
decoded_index += 1
else:
a = (d1 << 2) (d2 & 3)
b = ((d2 >> 2) << 4) (d3 & 0xF)
c = ((d3 >> 4) << 6) d4

decoded_bytes[decoded_index] = a
decoded_index += 1
if decoded_index < decoded_len:
decoded_bytes[decoded_index] = b
decoded_index += 1
if decoded_index < decoded_len:
decoded_bytes[decoded_index] = c
decoded_index += 1

return bytes(decoded_bytes[:decoded_index])


if __name__ == "__main__":
EncryptedFlag = 'RPVIRN40R9PU67ue6RUH88Rgs65Bp8td8VQm4SPAT8Kj97QgVG=='
enc1 = ''
for i in range(len(EncryptedFlag)):
k = ord(EncryptedFlag[i])
for j in range(48, 123):
if 64 < j <= 77 and j + 13 == k:
enc1 += chr(j)
break
elif 77 < j <= 90 and j - 13 == k:
enc1 += chr(j)
break
elif 96 < j <= 109 and j + 13 == k:
enc1 += chr(j)
break
elif 109 < j <= 122 and j - 13 == k:
enc1 += chr(j)
break
elif 48 <= j <= 57 and j == k:
enc1 += chr(j)
break
elif EncryptedFlag[i] == '=':
enc1 += chr(k)
break

enc2_bytes = decode_custom_base64(enc1)

if enc2_bytes is not None:
enc2_hex = enc2_bytes.hex()
for i in range(0, len(enc2_hex), 2):
k = int(enc2_hex[i:i + 2], 16)
print(chr(k ^ 1), end='')
print()
else:
print("解码失败。")