1.先抓包:参数体有2次动态加密,请求头看着也有2个,请求体的以后有时间再说
2.然后就可以用frida去hook对应的sp和sig的,比如hashmap和jsonObject都可以
3.然后在点两下跟踪到这里了
4.ida打开对应的so,发现是动态加密,用动态注册脚本去hook一下对应地址
5.跳到0x209a4地址去看下,看着函数不大,静态分析没发现敏感函数
6.我喜欢从下到上分析,一直跟的话就会到下图这里,下图v59就是返回值
7.静态分析到这里结束了,因为unidbg比较容易分析算法,所以开始搭一下架子,都不难我就贴出来了
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.file.linux.AndroidFileIO;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.api.PackageInfo;
import com.github.unidbg.linux.android.dvm.api.Signature;
import com.github.unidbg.linux.android.dvm.array.ArrayObject;
import com.github.unidbg.linux.file.ByteArrayFileIO;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.memory.MemoryBlock;
import com.github.unidbg.pointer.UnidbgPointer;
import com.sun.jna.StringArray;
import net.dongliu.apk.parser.bean.CertificateMeta;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class A extends AbstractJni implements IOResolver {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final DvmClass CheckCodeUtil;
private final Memory memory;
private final DalvikModule dvm;
public A(){
emulator = AndroidEmulatorBuilder.for64Bit()
.setProcessName("")
.build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); //设置系统类库解析
// emulator.getSyscallHandler().addIOResolver(this);
vm = emulator.createDalvikVM(new File("apk")); // 创建Android虚拟机
//vm.setVerbose(true); // 设置是否打印Jni调用细节
vm.setJni(this);
dvm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\boss\\apk\\libyzwg.so"), true); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
module = dvm.getModule(); // 加载好的libttEncrypt.so对应为一个模块
vm.callJNI_OnLoad(emulator,module);
CheckCodeUtil = vm.resolveClass("com/twl/signer/YZWG");
}
public static int randInt(int min, int max) {
Random rand = new Random();
return rand.nextInt((max - min) + 1) + min;
}
public A(AndroidEmulator emulator, VM vm, Module module, DvmClass checkCodeUtil, Memory memory, DalvikModule dvm) {
this.emulator = emulator;
this.vm = vm;
this.module = module;
CheckCodeUtil = checkCodeUtil;
this.memory = memory;
this.dvm = dvm;
}
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature){
case "android/content/pm/PackageManager->getPackagesForUid(I)[Ljava/lang/String;":
return new ArrayObject(new StringObject(vm,vm.getPackageName()));
}
return super.callObjectMethod(vm,dvmObject,signature,varArg);
}
@Override
public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) {
switch (signature) {
case "android/content/pm/PackageInfo->signatures:[Landroid/content/pm/Signature;":
PackageInfo packageInfo = (PackageInfo) dvmObject;
if (packageInfo.getPackageName().equals(vm.getPackageName())) {
CertificateMeta[] metas = vm.getSignatures();
if (metas != null) {
Signature[] signatures = new Signature[metas.length];
for (int i = 0; i < metas.length; i++) {
signatures[i] = new Signature(vm, metas[i]);
}
return new ArrayObject(signatures);
}
}
}
throw new UnsupportedOperationException(signature);
}
@Override
public int callIntMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature) {
case "java/lang/String->hashCode()I":
String string = dvmObject.getValue().toString();
return string.hashCode();
}
return super.callIntMethod(vm, dvmObject, signature, varArg);
}
public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
switch (signature) {
case "com/twl/signer/YZWG->gContext:Landroid/content/Context;": {
return vm.resolveClass("android/app/Activity",vm.resolveClass("android/content/ContextWrapper",vm.resolveClass("android/content/Context"))).newObject(null);
}
}
return super.getStaticObjectField(vm,dvmClass,signature);
}
public void nativeEncodeRequest(){
//这里的参数是前面hook java层得到的
String str1 = "client_info=%7B%22version%22%3A%2212%22%2C%22os%22%3A%22Android%22%2C%22start_time%22%3A%221735141170757%22%2C%22resume_time%22%3A%221735141170758%22%2C%22channel%22%3A%2228%22%2C%22model%22%3A%22Android%7C%7C22127RK46C%22%2C%22dzt%22%3A0%2C%22loc_per%22%3A0%2C%22uniqid%22%3A%222cd19eaa-9e9b-4a38-83a1-1ea5fecf70e6%22%2C%22oaid%22%3A%22NA%22%2C%22oaid_honor%22%3A%22NA%22%2C%22did%22%3A%22DUh8qhSRH0dLEKQrusw_A7mX6SXI4lfP1tb9RFVoOHFoU1JIMGRMRUtRcnVzd19BN21YNlNYSTRsZlAxdGI5c2h1%22%2C%22tinker_id%22%3A%22Prod-arm64-v8a-release-12.190.1219010_1018-11-55-40%22%2C%22is_bg_req%22%3A0%2C%22network%22%3A%22none%22%2C%22operator%22%3A%22UNKNOWN%22%2C%22abi%22%3A1%7D&curidentity=0&data=%5B%7B%22action%22%3A%22f1-tab%22%2C%22time%22%3A1735141987811%7D%5D&req_time=1735141988331&uniqid=2cd19eaa-9e9b-4a38-83a1-1ea5fecf70e6&v=12.190";
String str2 = "39982a32e92e18603e73ee090e774bc9";
try {
byte[] bytes = str1.getBytes("UTF-8");
DvmObject ret = CheckCodeUtil.callStaticJniMethodObject(emulator, "nativeEncodeRequest([BLjava/lang/String;)Ljava/lang/String;",bytes,str2);
String strOut = (String)ret.getValue();
System.out.println(strOut);
}catch (Exception e){
}
}
public static void main(String[] args) {
A a = new A();
a.nativeEncodeRequest();
}
}
8.这样就完成了,主动调用的话就会出结果,我想的是和frida hook结果对比了一下,是一致的
9.本篇是以学习为目的,所以接着分析,上图有讲v59就是返回值,不确定的话可以打断点看一下,v59是v23返回的
所以分析看一下sub_1CEB8(v23, v19)和sub_29E90(v23, size_4, v56);这2个函数
10,静态分析看了一下,sub_1CEB8函数什么也没做,在看看sub_29E90,先说参数a1是结果,a2是数据,a3是长度(我是分析完成写的文章,就不想在贴参数图了,有兴趣的自己去分析一下),这里像是对Base64编码的字符集做了一些操作,然后对传进来的a1和a2数据做了一些处理,这里的v23就是a1,就是我们想要的返回值
11,我照着这个还原了一下python
def sub_29E90(self,a1, a2, a3):
v5 = a1
v17 = 0
v23 = []
i = 0
while True:
if v17 > a3 - 2:
break
v23 = v5
v24 = v17
v23[i * 4 + 0] = aAbcdefghijklmn[(a2[v24] >> 2) & 0x3F]
# v23[1] 使用位运算和 Base64 字符集映射
v6 = v24 + 1
v23[i * 4 + 1] = aAbcdefghijklmn[((a2[v6] >> 4) & 0x3F) | (16 * (a2[v24] & 3))]
# 获取新的字节 v7 和 v8
v7 = a2[v6]
v8 = v24 + 2
# 更新 v23 数组
v23[i * 4 + 2] = aAbcdefghijklmn[((a2[v8] >> 6) & 0x3F) | (4 * (v7 & 0xF))]
v23[i * 4 + 3] = aAbcdefghijklmn[(a2[v8] ^ 0xC0) & a2[v8]]
# 更新 v17
v17 = v24 + 3
i += 1
return ''.join(v23)
12.接着分析size_4,很明显来源于sub_2E91C(v62, size_4, size_4, v56);函数,这里就很明显了,是对a1和a2得值进行了一系列或与操作,赋值给了a3也就是size_4
13 照常还原就行,因为我菜,真正分析的时候还是要hook数据进行比对,需要费点时间多次尝试了,直接拿gpt给的还原用,多次改明文尝试a1目前来看是固定的(这里我也好奇,没看见a1是在哪里赋值的,就看见初始化了。有大佬知道的话希望告知一下,其实a1在这里还不能固定,后面再说,记住这里是分析了2E91C这个函数),a2,a3是一样的,a4是长度,所以后面要分析a2了
def sub_2E91C(self,result, a2, a3, a4):
v4 = 0
while True:
v10 = -1033944663
while v10 != 491288569:
if v10 == -344237315:
# 执行字节操作
v5 = (result[257]) & 0xFF
v6 = (result[256] + 1) & 0xFF
result[256] = v6 & 0xFF
v7 = (v5 + (result[v6] & 0xFF)) & 0xFF
result[257] = v7
v8 = result[v6] & 0xFF
result[v6] = result[v7]
result[v7] = v8 & 0xFF
v9 = result[(result[result[257]] + result[result[256]]) & 0xFF]
a3[v4] = ((~v9 & 0x7B | v9 & 0x84) ^ (~a3[v4] & 0x7B | a3[v4] & 0x84))
# print(a3.hex())
# print(hex(v9))
if a3.hex().find('e4b778b3ef05c08093c78d68') > -1:
pass
v4 += 1
if v4 >= a4:
v10 = 491288569
else:
if v4 >= a4:
v10 = 491288569
else:
v10 = -344237315
if v10 == 491288569:
break
return result
14.a2的后按x键继续向上找,发现了 sub_1D444函数很可疑,点过去看看,看起来很像
15 经过断点hook验证,确实是这个地方对a1做了处理,然后返回的a2就是我们要的结果,这里的话,我就直说了,a1是传进来的明文str1,a2是申请的内存空间存放返回结果,a3是明文长度,a4是最大的压缩长度,先是对a1进行了lz4压缩算法,然后在填充固定值返回的a2,lz4算法后面跟踪发现函数太大打不开了,我改了
ida的max_function的值就会崩溃,所以尝试用python的lz4算法,发现和c运行的结果不一致,可能需要到github找到lz4算法源码进行编译,尝试在运行看看是否一致,因为我的c基础不行,就先不弄了,有大佬有兴趣了可以去尝试
16.到上面我当时以为就分析结束了,就不管了,后来分析了nativeSignature函数的时候有了意外发现,这个后面再说,篇幅太长了,后面我说快点
17.我们接着来到0x21864这个函数去看加密,先主动调用一下
public void sign(){
//这里的参数是前面hook java层得到的
String str1 = "sp";
String str2 = "5Ld4s-8FwICTx41oreQzXrOQ8ZW6jB1TNEhFs1f-O6e6w1oxuIMlXFJg7rcP5zsEP7js8mV2Lzs3qmDmaNi4ZTaU0LEBh7Bt1wck0OdTDh59YMIicVU-gnnbTjMBEVJbmtez21QdJxrD6kpMhC4Z9TGaaX03GA1nIq1avlufhEcelG_qO4GykesoPkw8G7U2yso9DLcvzla9P2LuD4F0BlpkAjcpxGJwGkDL9LhR52-AUrDEJI_8sCxe7jIjTiH4ReJkMeoDt4NSEqaUzjDsvHz_U5A8xaoIwbO7Hbj4IY3HFsHBJsoGTXzHs36y2sBv1PYkeO8XuUbuTKnMaWt0SDTCiE4GZ_8dSgYKghqGm3S1gbgtQkSy-pqFtKfLZV1qNvIO1yOROgIeJVE-OVAM12fVe4VAUzGq3NrU7jrIQLhEExom1p761emhP37pyt-vdWZY6InLZKhNrni58339R5uOOt-hcGiD9N1Q7axM1p41iksXsIMmybC2gAtsRBmN25NL56Y6eOMkcA0lWGOin77b3HbG8Ea3_FRjJWWKu41IWnpEuX5VAF4WDndNuNeOqJcn9cGPZ1RvbtIz9SrMrBzklcPLZ0olq_tUWufNOGjEnIHdld6XORVGf-pbOXYGhIaW3DQ73X9rD2NY-KvIENO8SpnS49n9GYr59b2gAQkNz1pF-i9yXA2AD8jPsa48A3ET84cGBO6xaBmNrZVxYYFoMSDIt6ic66-3BFOS7kVeHHhqMFsjcfdDF5VvgypozZpfnfMpRs22MhAHpm2LWuS-5V45I7emCB1zsLDSq2dp";
//str2="hello";
System.out.println("长度2:"+str2.length());
try {
byte[] bytes = str1.getBytes("UTF-8");
DvmObject ret = CheckCodeUtil.callStaticJniMethodObject(emulator, "nativeSignature([BLjava/lang/String;)[B",bytes,str2);
byte[] strOut = (byte[])ret.getValue();
String s = new String(strOut);
System.out.println(s);
}catch (Exception e){
}
}
18.返回值看着像md5,虽然长度不是,猜测很可能是拼接出来的,通过静态分析到了v27这里,返回值就是加密,hook对应的入参发现不止一个有1个,发现2个参数拼接起来就是返回值
19,接着往上分析,sub_1C714(dest, v40)函数,看了一下mx0,好家伙就是我们的str+固定值+str2,接着我就没往下看了,直接md5,果然和想的一样是标准md5加密,到此分析结束。
20.分析MD5的时候发现用了str2,但是上面那个算法好像没用上感觉有点奇怪,正常应该会用上,改了一下str2的入参(之前一直改str1入参尝试的),果然有一个地方的值就不对了。经过分析发现sub_2E91C就是这里的a1不是之前想的固定值,所以继续回来分析
21。其实这里就发现了有一个sub_2E680函数,通过hook参数发现,s就是盐值+str2,v62我还是没发现从哪里给的值,改了str1和str2的入参v62都是固定的,v53是长度,接下来就是扣2E680的算法了
22.我这里直接给gpt生成的算法,我也懒的优化了,到这里和之前的上文2E91C的a1固定值替换上就ok了
def sub_2E680(self,result, a2, a3):
x = 0
v5 = ~((x - 1) * x) | 0xFFFFFFFE # Equivalent of ~((_BYTE)x - 1) * (_BYTE)x | 0xFFFFFFFE
y = 0
v10 = y < 10 # Assuming 'y' is some global variable (not defined in the function)
v6 = 1857882829
v9 = v5 == -1
v12 = 0 # Assuming initial value of v12
v13 = 0 # Assuming initial value of v13
v4 = 0 # Assuming initial value of v4
while True:
while True:
while True:
while v6 > -490188628:
if v6 <= 1562097807:
if v6 == -490188627:
v8 = result[v12] & 0xFF
v3 = (a2[(v12 % a3)] + v8 + v13) & 0xFF
result[v12] = result[v3] & 0xFF
result[v3] = v8
v6 = 1644006132
v4 = v12 + 1
else:
v5 = 0
v6 = -2137814577
elif v6 == 1562097808:
v7 = (((x - 1) * x) ^ 0xFFFFFFFE) & ((x - 1) * x) == 0
if (y < 10 and v7) or (y < 10) ^ v7:
v6 = 1400577677
else:
v6 = -1905400192
elif v6 == 1644006132:
v12 = v4
v13 = v3
if v4 >= 256:
v6 = -1783888654
else:
v6 = -490188627
elif ((not v9) ^ (not v10)) & 1 or (v9 and v10):
v6 = 1562097808
else:
v6 = -1905400192
if v6 > -1879526958:
break
if v6 == -2137814577:
v11 = v5
if v5 >= 256:
v6 = -1375567831
else:
v6 = -1879526957
else:
v6 = 1562097808
if v6 != -1879526957:
break
result[v11] = v11
v6 = -2137814577
v5 = v11 + 1
if v6 != -1375567831:
break
v4 = 0
v3 = 0
result[256] = 0
v6 = 1644006132
return result.hex()
23.EncodeRequest算法总结:
第1步:固定值和(入参+盐值)进行运算
第2步lz4压缩算法:
第3步:第一步的值和lz4算法的值进行异或
第4步:对第三步的值进行base64编码