猿人学 App 第二题「简单的 so 层加密」离机复现:一个改版 RC4 的完整还原

本文最后更新于:2026年9月1日 下午

[猿人学 App 逆向系列]第二篇。第二题「简单的 so 层加密」:请求参数 data 由 native 加密。
这题的 so 不加壳、不混淆,可以直接反汇编读算法——是练”读一个改版 RC4”的好素材。本文记录怎么读 encrypt、认出它是带 header 的改版 RC4,并用 Frida 主动调用当预言机对拍,定位两个只看反汇编容易漏的细节,最后还原成纯 Python。

⚠️ 猿人学闯关 App 是面向逆向练习的 CTF 式靶场;本文仅用于授权范围内的学习研究,请遵守相关服务条款与法律。

本文目录

  • 请求模型(静态反编译)
  • libtwo.so 侦察:不加壳不混淆
  • 读 encrypt:header + 改版 RC4
  • Frida 预言机对拍,定位两个坑
  • 纯 Python 还原

一、请求模型(静态反编译)

ChallengeTwoFragment 里翻页:

1
2
3
4
String content = String.format("%d:%d", page, System.currentTimeMillis());   // "1:1788000000000"
byte[] enc = NativeLib.encrypt(content.getBytes(UTF_8), currentTimeMillis);
String data = Base64.encodeToString(enc, 10); // flag 10 = URL_SAFE|NO_WRAP
service.OooOOo(data); // @POST("/api/app2") @Field("data")

NativeLibcom.yuanrenxue.challenge.two):static { System.loadLibrary("two"); }public static native byte[] encrypt(byte[], long)。核心就是 libtwo.so 的 encrypt

二、libtwo.so 侦察:不加壳不混淆

lief/capstone 一看就轻松了:libtwo.so 只有 10KB,没有 UPX!.text 是正常指令(不是第十一题那种 udf),encrypt静态导出 Java_com_yuanrenxue_challenge_two_NativeLib_encrypt,函数序言干净(sub sp,sp,#0x50; stp x29,x30...)。也没有第四题那种 goron 间接跳转。直接反汇编读即可。

三、读 encrypt:header + 改版 RC4

外壳(Java_..._encrypt)逻辑很直白:

  1. GetByteArrayElements 取明文指针、GetArrayLength 取长度 len
  2. 输出长度 = len + 32malloc
  3. 调核心加密(x0=明文, x1=输出, x2=len+32, x3=ts);
  4. NewByteArray(len+32) + SetByteArrayRegion 返回。

核心(sub_c70)读下来是一个带 32 字节 header 的改版 RC4

  • **header(输出前 32 字节)= key1(16) ++ key2(16)**:
    • key1:由 ts 经一个 LCG 派生——x = x*1103515245 + 12345,取 (x>>16)&0x7fff,正是 glibc rand() 的内核;连续取 8 个、每个拆成 2 字节,凑 16 字节。
    • key2std::random_device 生成的 16 随机字节。
  • KSAS=0..255,先用 key1 跑 768 次(= 3 遍),再用 key2 跑 768 次;
  • PRGA:对明文逐字节异或 keystream。
  • 两把 key 都写进 header 发给服务器 —— 服务器据此重建同一个 S 盒、还原 keystream 解密(类似”把密钥material 摊在包头”的思路)。

KSA / PRGA 的 j 更新,都比标准 RC4 多一层 S 盒查表

1
2
3
KSA :  j = S[(j + S[i%256] + key[i%16]) & 0xff] ;  swap(S[i%256], S[j])
PRGA: j = S[(j + S[a]) & 0xff]
keystream = S[ S[ (S[j] + 1) & 0xff ] ] ; swap(S[a], S[j]) ; a=(a+1)&0xff

四、Frida 预言机对拍,定位两个坑

只看反汇编,有两处极易翻车。这题 so 就在真机上跑着,直接 Frida 主动调用 encrypt 当预言机最省事:拿到 env(JavaVM → AttachCurrentThread),用 NewByteArray/SetByteArrayRegion 造入参,调 Java_..._encrypt(env, 0, jarr, ts),再 GetByteArrayElements 读返回——喂任意 (明文, ts) 拿真机输出,和 Python 逐字节对拍。

对拍逼出两个细节:

  1. KSA 的 j 也带 S[] 包裹:反汇编里 and w9,#0xff 后还有一条 ldrb w9,[x8,w9]x8 是 S 盒基址)。漏掉它,header 对、但密文全错。
  2. key1 不能乱填:一开始以为两把 key 都能随机(都在 header 里嘛),结果服务器回 status=2。原因是服务器用明文里的 ts 重算 key1、和 header 里的 key1 比对——所以 key1 必须由 ts 经那套 LCG 派生;只有 key2 能随机。

对齐这两点后,Python 输出与真机 encrypt 逐字节一致,服务器也返回 status=1

五、纯 Python 还原

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
import base64, os, time, requests

def _lcg(x): return ((x*1103515245 + 12345) >> 16) & 0x7fff # glibc rand

def _key1(ms):
v=ms & 0xffffffff; b=bytearray()
for _ in range(8):
v=_lcg(v); b.append((v>>8)&0xff); b.append(v&0xff)
return bytes(b)

def _encrypt(pt, k1, k2):
S=list(range(256)); jj=0
for key in (k1,k2): # KSA 各 768 次(先 key1 后 key2)
for i in range(768):
si=i%256
jj=S[(jj+S[si]+key[i%16])&0xff] # 带 S[] 包裹
S[si],S[jj]=S[jj],S[si]
out=bytearray(k1+k2); a=0 # header = key1 ++ key2
for p in pt: # PRGA(改版)
jj=S[(jj+S[a])&0xff]
t=S[S[(S[jj]+1)&0xff]]
out.append(p^t); S[a],S[jj]=S[jj],S[a]; a=(a+1)&0xff
return bytes(out)

def data(page, ms):
return base64.urlsafe_b64encode(_encrypt(f"{page}:{ms}".encode(), _key1(ms), os.urandom(16))).decode()

def fetch(page, s):
ms=int(time.time()*1000)
j=s.post("https://www.python-spider.com/api/app2", data={"data":data(page,ms)},
headers={"User-Agent":"okhttp/4.9.2"}, timeout=10).json()
return [int(x["value"].strip()) for x in j["data"]]

print(fetch(1, requests.Session())) # key1 由 ts 派生、key2 随机 -> 服务器认,取回该页数据

到这一步就完全离机:不碰手机、纯 Python 复刻 native 的改版 RC4 即可逐页取数。key2 随机、ts 也不卡新鲜度,只要 key1ts 自洽即可。

小结

“so 层加密”听着唬人,实际这题 so 既不加壳也不混淆,encrypt 是个可直读的改版 RC4。真正的经验是:别只信眼睛——so 就在真机上,Frida 主动调用当预言机对拍,几秒就能揪出”KSA 少一层 S[] 包裹””key1 必须由 ts 派生”这种纯看反汇编容易漏的细节。往后第十一题会遇到加壳(读不了就 dump 内存),第四题会遇到 goron(读不动就 Unicorn 模拟),本题则是”读得动,但要对拍验证”的典型。


猿人学 App 第二题「简单的 so 层加密」离机复现:一个改版 RC4 的完整还原
https://kingjem.github.io/2026/09/01/猿人学App第二题离机复现-改版RC4还原/
作者
Ruhai
发布于
2026年9月1日
许可协议