安卓逆向 某词典app接口破解流程

admin 2024年11月23日01:48:19评论11 views字数 18673阅读62分14秒阅读模式

准备工具

  • Android模拟器:逍遥模拟器
  • Anroid抓包代.理软件: Drony 1.3.154
  • PC抓包软件: Fiddler
  • apk反编译工具: jadx 1.3.0
  • Android so库反编译工具:IDA Pro,站内有,这儿就不放链接了
安卓逆向  某词典app接口破解流程

,提取码uurr

沪江小D词典

首先安装好模拟器,apk包,还有抓包工具。需要在Android配置根证书才能抓HTTPS的包,站内应该有不少抓包的教程,不行我回头补一篇。

进入沪江小D词典app以后,可以直接点击试用,不用注册。

沪江小D查单词分两个接口,一个是弹窗快速查询接口/v10/quick/en/cn,一个是搜索栏的查询详情接口/v10/dict/en/cn

安卓逆向  某词典app接口破解流程

弹窗快速查询接口/v10/quick/en/cn分析

在Fiddler右边窗口中点击Composer,然后把请求拖过去可以看到请求的完整结构,包括请求头,请求body。

首先看/v10/quick/en/cn弹窗查词接口,POST请求,参数就word参数,返回值也很清晰,就是一个json结构。但是注意到请求头中有两个头hujiang-appkeyhujiang-appsign。从名称来开,hujiang-appkey是一个定值,但是hujiang-appsign是一个签名,而且不知道是如何签名的。

安卓逆向  某词典app接口破解流程

可以通过在postman中构造这个请求,主要填入下面几个关键信息:

  • 请求方式为POST,请求URL为https://dict.hjapi.com/v10/quick/en/cn
  • 请求参数为webForm格式,并且填入参数word=seven
  • 填入两个头hujiang-appkeyhujiang-appsign

可以得到返回值,并且修改参数word以后,不能获取返回值,那么请求头中的hujiang-appsign签名构造比如包含参数word,而且看签名结构可以大胆假设其就是md5签名,接下来需要弄清楚参与签名的字符串。

查询详情接口/v10/dict/en/cn分析

另外看详情查询接口/v10/dict/en/cn,其请求参数包含两个参数wordword_ext,请求头同样有hujiang-appkeyhujiang-appsign。最恶心的是其返回值json的data域是加密过的。

安卓逆向  某词典app接口破解流程

而且在postman中构造请求时,如果修改word_ext的值,则查不到结果,说明签名参数中还包含了word_ext

抓包分析总结

通过上面的抓包,基本确认了几个点:

  • 单词查询接口没有做特殊的用户限制,没有cookie也可以查询
  • 重要参数为wordword_ext,其含义为待查询的单词
  • 另外参数中的/en/cn表示根据英文查中文,通过切换app可以得到/jp/cn表示根据日文查中文
  • 需要弄清楚请求头中的hujiang-appsign签名规则
  • 需要弄清楚/v10/dict/en/cn接口返回值的解密方式

反编译apk

通过上面的抓包分析,我们还是不能构造和复用沪江的查词接口,换一个词我们就查不了,而且关键的详情查询接口有更多我们需要的数据,但是返回值我们无法解析。

针对上面这两点,只能通过反编译apk来看代码实现了。

反编译也很简单,打开jadx软件,首先选择文件 -> 首选项,勾选反混淆,因为apk代码一般是混淆过的,启用反混淆更方便搜索代码。

反编译之后,可以通过搜索接口来定位关联代码。

/v10/quick/en/cn反编译代码实现分析

直接搜索完整的url是没有结果的,说明这个url在代码里面是动态拼接的,尝试搜索/v10/quick,运气很好,只有一个匹配的代码。

安卓逆向  某词典app接口破解流程

双击进入包含/v10/quick所在的代码文件,可以看到这儿是定义了一个常量URL_LINK

安卓逆向  某词典app接口破解流程

继续搜索常量URL_LINK看在哪些地方被调用,找到发送HTTP请求的位置。

安卓逆向  某词典app接口破解流程

可以看到搜索结果都是在com.hujiang.supermenu.client.API这个类中,很显然这个类就是HTTP请求的构造类了。点进这个类可以看到常量URL_LINK在两个方法中引用。

安卓逆向  某词典app接口破解流程

复制代码隐藏代码

publicstaticvoidtranslateWord(Context context, String str, String str2, String str3, AbstractC12224a<String> aVar) {    Stringformat= String.format(getURL() + URL_LINK, str, str2);     C19398b.m141a("划词API" + format);     ((C12233h) ((C12233h) ((C12233h) ((C12233h) ((C12233h)newC12233h(context).m32079L(C12189g.f39935o)).m32058d("hujiang-appkey", APP_KEY)).m32058d("hujiang-appsign", getAppSign(str, str2, str3.trim()))).m32047j("word", str3)).m32051g0(format)).m32041p(String.class, aVar); }/* renamed from: d */// 双击this.f40105o可以看到其定义为 protected final Map<String, String> f40105o = new HashMap();public Rm32058d(String str, String str2) {    this.f40105o.put(str, str2);    returnthis; }// 签名函数publicstatic StringgetAppSign(String str, String str2, String str3) {    return md5(String.format(SIGN, str, str2, str3,"", APP_SECRET)); }publicstatic Stringmd5(String str) {    try {         byte[] digest = MessageDigest.getInstance("MD5").digest(str.getBytes("UTF-8"));         StringBuildersb=newStringBuilder(digest.length *2);         for (byte b : digest) {             inti= b &255;             if (i <16) {                 sb.append("0");             }             sb.append(Integer.toHexString(i));         }         return sb.toString();     }catch (UnsupportedEncodingException e) {         thrownewRuntimeException("Huh, UTF-8 should be supported?", e);     }catch (NoSuchAlgorithmException e2) {         thrownewRuntimeException("Huh, MD5 should be supported?", e2);     } }

很显然方法translateWord就是构造http请求的地方,而且注意看String.format(getURL() + URL_LINK, str, str2)用于构造url,注意到最后一行代码中反复出现的那个方法,这儿是m32058d(可能反混淆成其他名称),就是往HashMap中加key-value,进而设置hujiang-appkeyhujiang-appsign的请求头。

注意到其中的m32058d("hujiang-appsign", getAppSign(str, str2, str3.trim())))就是我们想要的签名构造方法。

先不要急着跳转到getAppSign函数中,我们先弄清楚这儿的三个参数分别是什么。直接看这个函数内部的实现是不能分析三个参数的含义的。

从URL的构造String.format(getURL() + URL_LINK, str, str2)可以看错,str就是翻译的原语言enstr2是翻译的目标语言cn,从m32047j("word", str3)可以推测str3就是查询的单词。弄清楚参数含义可以点进签名函数getAppSign,其实现很显然是md5,并且签名串构造为SIGN = "FromLang=%s&ToLang=%s&Word=%s&Word_Ext=%s%s"

到此就可以弄清楚弹窗查词的整个请求,然后可以写python爬虫实现了。

复制代码隐藏代码

import jsonimport requestsdefquery_hujiang_word(word:str, from_lang='cn', to_lang='jp') ->dict:     api_url ='http://dict.hjapi.com/v10/quick/{}/{}'.format(from_lang, to_lang)     word_ext =''    app_secret ='3be65a6f99e98524e21e5dd8f85e2a9b'    sign_str ='FromLang={}&ToLang={}&Word={}&Word_Ext={}{}'         .format(from_lang, to_lang, word, word_ext, app_secret).encode(encoding='UTF-8')     response = requests.post(         api_url,         data={             "word": word,             "word_ext":None        },         headers={             "User-Agent": u_file.COMMON_USER_AGENT,             "hujiang-appkey":"b458dd683e237054f9a7302235dee675",             "hujiang-appsign": hashlib.md5(sign_str).hexdigest()         },     )     log.info('end get info from web url: ' + api_url)    ifnot (400 <= response.status_code <500):         response.raise_for_status()    if response.textisNoneor response.text =='':         log.error('The response text is empty.')     query_result = json.loads(response.text)    if'data'notin query_resultor query_result.get('status', -1) !=0:         log.error('The response is not valid: {}'.format(response.text))         return {}    return query_result['data']

结语:可以说整个分析过程是很简单的,中途也没有遇到其他卡住的难题,我一度以为接下来的分析也会很顺利,然而,令我恐惧的在后面。

/v10/dict/en/cn反编译代码请求构造实现分析

和上面一样的流程,同样是jadx反编译后直接搜索关键词/v10/dict,结果很多,但是不用慌,大部分一眼就可以排除掉。

安卓逆向  某词典app接口破解流程

从结果可以很容易找到目标代码,就是截图中那一行,其他的都是别的请求url,我们要找的是拼接路径,很容易排除。

双击进入目标代码,可见整个目标类都已经被混淆,不像/v10/quick弹窗查词接口那样简单从类名和方法名就可以知道结果。不过也不用急。

安卓逆向  某词典app接口破解流程

复制代码隐藏代码

publicstaticvoidm40446y(String str, String str2, String str3, String str4, AbstractC12224a<JsonModel> aVar) {    if (str4 !=null) {         Stringtrim= str4.trim();         if (str.equals("cn")) {             trim = ZHConverter.convert(trim,1);         }         // hVar是一个HttpClient实例C12233hhVar= (C12233h)newC12233h(C11627h.m33736x().m33749k()).m32058d(f31569u, C11132q0.m35609s());         Stringd= m40467d(str, str2, trim, str3,"3be65a6f99e98524e21e5dd8f85e2a9b");         ((C12233h) ((C12233h) ((C12233h) ((C12233h) hVar.m32049h0(C9643a.m40497p(),"/v10/dict/" + str +"/" + str2)).m32058d("hujiang-appkey","b458dd683e237054f9a7302235dee675")).m32058d("hujiang-appsign", d)).m32047j("word", trim)).m32079L(C12189g.f39935o);         C11110j.m35802l("getWordDetail", hVar.m32087A());         longcurrentTimeMillis= System.currentTimeMillis();         longabs= Math.abs(newRandom().nextLong());         StringvalueOf= String.valueOf(abs);         StringhexString= Long.toHexString(abs);         // m32058d 为设置http的请求头        hVar.m32058d("X-B3-SpanId", hexString);         hVar.m32058d("X-B3-TraceId", hexString);         hVar.m32058d("X-B3-Sampled","1");         if (!TextUtils.isEmpty(str3)) {             hVar.m32047j("word_ext", str3);         }         // 参数 aVar 是http回调处理类实例        ((C12233h)newC9649d(hVar).m40484b()).m32041p(JsonModel.class,newC9670l(valueOf, currentTimeMillis, hVar, aVar));     } }// 签名参数构造privatestatic Stringm40467d(String str, String str2, String str3, String str4, String str5) {    Stringstr6="FromLang=" + str +"&ToLang=" + str2 +"&Word=" + str3 +"&Word_Ext=";    if (!TextUtils.isEmpty(str4)) {         str6 = str6 + str4;     }    return m40466e(str6 + str5); }

可以右键函数或者类名进行改名,也可以添加注释,这样方便分析代码。

首先分析请求构造方法,其中的C12233h类型可以点进去看,通过父类看到包含一些诸如HTTP请求的参数,显然是一个HttpRequestClient类似的封装。

m32058d函数是添加请求头的key-value,其中请求头hujiang-appsign签名参数赋值变量d,而d = m40467d(str, str2, trim, str3, "3be65a6f99e98524e21e5dd8f85e2a9b")。此处的str=enstr2=cnstr2=trim是查询单词,并且看上面有个逻辑如果查询的是中文则要转义一下,str3就是word_ext参数。进入签名方法看其实现:

复制代码隐藏代码

privatestatic Stringm40467d(String str, String str2, String str3, String str4, String str5) {    Stringstr6="FromLang=" + str +"&ToLang=" + str2 +"&Word=" + str3 +"&Word_Ext=";    if (!TextUtils.isEmpty(str4)) {         str6 = str6 + str4;     }    return m40466e(str6 + str5); }/* renamed from: e */privatestatic Stringm40466e(String str) {    try {         return m40471A(MessageDigest.getInstance("MD5").digest(str.getBytes()));     }catch (NoSuchAlgorithmException e) {         C11110j.m35811c("","", e);         returnnull;     } }

显然还是md5算法,并且签名串和v10/quick上面的一致,都是FromLang={}&ToLang={}&Word={}&Word_Ext={}{app_secret}

同样用python实现请求,代码和上面的一致,只是url不一样,可以获取到json返回结果,显然请求构造破解完毕。

接下来就是重头戏,返回值的解密。

/v10/dict/en/cn反编译代码返回值解密实现分析

继续看上面定位到的/v10/dict请求构造函数,注意到最后一行((C12233h) new C9649d(hVar).m40484b()).m32041p(JsonModel.class, new C9670l(valueOf, currentTimeMillis, hVar, aVar));。上面已经分析过C12233h是一个HttpRequestClient封装。

这个地方注意前面都是类型转换成C12233h,然后调用方法m32041p,这个方法两个参数,第一个参数是一个class类型参数JsonModel.class,第二个参数新建一个实例C9670l。找到这两个关键类型的定义:

复制代码隐藏代码

publicclassJsonModelextendsC9645b<String> { }publicclassC9645b<T> {    publicstaticfinalintSTATUS_SUCCESS=0;    private T data;    private String message;    privateint status;     ...     ... }publicstaticclassC9670lextendsAbstractC12224a<JsonModel> {    /* renamed from: a */final/* synthetic */ String f31612a;    /* renamed from: b */final/* synthetic */long f31613b;    /* renamed from: c */final/* synthetic */ C12233h f31614c;    /* renamed from: d */final/* synthetic */ AbstractC12224a f31615d;     C9670l(String str,long j, C12233h hVar, AbstractC12224a aVar) {         this.f31612a = str;         this.f31613b = j;         this.f31614c = hVar;         this.f31615d = aVar;     }    /* renamed from: onFail  reason: avoid collision after fix types in other method */publicvoidonFail2(int i, JsonModel jsonModel, Map<String, String> map,boolean z,long j, String str) {         C9092b.m42463g().mo7827h(C11627h.m33736x().m33749k(),this.f31612a,this.f31613b, System.currentTimeMillis(),this.f31614c.m32087A(), i,"POST", str);         this.f31615d.onFail(i, jsonModel, map, z, j, str);     }    @Override// com.hujiang.restvolley.webapi.AbstractC12224apublic/* bridge *//* synthetic */voidonFail(int i, JsonModel jsonModel, Map map,boolean z,long j, String str) {         onFail2(i, jsonModel, (Map<String, String>) map, z, j, str);     }    /* renamed from: onSuccess  reason: avoid collision after fix types in other method */publicvoidonSuccess2(int i, JsonModel jsonModel, Map<String, String> map,boolean z,long j, String str) {         C9092b.m42463g().mo7827h(C11627h.m33736x().m33749k(),this.f31612a,this.f31613b, System.currentTimeMillis(),this.f31614c.m32087A(), i,"POST", str);         this.f31615d.onSuccess(i, jsonModel, map, z, j, str);     }    @Override// com.hujiang.restvolley.webapi.AbstractC12224apublic/* bridge *//* synthetic */voidonSuccess(int i, JsonModel jsonModel, Map map,boolean z,long j, String str) {         onSuccess2(i, jsonModel, (Map<String, String>) map, z, j, str);     } }publicabstractclassAbstractC12224a<T> {    protected Exception mException;    public ExceptiongetException() {         returnthis.mException;     }    publicabstractvoidonFail(int i, T t, Map<String, String> map,boolean z,long j, String str);    publicvoidonFinished(AbstractC12235j jVar) {     }    publicvoidonStart(AbstractC12235j jVar) {     }    publicabstractvoidonSuccess(int i, T t, Map<String, String> map,boolean z,long j, String str);    publicvoidsetException(Exception exc) {         this.mException = exc;     } }

注意到其中的JsonModel继承自C9645b<T>,并且该类C9645b包含datastatusmessage三个参数,显然就是返回值的json结构。

另外注意到C9670l的父类AbstractC12224a<JsonModel>是一个抽象类,并且定义了onFinishedonSuccess这样的方法,而且包含泛型参数JsonModel,显然是一个HTTP请求的回调处理类。那么就要注意onSuccess方法的实现,其中有对返回值的处理,data的解密就在其中。

回到实现类C9670l,注意其中的onSuccess方法实现,直接调用了onSuccess2,其中的核心实现为this.f31615d.onSuccess(i, jsonModel, map, z, j, str),调用了this.f31615d的请求成功处理函数,而this.f31615dC9670l的构造函数中赋值。

C9670l的构造在上层中:new C9670l(valueOf, currentTimeMillis, hVar, aVar),比对参数发现this.f31615d就是这儿的aVar,而aVar是由上层传入的。

安卓逆向  某词典app接口破解流程

为了弄清楚aVar的值或者类型,我们搜索m40446y这个方法的调用:

安卓逆向  某词典app接口破解流程

可以看到除了函数声明外,有三个调用的地方,依次点开。

首先第一个调用:C9658c.m40446y(localReviewWord.getFromLan(), localReviewWord.getToLan(), str2, localReviewWord.getWord(), new C9810a(localReviewWord));显然aVar= new C9810a(localReviewWord),其中localReviewWord是一个localReviewWord类型,里面是很复杂的词汇结构,显然是app端展示用的已经解密的数据结构。

再点进C9810a类,这是一个http请求返回值的回调处理类。

安卓逆向  某词典app接口破解流程

复制代码隐藏代码

/* renamed from: onSuccess  reason: avoid collision after fix types in other method */publicvoidonSuccess2(int i, JsonModel jsonModel, Map<String, String> map,boolean z,long j, String str) {     C11106i.m35822a("OnlineApi.postWordDetail" +this.f32035a.getWordServerRawId(),true);    if (this.f32035a.getmRememberTimes() !=2) {         C11110j.m35812b(C9808c.f32015i,"word setDate: " +this.f32035a.getWord());         if (jsonModel !=null && !TextUtils.isEmpty(jsonModel.getData())) {             Stringdata= jsonModel.getData();             if (!TextUtils.isEmpty(data)) {                try {                     WordEntryResultDictwordEntryResultDict= (WordEntryResultDict) C11152w.m35489a(C9658c.m40470a(0, data,true), WordEntryResultDict.class);                     List<WordEntry> wordEntries = wordEntryResultDict.getWordEntries();                     if (wordEntries !=null && wordEntries.get(0) !=null && wordEntries.get(0).getDict(1) !=null) {                         this.f32035a.setContent(wordEntryResultDict);                         this.f32035a.setContentFrom(3);                     }                 }catch (Exception unused) {                     C11110j.m35806h(C11155z.f36759w);                 }             }         }     } }

注意其中的onSuccess2方法显然是http请求成功的回调处理,注意到其中通过jsonModel.getData()获取返回值的data字段,然后通过代码(WordEntryResultDict) C11152w.m35489a(C9658c.m40470a(0, data, true), WordEntryResultDict.class);直接将加密的data字符串转成了WordEntryResultDict词汇详情结构。

显然解密就是在这句代码实现的,外层的C11152w.m35489a方法实现是把string转成json,点进入C9658c.m40470a方法的实现,显然就是解密函数了。

安卓逆向  某词典app接口破解流程

这儿用android.util.Base64进行解密,其中的参数0表示没有wrap,查询其源码可知。然后调用了OfflinewordAPI.decodeAndUnzip静态方法,然而这是一个native库方法,我们知道Android可以通过JNI NDK来实现这种native的方法,而其代码已经编译到so动态链接库中。

安卓逆向  某词典app接口破解流程

事情开始变得麻烦了!

反编译so获取解密密钥和实现

native的方法是通过JNI实现,只能去so库中寻找了,然而so是动态链接库,很多人会在这儿放弃吧,然而已经花了这么多时间看混淆代码,怎么可以轻言放弃!

继续看OfflinewordAPI这个类,可以看到上面有加载so库的代码System.loadLibrary("decodermarker"),看so库的名称,猜测解密函数的实现在decodermarker这个库中,还原路径就是apk包中的lib/libdecodermarker.so文件。

安卓逆向  某词典app接口破解流程

可以通过下面的方式提取到这个so文件:

  1. apk包后置名改为zip然后解压
  2. 使用apktool工具解压

    1. 点击apktool页面直接另存为apktool.bat
    2. 点击页面

      下载最新版本的jar包并重命名为apktool.jar
    3. 将上面两个文件放在同一个文件加
    4. 可以选择配置刚才的文件夹为环境变量,或者直接在该文件夹下cmd打开控制台
    5. 执行命令apktool b hujiang-dict.apk解压
  3. 直接jadx另存项目提取文件
  4. 其他工具解压

这儿直接取armeabe-v7a架构下的libdecodermarker.so文件。

拿到so文件以后,打开下载好的IDA,不要打开IDA64.exe,64位版不支持将汇编反编译为C语言,不方便阅读源码。

打开时选择Load File ELF for ARM(Shared object)。其他的配置可以不用修改。

安卓逆向  某词典app接口破解流程

这个so文件不大,打开后首先看最左侧的Function一栏,可以看到AES_CBC的一些函数,可以大胆猜测使用了AES_CBC的解密方式,如果是用这种解密方式,就要找到keyiv了。不过先不急。

首先在左侧Function WindowCtrl+F搜索native方法decodeAndUnzip找到其入口。native方法绑定的方式是包名称加方法名,很容易定位到,然后按F5可以将汇编反编译为C语言。

通过Option -> General -> Auto Comments可以打开汇编指令自动注解功能。

安卓逆向  某词典app接口破解流程

可见Java_com_hujiang_offlineword_OfflinewordAPI_decodeAndUnzip这个方法的实现,点击return语句中调用的方法,一路嵌套下来:

j_j_decodeAndUnzip_0 -> j_decodeAndUnzip -> off_16DB8

off_16DB8这个地址标记位置:

安卓逆向  某词典app接口破解流程

双击其中的函数decodeAndUnzip然后按F5反编译为C语言。

安卓逆向  某词典app接口破解流程

这个函数就是解密实现的核心逻辑了,注意到其中的函数调用HJCryptoManager::decryptAndInflate,双击跳转到该函数地址。

其中是一个嵌套函数,一直跟着函数地址跳转。

HJCryptoManager::decryptAndInflate -> off_16DC4 -> _ZN15HJCryptoManager17decryptAndInflateEiPhiPS0_Pii+1 -> F5

可以看到调用解密函数。

安卓逆向  某词典app接口破解流程

寻寻觅觅,在汇编海洋中逐渐迷失了,根本找不到密钥在哪儿!!

换一种思路,比如动态调试,现在我不是已经知道函数入口了吗?能不能在Python种调用so库函数呢?模拟Android环境。

查了一下,还真有,有一个python库,AndroidNativeEmu,支持在python中调用so库中的方法,整个完全模拟Android环境,文档参考:https://github.com/P4nda0s/AndroidNativeEmu/blob/master/README_cn.md。

激动啊!

赶紧把代码clone到本地,照着文档中的方法,本地的python是3.7版本和文档匹配,直接安装依赖pip install -r requirements.txt,本来一切正常,但是到keystone-engine这个库的时候好像有报错。不过后面好像成功安装了,先不管了。

将项目导入到Pycharm中后,提示androidemu包找不到,只需要右键项目,选择Mark Directory as Source Root即可。

直接运行samples中的示例,结果肯定是运行不了。没办法,只能重新安装keystone-engine,仔细看了下报错,好像是版本的问题。

尝试着先把已经按照的库删掉pip uninstall keystone-engine,然后再重新安装pip install keystone-engine,居然没报错成功了!谢天谢地!

重新运行samples下的示例,居然跑起来!!!LUCKY!!

好的,接下来把我们的库移动进去,然后改一下example_douyin.py代码,首先定义一个目标类OfflinewordAPI对应上JNI里面的类,然后加载库。

复制代码隐藏代码

import loggingimport posixpathimport sysfrom unicornimport UcError, UC_HOOK_CODE, UC_HOOK_MEM_UNMAPPEDfrom unicorn.arm_constimport *from androidemu.emulatorimport Emulatorfrom androidemu.java.java_class_defimport JavaClassDeffrom androidemu.java.java_method_defimport java_method_deffrom samplesimport debug_utilsclassOfflinewordAPI(metaclass=JavaClassDef, jvm_name='com/hujiang/offlineword/OfflinewordAPI'):    def__init__(self):         pass    @java_method_def(name='leviathan', signature='(I[B)[B', native=True)    defdecodeAndUnzip(self, mu):         passemulator.load_library("example_binaries/libdecodermarker.so")

先运行一下有没有问题,结果库加载不了,总是报各种莫名奇妙的问题,有可能是这个so库有问题,或者架构不兼容,或者其他的,而且定义OfflinewordAPI的时候,方法里面的signature也不知道写啥。

查了半天文档,找不到任何的线索,而且这个项目貌似star也不多,可能不行吧,放弃此路吧,还是找到密钥更爽。

劝劝自己,还是回去看汇编代码吧!继续下去!不能半途而废啊!它这个so库也没加壳没做安全处理,应该很容易找到的。

所以,又回到了IDA。这一次仔细研究了一下IDA的功能,首先是最上面的工具栏,能看到StructuresEnumsImportsExports等。然后之前在查资料的时候又看到,反编译so时,先看看export。

安卓逆向  某词典app接口破解流程

点开export窗口,里面有导出的函数名,变量等,仔细找可以发现导出的keyiv

等等!难道!我知道我脸上的笑容已经开始逐渐放肆!

别激动,稳一手!深呼吸两口气,然后双击其中的key,进入代码地址。

这儿的DCB是指开辟两个字节的存储空间。

安卓逆向  某词典app接口破解流程

显然这就是苦苦寻找的AES_CBC解密的keyiv了!可给找到了啊!你们哥俩!

还不能确定,先在python中用这两个参数解密试试看。

python已经有现有的库实现了AES_CBC解密,windows安装pycryptodome库即可,之前爬取hlsv解密时用过。

解密函数的代码如下:

复制代码隐藏代码

import base64from Crypto.Cipherimport AESdefhujiang_des_cbc_decrypt(encode_data):    """     沪江小D单词详情查询接口返回的 data 是加密的,需要解密,AES_CBC解密     :param encode_data: 加密数据     :return: 解密后的json数据     """    key ='ceh[Een,3d3o9neg}fH+Jx4XiA0,D1cT'.encode('UTF-8')     iv ='K+\~d4,Ir)b$=paf'.encode('UTF-8')     cipher = AES.new(key, AES.MODE_CBC, iv)    # 注意不要遗漏调用的 android.util.Base64    decode_data = base64.decodebytes(encode_data.encode('UTF-8'))     decode_data = cipher.decrypt(decode_data)    print(decode_data)

找一个抓包到的加密data传进去验证一下,解密应该能得到一个json字符串才对。

激动人心的时候到了。Run!

怎么结果是b'x1fx8bx08x00x00x00x00.....这样的乱码。要遭啊!

这可咋搞?!难道提取的keyiv是错的?但是这个keyiv一个是32位,一个是16位,很显然是对的啊。或者解密算法还有其他的参数?

于是吭哧吭哧打开在线AES解密网站,把参数填进去,选择不同的填充方式,什么no padding啊,什么pkcs7padding啊都试过了,就是不能解密。

难道一切要重新开始来吗?

等等!!!

x1fx8bx08这个不是压缩文件的开头标识吗?会不会解密的结果被压缩过了!

试一试!

加上decode_data = gzip.decompress(decode_data)的解压缩代码。

运行!

然而。。。报错了。

OSError: Not a gzipped file (b'x0fx0f')

这个x0f是什么鬼东西?回头检查解压前的数据,搜索这两个字节码。

怎么回事!?怎么解密后的数据后面全是x0f,这不对啊!

找资料,发现AES_CBC解密时,还需要去掉末尾填充的字符。

加上代码decode_data = decode_data[:-decode_data[-1]]

运行!

最后居然成功了,熟悉的json字符串出来了!想不到终于成功了!!!

安卓逆向  某词典app接口破解流程

最后的完整解密代码如下:

复制代码隐藏代码

import gzipimport base64from Crypto.Cipherimport AESdefhujiang_des_cbc_decrypt(encode_data):    """     沪江小D单词详情查询接口返回的 data 是加密的,需要解密,AES_CBC解密     :param encode_data: 加密数据     :return: 解密后的json数据     """    key ='ceh[Een,3d3o9neg}fH+Jx4XiA0,D1cT'.encode('UTF-8')     iv ='K+\~d4,Ir)b$=paf'.encode('UTF-8')     cipher = AES.new(key, AES.MODE_CBC, iv)    # 解密之前还需要 base64解码    decode_data = base64.decodebytes(encode_data.encode('UTF-8'))     decode_data = cipher.decrypt(decode_data)    # 注意需要去掉尾部的填充字符,复杂解压失败print(decode_data)     decode_data = decode_data[:-decode_data[-1]]    print(decode_data)     decode_data = gzip.decompress(decode_data)    print(decode_data)     decode_data = json.loads(decode_data.decode('UTF-8'))    print(decode_data)    return decode_data

结语

其实很辛运,对方做的安全措施也不是很严格。

  • 代码没有加固,没有加壳
  • 混淆也没那么恐怖
  • url直接明文写,其实可以把url拆分,或者写成ascii吗收不到
  • so库没有加壳,也没有混淆
  • key和iv没有做特殊运算,直接写死的字符串

不过如果做了这些的话,可能需要动态调试,或者其他方法吧。

 

安卓逆向  某词典app接口破解流程

原文始发于微信公众号(逆向有你):安卓逆向 -- 某词典app接口破解流程

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月23日01:48:19
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   安卓逆向 某词典app接口破解流程https://cn-sec.com/archives/3422574.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息