0x00 前言
逆向课上闲来无事,想起之前丢下的一个坑,花了一天时间做完了。
算是第一次完整的逆向实践,记录一下。
目标:某国服PPT游戏,未加壳,解密加密图片和lua脚本
工具:
- IDA Pro 7.5
- Frida
0x01 简单分析
直接打开APK文件查看结构,确定该游戏客户端使用cocos2dx作为引擎。
尝试采用cocos2d通用的xxtea算法解密,看了几个文件之后发现文件头对不上,也没找到sign和key,应该不是。
加密的文件特征为前两个字节是0x55 0x46
,即字母的UF
。
Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: 55 46 00 01 05 21 20 3E 55 56 0E 0E 08 11 71 1A UF...!.>UV....q.
用JEB简单地看了一下Java类,没找到类似加密的方法。推测在封装的.so库中。
由于该客户端只引用了3个so库,很简单地锁定libcocos2dlua.so
,丢到IDA里面分析。
这里算是走了点弯路,因为开始还在想是不是改了xxtea(同样是固定字节),所以搜了xxtea
,其实直接定位luaLoadBuffer
会更快。
int __fastcall cocos2d::LuaStack::luaLoadBuffer(int a1, int a2, CCCrypto *this, unsigned __int8 *a4, int a5)
{
//...
else
{
if ( (int)a4 > 3 && *(_BYTE *)this == 85 && *((_BYTE *)this + 1) == 70 )
{
v22 = 0;
v23 = 0;
v24[0] = 0;
CCCrypto::decryptUF(this, a4, (int)&v22, v24, &v23, v13);
v10 = a2;
v11 = this;
v12 = (unsigned __int8 *)v23;
}
else
{
v11 = this;
v12 = a4;
v10 = a2;
}
result = j_luaL_loadbuffer(v10, (int)v11, (int)v12, a5);
}
return result;
}
很显然加密用的就是这个CCCrypto::decryptUF
了,点进去看看:
int __fastcall CCCrypto::decryptUF(CCCrypto *pInBuff, unsigned __int8 *inlen, int a2, int *a3, int *a4, int *a5)
{
//...
if ( (int)inlen <= 3 )
{
v26 = 1;
return -v26;
}
if ( *(_BYTE *)pInBuff != 0x55 || *((_BYTE *)pInBuff + 1) != 0x46 )
{
v26 = 2;
return -v26;
}
//...
}
没错,就是你!
搜索decryptUF
可以定位另一个用于加密的函数decryptUFImage
,其实只要根据反汇编抄代码就行了,但是看的有点头大所以决定先跑HOOK。
0x02 Hook尝试
没做过Android Hook,先查资料。
锁定Firda,不需要编译,直接远程跑脚本就行。
服务端直接用Magisk Frida,少一步手动启用Frida。
写js脚本:
setTimeout(function(){
Java.performNow(function () {
var rawbyte;
var rawlen;
var luaL_loadbuffer = Module.findExportByName("libcocos2dlua.so", "_ZN8CCCrypto14decryptUFImageEPhi");
Interceptor.attach(luaL_loadbuffer, {
onEnter: function(args) {
rawbyte = args[0];
rawlen = args[2];
//console.log(args[1].readByteArray(20))
},
onLeave: function (retval) {
send('isbyte',rawbyte.readByteArray(rawlen.toInt32()));
}
} );
});
}, 100)
这个是抓的图片,lua脚本也类似。
对应的python脚本:
# -*- coding: utf-8 -*-
# adb forward tcp:27043 tcp:27043
# adb forward tcp:27042 tcp:27042
import frida
import sys
import os
import random
def generate_random_str(randomlength=16):
"""
无法hook文件路径,暂时用随机串生成文件名
"""
random_str = ''
base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789'
length = len(base_str) - 1
for i in range(randomlength):
random_str += base_str[random.randint(0, length)]
return random_str
def write(path, content):
print('write:', path)
folder = os.path.dirname(path)
if not os.path.exists(folder):
os.makedirs(folder)
open(path, 'wb').write(content)
def on_message(message, data):
if message['payload']=='isbyte':
name = "test"+generate_random_str()+".png"
name = "./output/"+ name
content = data
dirName = os.path.dirname(name)
if not os.path.exists(dirName):
os.makedirs(os.path.dirname(name))
write(name, content)
else:
print(message)
device = frida.get_usb_device(timeout=100)
pid = device.spawn(["com.xxxxxxx"])
session = device.attach(pid)
device.resume(pid)
jscode = open('test.js', 'r',encoding='utf-8').read()
script = session.create_script(jscode)
script.on("message", on_message)
script.load()
sys.stdin.read()
直接执行,自动拉起App,抓取数据。
0x03 重写解密
Hook嘛,内容不一定完整。于是还是回到阅读解密函数。
先从简单的图片解密看起:
//generated by The Interactive Disassembler (IDA)
unsigned __int8 *__fastcall CCCrypto::decryptUFImage(CCCrypto *this, unsigned __int8 *a2, int a3)
{
unsigned __int8 *result; // r0
CCCrypto *v5; // r2
char v6; // r6
char v7; // r5
char v8; // r4
char *i; // r2
unsigned __int8 *v10; // r1
char v11; // [sp+4h] [bp-1Ch]
result = a2;
if ( (int)a2 > 0x19 && *(_BYTE *)this == 0x55 && *((_BYTE *)this + 1) == 0x46 )
{
v11 = *((_BYTE *)this + 5);
v5 = this;
v6 = *((_BYTE *)this + 2);
v7 = *((_BYTE *)this + 3);
v8 = *((_BYTE *)this + 4);
do
{
*(_BYTE *)v5 = *((_BYTE *)v5 + 6);
v5 = (CCCrypto *)((char *)v5 + 1);
}
while ( v5 != (CCCrypto *)((char *)this + 20) );
*((_BYTE *)this + 21) = v7;
*((_BYTE *)this + 20) = v6;
*((_BYTE *)this + 23) = v11;
*((_BYTE *)this + 22) = v8;
for ( i = (char *)this + 24; ; ++i )
{
result = a2 - 2;
if ( i - (char *)this >= (int)(a2 - 2) )
break;
*i = i[2];
}
v10 = &a2[(_DWORD)this];
*(v10 - 2) = 0;
*(v10 - 1) = 0;
}
return result;
}
看得出来只是移位并添加了几个开头的字节,结合hook到的解密后图片,写出解密代码:
def decryptUFImg(img: bytes):
img=list(img)
if (int(len(img)) < 0x1A or img[0] != 0x55 or img[1] != 0x46):
return (0,)
shift = img[2:6]
img[0:21]=img[6:27]
img[20:24] = shift
img[24:-2]=img[26:]
img[-2:]=[0,0]
return bytes(img)
def replaceImg(imgpath):
imgbyte = ()
with open(imgpath, "rb") as r:
imgbyte = r.read()
decode = decryptUFImg(imgbyte)
if (decode[0] != 0x00):
with open(imgpath, "wb") as w:
w.write(decode)
print(f"decode img {imgpath}")
简单到不配叫加密。
decryptUF:
//generated by The Interactive Disassembler (IDA)
int __fastcall CCCrypto::decryptUF(CCCrypto *pInBuff, unsigned __int8 *inlen, int a2, int *a3, int *a4, int *a5)
{
int v6; // r4
int v7; // r3
char *v8; // r7
int v10; // r0
int v11; // r2
char *v12; // r6
unsigned __int8 *v13; // r4
int v14; // r5
int v15; // r1
unsigned int v16; // r7
unsigned int v17; // r4
unsigned __int8 *v18; // r6
int v19; // r1
int v20; // r3
int v21; // r1
int v22; // r2
CCCrypto *v23; // r3
unsigned __int8 *v24; // r1
int v26; // r0
int v27; // [sp+0h] [bp-20h]
int v28; // [sp+0h] [bp-20h]
unsigned int v29; // [sp+4h] [bp-1Ch]
if ( (int)inlen <= 3 )
{
v26 = 1;
return -v26;
}
if ( *(_BYTE *)pInBuff != 0x55 || *((_BYTE *)pInBuff + 1) != 0x46 )
{
v26 = 2;
return -v26;
}
if ( *((_BYTE *)pInBuff + 2) == 0x4F )
{
*a3 = *((unsigned __int8 *)pInBuff + 3);
v6 = 1;
v7 = 4;
}
else
{
v6 = 0;
v7 = 2;
}
v8 = (char *)pInBuff + v7;
*(_DWORD *)a2 = *((unsigned __int8 *)pInBuff + v7);
v10 = *((unsigned __int8 *)pInBuff + v7 + 1);
v11 = v7 + 2;
if ( v10 == 1 )
{
v12 = v8;
v27 = -v7;
v13 = &inlen[-v7 - 3];
v14 = *((unsigned __int8 *)pInBuff + v11) - (_DWORD)pInBuff - v7;
while ( v12 - v8 < (int)v13 )
{
j_j___aeabi_idivmod((int)&v12[v14], 0x21);
v12[v27] = v12[3] ^ byte_B82264[v15];
++v12;
}
*a4 = (int)v13;
}
else if ( v10 == 2 )
{
v16 = 0;
v17 = v7 + 3;
v18 = &inlen[-v7 - 3];
v28 = *((unsigned __int8 *)pInBuff + v11);
while ( v16 < v17 )
{
j_j___aeabi_idivmod(v16 + v28, 33);
*((_BYTE *)pInBuff + v16) = v18[(_DWORD)pInBuff + v16] ^ byte_B82264[v19 + 36];
++v16;
}
v20 = (int)&v18[-v17];
if ( (int)&v18[-v17] > 95 )
v20 = 95;
v29 = v20 + v17;
while ( v17 < v29 )
{
j_j___aeabi_idivmod(v17 + v28, 33);
*((_BYTE *)pInBuff + v17++) ^= byte_B82264[v21 + 36];
}
*a4 = (int)v18;
}
else
{
v22 = 7;
if ( !v6 )
v22 = 5;
v23 = pInBuff;
v24 = &inlen[-v22];
while ( v23 - pInBuff < (int)v24 )
{
*(_BYTE *)v23 = *((_BYTE *)v23 + v22);
v23 = (CCCrypto *)((char *)v23 + 1);
}
*a4 = (int)v24;
}
return 0;
}
这个比较麻烦,变量太多看起来头大。
结合csdn大佬提供的之前版本的decryptUF,写出解密函数如下:
import os
RKey=[0x11, 0x2B, 0x65, 0x78, 0x17, 0xC, 0xD, 0x13, 0x15,
0x35, 0x62, 0x6F, 0x7B, 0x62, 0x15, 0x7F, 0x11, 0x2C,
0x63, 0x17, 0x16, 0x57, 0xC, 0x59, 0xB, 0x20, 0x65,
0x21, 0x20, 0x63, 0xC, 0x7F, 8, 0, 0, 0, 0x11, 0x2B,
0x65, 0x78, 0x17, 0xC, 0xD, 0x13, 0x15, 0x35, 0x62,
0x6F, 0x7B, 0x62, 0x15, 0x7F, 0x11, 0x2C, 0x63, 0x17,
0x16, 0x57, 0xC, 0x59, 0xB, 0x20, 0x65, 0x21, 0x20,
0x63, 0xC, 0x7F, 8, 0, 0, 0]
def decryptUFLua(buf):
buf = list(buf)
buflen = len(buf)
if (buflen < 3 or buf[0] != 0x55 or buf[1] != 0x46):
return (0,)
delen = buflen - 5
a = 0
b = 2
if (buf[2] == 0x4f):
a = 1
b = 4
cbit = buf[b + 1]
if (cbit == 0x01):
i = b
sbit = buf[b + 2] - 2
while (i - b < delen):
buf[i - b] = buf[i + 3] ^ RKey[(i + sbit) % 33]
i = i + 1
elif (cbit == 0x02):
print("cbit==2 detected")
i = 0
j = b + 3
sbit = buf[b + 2]
while (i < j):
buf[i] = buf[i + delen] ^ RKey[(i + sbit) % 33 + 36]
i = i + 1
sbit2 = 95 if (delen - j > 95) else (delen - j)
while (j < sbit2 + j):
buf[j] = buf[j] ^ RKey[(j + sbit) % 33 + 36]
j += 1
else:
print("cbit==else detected")
sbit = 7 if (a) else 5
i = 0
v24 = buflen - sbit
while (i < v24):
buf[i] = buf[i + sbit]
i = i + 1
return bytes(buf[:-5])
def replaceLua(path):
bufbyte = ()
with open(path, "rb") as r:
bufbyte = r.read()
decode = decryptUFLua(bufbyte)
if (decode[0] != 0x00):
with open(path[:-1], "wb") as w:
w.write(decode)
print(f"decode luac {path}")
os.remove(path)
值得一提的是这个算法虽然看上去很长,但是实际上解密的时候根本没跑到过下面两个分支,也就是说这个代码可以精简为:
def decryptUFLua(buf):
buf = list(buf)
buflen = len(buf)
if (buflen < 3 or buf[0] != 0x55 or buf[1] != 0x46 or buf[3] != 0x01):
return (0,)
sbit = buf[4] - 2
for i in range(2,buflen-3):
buf[i - 2] = buf[i + 3] ^ RKey[(i + sbit) % 33]
return bytes(buf[:-5])
写个文件夹遍历,完成一次性解密:
def DFT(rootpath):
stack = [rootpath]
while stack != []:
path = stack.pop()
if os.path.isdir(path):
for a_path in os.listdir(path):
stack.append(path + '/' + a_path)
if (path.endswith(".png") or path.endswith(".jpg")):
replaceImg(path)
elif (path.endswith(".luac")):
replaceLua(path)
上次修改於 2021-03-27