安卓逆向-jdgs模拟执行

下雨天
昨天发布 /正在检测是否收录...

jdgs模拟执行

1. 前言

  • 版本:13.1.0,没有加固;com.jingdong.app.mall;
  • 主要目的是分析jdgs这个参数,一部分在在vm里;

2. 抓包&定位

  • 抓包过掉sslping就可以了,这个没什么难度的,不像其他一些大厂有一些协议需要hook;
  • 在请求头有一个键为jdgs的字段;

image-20260323210937624

  • 它是这样的一个值;
{"b1":"6bbc8976-d0fe-44ef-9776-ab9da0c127e8","b2":"3.2.8.4_0","b3":"2.1","b4":"LBFU0PEU7/zZ0YeMijf8f9kd1OU3LDJE9Bs438Ot5GPu+Vhr/aG+i1CA4ZyNFefve8geq+WFnDkLZlF5bXQoN6qvFrZjxcCuoMM2Ms9qeUaYazGSH/rvI9NHs4AP4SEKe+Q4wxw+jUxBwJqgXVKmU650y1/atwFkmGRJyYBVfkh5BSomAawHqdHLeLRs/XSuDceeijEtOoYPLDJL3JuyFzqwqASRcpaZ2nTlmYqj+XlPgPvyV0xcp+zAsug9Qn/gBn8IwsIslWJ5Qdsq8KARdQSfUwphSX/ZfmkwXDkQyEDF6evyJM4V+B53lqvj/i86RrtZHHyfG+LESITEV/+2ZrzulmV86h6NipmrUDYNQDFcSAfSfM5alQREFVnfYhZUm9o+QYSNJx/NbPIX/+mWjR63hSybesiDInfTNkdAww==","b5":"d87bf2b55908d4c6c574b1bd06fe5ed3b5217d8c","b7":"1774271324431","b6":"1ae7fdde260504627c3e41e22edbfbe7553e3352"}
  • 定位使用hashmap的hook就好,一般请求头这个hook方式会非常好使;
function hooks() {
    Java.perform(function () {
        var hashMap = Java.use("java.util.HashMap");
        hashMap.put.implementation = function (a, b) {
            if (a != null && a.equals("jdgs")) {
                console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
                console.log("hashMap.put: ", a, b);
            }
            return this.put(a, b);
        }
    })
}
hooks()
  • 日志如下:
java.lang.Throwable
    at java.util.HashMap.put(Native Method)
    at pc.e.k(SourceFile:23)
    at pc.a.a(SourceFile:102)
    at pc.a.b(SourceFile:34)
    at com.jingdong.common.guard.JDGuardHelper$1.genSign(SourceFile:1)
    at com.jingdong.jdsdk.network.toolbox.HttpSettingTool.doSignUsingJdGuard(SourceFile:114)
    at com.jingdong.jdsdk.network.toolbox.ParamBuilderForJDMall.setupParams(SourceFile:41)
    at com.jingdong.jdsdk.network.toolbox.HttpSettingTool.setupParams(SourceFile:30)
    at com.jingdong.jdsdk.network.toolbox.HttpGroupAdapter$RequestTask.run(SourceFile:152)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
    at java.lang.Thread.run(Thread.java:920)

hashMap.put:  jdgs {"b1":"6bbc8976-d0fe-44ef-9776-ab9da0c127e8","b2":"3.2.8.4_0","b3":"2.1","b4":"BS7523+bQYFkfKXCF6AYosr4Py1bEvETavLeTensmXGcfRbpQIzEiDpMYviaePf0Txx1bLpiznEnmhkhmGeBNaEbF+tD/zi1ZKPpbwNz/GS5gFn5oMNjAhEIZV5SHoYvjvHyAOZ0N+Ww423eZ0kxbo7VR45dy/puIZ2e8CKrXYTowxMYBxlTLre7lfv22TcpdV30zSUixSFI+EXSCGwze1fjhhCH9QLXlOhPeljvTlIOb6Isebcqt0DucF9tVipz4+mQ6hgjdpKlYFoxkZ/mSQrnWGN/u9SbfWm4kg74/L8GS+gMm5Q4rjTFKPhXiYCV58DZEYcI5K614OmGd2GuK1MSSrNZyshHWImDi5MsNPS5mJV6w7zb9q04Azw0VYulCkbOrT1jct/nUzJSb8V5tM55hB732n+cxM13Tfy7Ew==","b5":"5e8e4132c39df1ab59b38645d2518f07aa09754d","b7":"1774317169543","b6":"3c072ff395a3c6c3e62de77456835145f597a61c"}
  • 我们去找目标函数,这种我直接写结果不再写过程了;

image-20260324095837052

  • 尝试hook一下,这里比较重要的就是关于参数的打印了;
function showObjectArray(objArr, name) {
    if (objArr == null) return;
    var length = objArr.length;
    console.log(name + ' length: ' + length);
    for (let i = 0; i < length; i++) {
        var item = objArr[i];
        if (item != null && item.getClass().getName() === "[B") {
            var str = Java.use('java.lang.String').$new(Java.array('byte', item)).toString();
            console.log('  [' + i + '] = (byte[]) ' + str);
        } else {
            console.log('  [' + i + '] = ' + (item != null ? item.toString() : 'null'));
        }
    }
}

function hook_mointor_main() {
    Java.perform(function () {
        let com_jd_security_jdguard_core_Bridge = Java.use("com.jd.security.jdguard.core.Bridge");
        com_jd_security_jdguard_core_Bridge["main"].implementation = function (i10, objArr) {
            console.log(`[->] Bridge.main is called! args are as follows: ->i10= ${i10} ->objArr= ${objArr}`);
            showObjectArray(objArr, "objArr");
            var retval = this["main"](i10, objArr);
            console.log(`[<-] Bridge.main ended! retval= ${retval[0] + ""}`);
            return retval;
        };
    });
}

hook_mointor_main();
  • 日志如下:
[->] Bridge.main is called! args are as follows: ->i10= 101 ->objArr= [B@5848b9,oriole|-|oriole|-|-|-|-|-|-|-|-|-|-|-|-|-|-§§0|1§§-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-§§-|118396899328§§-|-|1.0|-§§1|1|-|-|-|-|-|-|-|-|-§§google/oriole/oriole:12/SQ3A.220705.004/8836240:user/release-keys/|-|-|1774255476556|-§§-|-|-|-|-|-|-,eidA1bde812326sfu82WCNn2Qm2LUean4FbZqrvNl6X0wizjUejSXXgX/IxprM1FiPVhLlVU1fcGXWDpvyIYErTNI1eEwnsTrtvo9PLKYD5miUH5AgJJ,1.0,83
objArr length: 5
  [0] = (byte[]) POST /client.action 1a5b8712d78781f572109ea1f1d7b26da8d319157add01eaecb285e5066d7d60=&avifSupport=1&bef=1&build=99208&client=android&clientVersion=13.1.0&ef=1&eid=eidA1bde812326sfu82WCNn2Qm2LUean4FbZqrvNl6X0wizjUejSXXgX%2FIxprM1FiPVhLlVU1fcGXWDpvyIYErTNI1eEwnsTrtvo9PLKYD5miUH5AgJJ&ep=%7B%22hdid%22%3A%22JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw%3D%22%2C%22ts%22%3A1774317159647%2C%22ridx%22%3A-1%2C%22cipher%22%3A%7B%22area%22%3A%22CV83Cv81DJY3DP8m%22%2C%22d_model%22%3A%22UQv4ZWm2%22%2C%22wifiBssid%22%3A%22dW5hbw93bq%3D%3D%22%2C%22osVersion%22%3A%22CJS%3D%22%2C%22d_brand%22%3A%22H29lZ2nv%22%2C%22screen%22%3A%22CtS0CMenCNqm%22%2C%22uuid%22%3A%22YtvuCJvsZtY3DJvrZJOnDK%3D%3D%22%2C%22aid%22%3A%22YtvuCJvsZtY3DJvrZJOnDK%3D%3D%22%2C%22openudid%22%3A%22YtvuCJvsZtY3DJvrZJOnDK%3D%3D%22%7D%2C%22ciphertype%22%3A5%2C%22version%22%3A%221.2.0%22%2C%22appname%22%3A%22com.jingdong.app.mall%22%7D&ext=%7B%22prstate%22%3A%220%22%2C%22pvcStu%22%3A%221%22%2C%22cfgExt%22%3A%22%7B%5C%22privacyOffline%5C%22%3A%5C%220%5C%22%7D%22%7D&functionId=search&harmonyOs=0&lang=zh_CN&networkType=wifi&oaid=8950da45-c76f-481c-a0b8-ce0ccb6c8947&partner=xiaomi001&sdkVersion=32&sign=9a5f13130d3591a12a08629ee244a4bc&st=1774318202702&sv=120&uemps=2-2-2&x-api-eid-token=jdd01ZBOXJWGZXOSMGLXTOLEIHXJX74USYLVMIH76MWKRCYPLRP2RAVY3JH6SUTQE726SAYNECWJNFQTJQJUULNA3VTQQ2RIPTNCK52ILQ4Y01234567
  [1] = oriole|-|oriole|-|-|-|-|-|-|-|-|-|-|-|-|-|-§§0|1§§-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-§§-|118396899328§§-|-|1.0|-§§1|1|-|-|-|-|-|-|-|-|-§§google/oriole/oriole:12/SQ3A.220705.004/8836240:user/release-keys/|-|-|1774255476556|-§§-|-|-|-|-|-|-
  [2] = eidA1bde812326sfu82WCNn2Qm2LUean4FbZqrvNl6X0wizjUejSXXgX/IxprM1FiPVhLlVU1fcGXWDpvyIYErTNI1eEwnsTrtvo9PLKYD5miUH5AgJJ
  [3] = 1.0
  [4] = 83
  
[<-] Bridge.main ended! retval= {"b1":"6bbc8976-d0fe-44ef-9776-ab9da0c127e8","b2":"3.2.8.4_0","b3":"2.1","b4":"dUZEzzI0hq14El85YhKKnCYJ7hNfpVk7bJ71QC9VWOIWXcmdGGlWRfjMYl+fLNOkgOvDY06ncld38eZQZalv40ms1XHM4B4pyH/FJOdhBmhxuIfzIlKzXFLAGbkXg5QixMpBV8zS7sBw9a4O8heyWI3sFPQ1vgI/DHFxeRlSk+alRWFsgRWb160dUv74ix5Mm7jj4FeeNKh37tL3tHtaMlinMdynbVXzMbTrh6Ufjs2brP/crX603MKfNt7Q0YiUotybDvd+BiCjfax8r2LZVy+A+q0WPPkmzv1XZprNWk8sQ9B7xy9OutKwFbfXwJlqnG7xKr1PSCPCbMhZgQzEZjObUkmHDFNLcK0/lIa2QDQPQrjfQZ1W6ob3OTmFumCwXtpTOq9vKPnYSuqxA39Vc8LhE63V8HBVhC+NJUlhRvnRDoI=","b5":"aa7194786203fdd040838a0de40e402e501ba328","b7":"1774318202763","b6":"6b38cb1d17176c0a3b49c13e8a91143158b05bbb"}
  • 看起来比较多的参数1是一些载荷数据,对比了一下就是顺序不一样,后面留个心眼就好;
  • 接下来开始主动调用,看看结果如何;
function call_main(){
    Java.perform(function (){
        let Bridge = Java.use("com.jd.security.jdguard.core.Bridge");
        var str0 = 'POST /client.action avifSupport=1&b9aa898b3bc3febb04a7b0d1cc099577fded30aac89532ad35d0ceaab89be770=&bef=1&build=99208&client=android&clientVersion=13.1.0&ef=1&eid=eidA943781213fsbM1YH2pJ2Rfqo%2B%2B7swN3q3zNWCW%2B5amEMdjBONjMO5gqo6HorEr9UADNI1%2BJqE1zYmDNNluQ2UBN2%2FhIKcsN7ffqaBKB3jQrbejky&ep=%7B%22hdid%22%3A%22JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw%3D%22%2C%22ts%22%3A1739247938442%2C%22ridx%22%3A-1%2C%22cipher%22%3A%7B%22area%22%3A%22CV83Cv81DJY3DP8m%22%2C%22d_model%22%3A%22UQv4ZWm0WOm%3D%22%2C%22wifiBssid%22%3A%22dW5hbw93bq%3D%3D%22%2C%22osVersion%22%3A%22CJK%3D%22%2C%22d_brand%22%3A%22H29lZ2nv%22%2C%22screen%22%3A%22Ctu4DMenDNGm%22%2C%22uuid%22%3A%22YzG0CtOmDNczDzK5CWVtEG%3D%3D%22%2C%22aid%22%3A%22YzG0CtOmDNczDzK5CWVtEG%3D%3D%22%2C%22openudid%22%3A%22YzG0CtOmDNczDzK5CWVtEG%3D%3D%22%7D%2C%22ciphertype%22%3A5%2C%22version%22%3A%221.2.0%22%2C%22appname%22%3A%22com.jingdong.app.mall%22%7D&ext=%7B%22prstate%22%3A%220%22%2C%22pvcStu%22%3A%221%22%2C%22cfgExt%22%3A%22%7B%5C%22privacyOffline%5C%22%3A%5C%220%5C%22%7D%22%7D&functionId=search&harmonyOs=0&lang=zh_CN&networkType=wifi&partner=xiaomi001&sdkVersion=29&sign=3b5c882977fdb680c297d34adaf01931&st=1739248022333&sv=100&uemps=2-2-2&x-api-eid-token=jdd01IHFGXYKIKWYJTG3BV5U6TBHKBPYJKQ35JYSSNHEY434T633JS5HWSPQN5KE7PNU7YQXZ7VPOCSLZC2VQSK2SYTIBEYC4PO5OAKVE6GY01234567'
        var StringClass = Java.use('java.lang.String');
        var byteArray = StringClass.$new(str0).getBytes();
        var objArr = Java.array('Ljava.lang.Object;',[byteArray,'coral|-|coral|-|-|-|-|-|-|-|-|-|-|-|-|-|-§§0|1§§-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-§§-|53684973568§§-|-|1.0|-§§1|1|-|-|-|-|-|-|-|-|-§§google/coral/coral:10/QD1A.190821.007/5831595:user/release-keys/|-|-|1739164885415|-§§-|-|-|-|-|-|-','eidA943781213fsbM1YH2pJ2Rfqo++7swN3q3zNWCW+5amEMdjBONjMO5gqo6HorEr9UADNI1+JqE1zYmDNNluQ2UBN2/hIKcsN7ffqaBKB3jQrbejky','1.0','83'])
        let result = Bridge["main"](101, objArr);
        console.log("res:",result)
    })
}
  • 请以这个参数来调用,后续算法还原也使用这个入参,先call几次看看;
[Pixel 6::京东 ]-> call_main()
res: {"b1":"6bbc8976-d0fe-44ef-9776-ab9da0c127e8","b2":"3.2.8.4_0","b3":"2.1","b4":"MeL0DP9NjGH1gBiZqyE7MM5rjGSYvkXKez7tzNEyhwFV3AuoSxRPbug5cOQ7T5WrOPqa23KEEN8oepwuu5ZYPK/hBTaHeALRgZvkBQCXcC7xAQg5UoCdBCBdoEAP9w2bUqKBqXu1zQDJuFyReFyU5YK9FZZn1EhOaVMxtQL9ZwjehEPeC+OUjK/ELEY5CjYPBYngbvjpxK78ca654nKymL8zEANN64wH+qBBvt71Bu5L2zJ9JJmr6kx2MjcOr7CqYKtjJwB9XDo7YlHh1FOJ+5bFX/YGSg7jGrCq3d03AHI3/a21WLgn7h7RZWIi6axdGg0nt/1AFesSbfNRvrIYzzBjZa+n7A+GuQCzC1nODNpLFpgawylFMRm1liSCFEsdpLScHsCAmtxfEhmW93cMSFFP1+SbjIfBuqTtRZU=","b5":"79154e49c97f5d8e36bc36614755281a9b393246","b7":"1774319855853","b6":"cf1277894a75ddaf066b9fa0cc8a2c9d400adf60"}
[Pixel 6::京东 ]-> call_main()
res: {"b1":"6bbc8976-d0fe-44ef-9776-ab9da0c127e8","b2":"3.2.8.4_0","b3":"2.1","b4":"626SaJ85sJYzsoExb1ucfK+IoOXm/dM3mPfn6XopXIT1gfTGoeqCBTD2Hn32Q0Q1Qow6Jt+BqyHQGsznqODlDBMUzokDCHfh6x+VfoJvH0q+D8anKx1FWH6OjJYgc5BoNsr0FAI1SmEB+o7XfbLUWPluXLNMVMK6fW/8VwnxrNKn0NiMEjomitKcGzEwDblYqAznbJDMoUIfCC4svA/Xnobbzz6g+eEwmQDizQ5Pva6zCgs41Ph/8jEu/LFABHLesz+9y3tHEAyomxOyfnCtrOZKaE7rq2TeoHtQNjqhRSS7hiE4gPMEbbmpdU73yxxmBrO/9NbY0enU1b7QiinRSWYPacI46udx1NxdbtO4taWbIPl8FdV8BV9YO5W+TNvTZU25tOSuzKFe/qO9ZYwRYuAX6D3JLvX8lCNWVLI=","b5":"79154e49c97f5d8e36bc36614755281a9b393246","b7":"1774319856721","b6":"ce3401a4c924335aab70c4a98746ea7cdc778627"}
[Pixel 6::京东 ]-> call_main()
res: {"b1":"6bbc8976-d0fe-44ef-9776-ab9da0c127e8","b2":"3.2.8.4_0","b3":"2.1","b4":"PYWts9KIZxuWCxltyWLX8iBg7HnRN2BOvgaoCgsYtnEgjiUFrBqNhIGiOYhNHkj+G/SUOqOVJGFkZ3nWV7bnVNlJWv7HcT7flRtLjbNaLrcxyRJwC3AbX2B7l9Xk2L+dvYZQ0AgiUVSz6oa3dfz1HXhhN5MRJFfCxNyGsvBM1f7z6CnS4QzKFV9Lt7sOqVwJn07XKHPKGCDm38JFarly14Nzn/b7RBlizMcGxSRRW/1a4Xi8NLSF4I9zAf8KaWhy+l8QE7mRwG+cXl3Pb4qtBx81GPBGs/JROl3ChJqNylWmwd8qJ32rHA+QYbqo1HG7NzcPvxKekC5NPNniBe7y0uHUppRCttTDxOqSeUGnFo7Lob4wGFRkWLufq81ka5n9dl3nwLplGFZtVRipD8kclrJ95fr7wnAwWaU6X1lV","b5":"79154e49c97f5d8e36bc36614755281a9b393246","b7":"1774319857360","b6":"8daac1d15d2c3c0dd35d8ea1aa6ba11f28998f6f"}
  • b1、b2、b3、b5是不变的,而b4、b6、b7是变化的,b7是时间戳;

3. 初始化

  • 怎么判断初始化?在前面的样本里,我们是把看着比较像或者当前类中的其他函数hook上,然后看看执行顺序;这里只有一个函数,应该怎么办?
  • 对于大厂而言,非常喜欢只留一个函数,它可以根据不同的分支来做不同的事情,比如这里就是,第一个参数是int,调用函数传的是101,我们去看交叉引用;

image-20260324104915726

  • 这里有101、102、103、104这几种入参,它们应该有一种是对应初始化操作的;我们把main函数hook上,然后使用spawm的方式启动应用;
function hook_mointor_main() {
    Java.perform(function () {
        let com_jd_security_jdguard_core_Bridge = Java.use("com.jd.security.jdguard.core.Bridge");
        com_jd_security_jdguard_core_Bridge["main"].implementation = function (i10, objArr) {
            console.log(`[->] Bridge.main is called! args are as follows: ->i10= ${i10} ->objArr= ${objArr}`);
            showObjectArray(objArr, "objArr");
            var retval = this["main"](i10, objArr);
            console.log(`[<-] Bridge.main ended! retval= ${retval[0] + ""}`);
            return retval;
        };
    });
}

setTimeout(hook_mointor_main, 100)
  • 直接hook一直没反应,所以加点延迟,日志的一部分如下:
[->] Bridge.main is called! args are as follows: ->i10= 103 ->objArr= 0
objArr length: 1
  [0] = 0
[<-] Bridge.main ended! retval= 0
[->] Bridge.main is called! args are as follows: ->i10= 103 ->objArr= 1
objArr length: 1
  [0] = 1
[<-] Bridge.main ended! retval= 0
[->] Bridge.main is called! args are as follows: ->i10= 101 
  • 前面说过,101是调用,它前面这两次103就有可能是初始化,但我们不能一家之言,要做求证;
  • 怎么验证?它不是两次103吗,我们在每一次103之后调用call,看看什么时候会有正确结果;我这里分别做了两次,第一次是进入时判断,第二次是执行完后判断,分别如下:
function hook_mointor_main() {
    Java.perform(function () {
        let com_jd_security_jdguard_core_Bridge = Java.use("com.jd.security.jdguard.core.Bridge");
        com_jd_security_jdguard_core_Bridge["main"].implementation = function (i10, objArr) {
            console.log(`[->] Bridge.main is called! args are as follows: ->i10= ${i10} ->objArr= ${objArr}`);
            // 第一次位置
            if (i10 === 103) {
                call_main()
            }
            showObjectArray(objArr, "objArr");
            var retval = this["main"](i10, objArr);
            // 第二次位置
            if (i10 === 103) {
                call_main()
            }
            console.log(`[<-] Bridge.main ended! retval= ${retval[0] + ""}`);
            return retval;
        };
    });
}
  • 先测第一次,再测第二次,最好不要同时,第一次日志如下:

image-20260324111408490

  • 两次call都没有输出,再看第二次;

image-20260324111432745

  • 这里对比两部分我们可以清晰的得知,这两次103的调用是必须做的,而且单做第一次也是不行的;
  • 所以后续模拟执行之前需要先做这两次初始化的cal,并且参数是["0"] 和["1"],注意先后顺序;
  • 在执行之前还需要知道是哪个so,它是libjdg.so;
RegisterNative(com/jd/security/jdguard/core/Bridge, main(I[Ljava/lang/Object;)[Ljava/lang/Object;, RX@0x40029ce4[libjdg.so]0x29ce4)

4. 模拟执行

  • 先搭一个基本的框架,把apk加载起来,发现并没有报错;那我们开始第一次call初始化;
public void callInit(String arg) {
    List<Object> list = new ArrayList<>(4);
    list.add(vm.getJNIEnv());
    list.add(0);
    list.add(103);
    StringObject arg0 = new StringObject(vm, arg);
    vm.addLocalObject(arg0);

    ArrayObject arrayObject = new ArrayObject(arg0);

    list.add(vm.addLocalObject(arrayObject));

    Number number = module.callFunction(emulator, 0x29CE4, list.toArray());
    ArrayObject arrayObject2 = vm.getObject(number.intValue());
    System.out.println("call init:" + arrayObject2.getValue()[0].getValue());
}
  • 这里调用两次,传参注意好,先传0;不用补环境,直接就是0,应用返回的也是0;

image-20260324215207837

  • 再来调用传参为1的初始化;
demo.callInit("1");
  • 这时候返回值就不是我们期望的了;

image-20260324215247616

  • 这种一般情况是走到了错误分分支,我们应该如何定位出错位置?又或者说遇到这种没有明确提示的错误我们究竟应该如何有一个完善的思想去解决?
  • 首先看这个值大概可能来源与哪里;

image-20260325204148082

  • 应该就是前面的那个0xfffff31b,我们打开tracecode,直接输出在控制台就好了;

image-20260325204221171

  • 找这个值最开始在哪里出现的,在0x2a14c位置;到ida去分析一下;

image-20260325204256024

  • 有花指令,但是这个和快手的花指令是一样的,我们使用同一个脚本来去除;
import keystone  # pip install keystone-engine
from keystone import *

import ida_bytes
import idc
import ida_nalt


# https://python.docs.hex-rays.com/
# https://python.docs.hex-rays.com/ida_bytes

def binSearch(start, patternStr):
    matches = []

    pattern = ida_bytes.compiled_binpat_vec_t()
    ida_bytes.parse_binpat_str(pattern, 0x0, patternStr, 16, ida_nalt.BPU_2B)
    # ida_nalt.BPU_2B 表示使用2字节的搜索单位
    while True:
        # 从 start 地址到 idc.BADADDR(表示无效地址)的范围内搜索 pattern模式 1 表示只搜索第一个匹配项
        addr, _ = ida_bytes.bin_search3(start, idc.BADADDR, pattern, 1)
        if addr == idc.BADADDR:  # bad address
            break
        else:
            matches.append(addr)
            start = addr + 1
    return matches


def getJumpAddress(addr):
    A = idc.get_operand_value(addr + 8, 1)  # get_operand_value 需要地址识别为代码
    B = idc.get_operand_value(addr + 12, 2)
    C = idc.get_operand_value(addr + 20, 2)
    return A - B + C  # 获取跳转的地址


def makeInsn(addr):
    # 尝试在给定的地址 addr 上创建一条指令
    if idc.create_insn(addr) == 0:
        # 如果在地址 addr 上没有成功创建指令,则删除该地址上的所有项(扩展项)
        idc.del_items(addr, idc.DELIT_EXPAND)
        # 重新尝试在地址 addr 上创建一条指令
        idc.create_insn(addr)
    # 等待自动操作完成
    idc.auto_wait()


def generate(code, addr):
    ks = Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
    # 参数2是地址,很多指令是地址相关的,比如 B 指令,如果地址无关直接传 0 即可,比如 nop
    encoding, _ = ks.asm(code, addr)
    return encoding


# 1 搜索匹配
matches = binSearch(0, "E0 07 BE A9 E2 7B 01 A9")

for addr in matches:
    # 2 计算跳转地址
    makeInsn(addr)  # 给定的地址 addr 上创建一条指令
    targetAddr = getJumpAddress(addr)
    print(hex(targetAddr))

    # 3 patch
    code = f"B {hex(targetAddr)}"
    bCode = generate(code, addr)
    nopCode = generate("nop", 0)

    ida_bytes.patch_bytes(addr, bytes(bCode))
    ida_bytes.patch_bytes(addr + 4, bytes(nopCode) * 9)
print("patch compelete")
  • 再去当前偏移看看;

image-20260325204546560

  • 这里还是很明显的,我们要去看v32的计算方式,也就是sub_43954这个函数;

image-20260325205019470

  • 这个v2居然是jclass,这里的类名被加密了,但是不要紧,我们在日志里搜索这个偏移;

image-20260325205146892

  • 看起来fiadclass找的就是下面这个类,这里如果熟悉一点就可以看出来,java是没有这个类的,这是用来anti unidbg的,正确的应该是大写的String,这个怎么避免?
  • 这个anti的原理后续有需要我可以写,这里作者也想到了这个anti,所以给出了对应的解决方案;
vm.addNotFoundClass("java/lang/string"); // anti unidbg
  • 在构建虚拟机的时候加上这个即可;语义也很明显;
  • 之所以有anti的效果是因为安卓是找不到这个类的,但是unidbg不知道,它是无法感知java环境的,所以不管找什么类,它通通接受,所以就造成了环境上的差异;
  • 再次运行,就到正轨了;
java.lang.UnsupportedOperationException: com/jd/security/jdguard/core/Bridge->getAppContext()Landroid/content/Context;
    at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:508)
    at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:442)
  • 在补这个之前可以发现前面有很多系统调用的日志;

image-20260325211257074

  • 这里的系统调用号是49,我们去搜一下是什么调用;

image-20260325212027849

  • chdir(全称 change directory)是类 Unix 操作系统(如 Linux、macOS、BSD)中用于改变调用进程当前工作目录(Current Working Directory, CWD) 的系统调用;
  • 其实就是cd,切换目录,unidbg并没有实现这个系统调用;我们仿照这原来的模仿一个;
case 49:
    backend.reg_write(Arm64Const.UC_ARM64_REG_X0, chdir(emulator));
    return;    

protected int chdir(Emulator<AndroidFileIO> emulator) {
    RegisterContext context = emulator.getContext();
    String path = context.getPointerArg(0).getString(0);
    System.out.println("chdir->"+path);
    return 0;
}
  • 就会有一些输出;

image-20260325213449375

  • 大多数都是模拟器相关的文件,应该是检测模拟器,这里为何之前文件访问没有输出呢?先不管,但是这里最好不要返回0,根据语义,返回0是代表打开文件,有些文件是检测的,最好返回非0,我返回1;
protected int chdir(Emulator<AndroidFileIO> emulator) {
    RegisterContext context = emulator.getContext();
    String path = context.getPointerArg(0).getString(0);
    System.out.println("chdir->"+path);
    return 1;
}
  • 再运行,我稍微摘几个出来,还有环境检测相关的内容;
chdir->/dev/socket
chdir->/dev
chdir->/dev/socket
chdir->/dev/socket
chdir->/data
chdir->/dev
···
chdir->/sys/bus/platform/drivers
chdir->/sys/bus/platform/drivers
chdir->/system/usr/idc
chdir->/system/usr/keylayout
chdir->/system/xbin
  • 好在警告也消失了,这里其实不管应该也没事,接下来补第一个环境;
case "com/jd/security/jdguard/core/Bridge->getAppContext()Landroid/content/Context;":{
    return vm.resolveClass("android/content/Context").newObject(null);
}
  • 继续;
java.lang.UnsupportedOperationException: android/content/Context->getPackageCodePath()Ljava/lang/String;
    at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:421)
    at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
  • 这个方法是返回apk路径,是字符串;
case "android/content/Context->getPackageCodePath()Ljava/lang/String;":{
    return new StringObject(vm, "/data/app/com.jingdong.app.mall-NMPcDqBzdYwQrAm-23cOyQ==/base.apk");
}
  • 给什么其实影响不大,有可能会参与运算,那就有关系;
java.lang.UnsupportedOperationException: com/jd/security/jdguard/core/Bridge->getAppKey()Ljava/lang/String;
    at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:508)
    at com.Samples.jd.jdgs.callStaticObjectMethodV(jdgs.java:130)
  • 但是在补环境的过程中要时刻注意它打开了哪些文件,这是很容易忽视的点;

image-20260325220331989

  • 这里它打开了apk,所以我们把apk给它;
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
    System.out.println("sana file open-->>" + pathname);
    if (pathname.equals("/data/app/com.jingdong.app.mall-NMPcDqBzdYwQrAm-23cOyQ==/base.apk")){
        return FileResult.success(new SimpleFileIO(oflags,new File("src/test/java/com/Samples/jd/file/demo4.apk"), pathname));
    }
    return null;
}
  • 此时的环境是和apk有关的,所以需要去hook真机的返回值;直接调用就好,因为是没有参数的;
case "com/jd/security/jdguard/core/Bridge->getAppKey()Ljava/lang/String;":{
    return new StringObject(vm, "6bbc8976-d0fe-44ef-9776-ab9da0c127e8");
}
  • 实际上这里就是b1,应该是一个apk版本的标识,不同版本是不一致的;继续运行;
java.lang.UnsupportedOperationException: com/jd/security/jdguard/core/Bridge->getJDGVN()Ljava/lang/String;
    at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:508)
    at com.Samples.jd.jdgs.callStaticObjectMethodV(jdgs.java:137)
  • 这个是写定的,版本号吧,也就是b2;
case "com/jd/security/jdguard/core/Bridge->getJDGVN()Ljava/lang/String;":{
    return new StringObject(vm,"3.2.8.4");
}
  • 继续执行;
java.lang.UnsupportedOperationException: com/jd/security/jdguard/core/Bridge->getPicName()Ljava/lang/String;
    at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:508)
    at com.Samples.jd.jdgs.callStaticObjectMethodV(jdgs.java:140)
  • 都是一样的道理,都是没有参数的方法,主动调用一下就好;
case "com/jd/security/jdguard/core/Bridge->getPicName()Ljava/lang/String;":{
    return new StringObject(vm,"6bbc8976-d0fe-44ef-9776-ab9da0c127e8.jdg.jpg");
}
java.lang.UnsupportedOperationException: com/jd/security/jdguard/core/Bridge->getSecName()Ljava/lang/String;
    at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:508)
    at com.Samples.jd.jdgs.callStaticObjectMethodV(jdgs.java:146)
  • 继续补上;
case "com/jd/security/jdguard/core/Bridge->getSecName()Ljava/lang/String;":{
    return new StringObject(vm,"6bbc8976-d0fe-44ef-9776-ab9da0c127e8.jdg.xbt");
}
  • 到这里初始化就执行完成了;

image-20260325221413190

  • 接下来就可以调用目标函数了;

image-20260326113704405

  • 别忘了看看打开了哪些文件这里就可以先不管,因为已经跑出结果了,如果有问题我们再补上;
  • 来调用目标函数,使用和frida主动调用一致的参数;
public void callFun() {
    List<Object> list = new ArrayList<>(4);
    list.add(vm.getJNIEnv());
    list.add(0);
    list.add(101);
    byte[] bytes = "POST /client.action avifSupport=1&b9aa898b3bc3febb04a7b0d1cc099577fded30aac89532ad35d0ceaab89be770=&bef=1&build=99208&client=android&clientVersion=13.1.0&ef=1&eid=eidA943781213fsbM1YH2pJ2Rfqo%2B%2B7swN3q3zNWCW%2B5amEMdjBONjMO5gqo6HorEr9UADNI1%2BJqE1zYmDNNluQ2UBN2%2FhIKcsN7ffqaBKB3jQrbejky&ep=%7B%22hdid%22%3A%22JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw%3D%22%2C%22ts%22%3A1739247938442%2C%22ridx%22%3A-1%2C%22cipher%22%3A%7B%22area%22%3A%22CV83Cv81DJY3DP8m%22%2C%22d_model%22%3A%22UQv4ZWm0WOm%3D%22%2C%22wifiBssid%22%3A%22dW5hbw93bq%3D%3D%22%2C%22osVersion%22%3A%22CJK%3D%22%2C%22d_brand%22%3A%22H29lZ2nv%22%2C%22screen%22%3A%22Ctu4DMenDNGm%22%2C%22uuid%22%3A%22YzG0CtOmDNczDzK5CWVtEG%3D%3D%22%2C%22aid%22%3A%22YzG0CtOmDNczDzK5CWVtEG%3D%3D%22%2C%22openudid%22%3A%22YzG0CtOmDNczDzK5CWVtEG%3D%3D%22%7D%2C%22ciphertype%22%3A5%2C%22version%22%3A%221.2.0%22%2C%22appname%22%3A%22com.jingdong.app.mall%22%7D&ext=%7B%22prstate%22%3A%220%22%2C%22pvcStu%22%3A%221%22%2C%22cfgExt%22%3A%22%7B%5C%22privacyOffline%5C%22%3A%5C%220%5C%22%7D%22%7D&functionId=search&harmonyOs=0&lang=zh_CN&networkType=wifi&partner=xiaomi001&sdkVersion=29&sign=3b5c882977fdb680c297d34adaf01931&st=1739248022333&sv=100&uemps=2-2-2&x-api-eid-token=jdd01IHFGXYKIKWYJTG3BV5U6TBHKBPYJKQ35JYSSNHEY434T633JS5HWSPQN5KE7PNU7YQXZ7VPOCSLZC2VQSK2SYTIBEYC4PO5OAKVE6GY01234567".getBytes();
    ByteArray arr = new ByteArray(vm, bytes);

    StringObject str_1 = new StringObject(vm, "coral|-|coral|-|-|-|-|-|-|-|-|-|-|-|-|-|-§§0|1§§-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-§§-|53684973568§§-|-|1.0|-§§1|1|-|-|-|-|-|-|-|-|-§§google/coral/coral:10/QD1A.190821.007/5831595:user/release-keys/|-|-|1739164885415|-§§-|-|-|-|-|-|-");
    StringObject str_2 = new StringObject(vm, "eidA943781213fsbM1YH2pJ2Rfqo++7swN3q3zNWCW+5amEMdjBONjMO5gqo6HorEr9UADNI1+JqE1zYmDNNluQ2UBN2/hIKcsN7ffqaBKB3jQrbejky");
    StringObject str_3 = new StringObject(vm, "1.0");
    StringObject str_4 = new StringObject(vm, "83");
    ArrayObject arrayObject = new ArrayObject(arr, str_1, str_2, str_3, str_4);
    list.add(vm.addLocalObject(arrayObject));

    Number number = module.callFunction(emulator, 0x29CE4, list.toArray());
    ArrayObject resultArr = vm.getObject(number.intValue());
    System.out.println("result:" + resultArr);
}
  • 直接执行就出结果了;
{"b1":"6bbc8976-d0fe-44ef-9776-ab9da0c127e8","b2":"3.2.8.4_0","b3":"2.1","b4":"Vdc/tUrcpww1IneAQUqwAE+Vdaelo3Ol6FP5kqhM+92Yb9NyIRqHyQMzvbz3K5+lvmvPc9C2cnRDGbMhgH8iIp3CMeFDBJbPUh8zPWSMoYmVBC1nfgWakgBOkjg+kVr4QKTzZ6lbSB9+hUmfX6nakFuqEYBKxmvwpnCGEyoy/XOL7lovdQRJpUA6fTrgriSeGD3Bvpan/vAOoQmurXGoLT76zDGU6lsZlF9ysL/WOhUC2FYNqHN4EYkDVlrPzOwZEHmgoIzd20evwaAN2ag9PfaB9pF9K/T5oyH4xYDewEGhGaeXoMYarWW/IANw1Xc0nZK3RJFiz6bSA2vSbeeYy47cbPil3TDJssj5FR2aqoMrsaghqDDx2vNeUznuA8KK1MZl3kjIBqOomlmYMJxQcmfUi+08Q+RFcfl4kQ==","b5":"dbce5813d079c83c6eb5f9ba29dea1b8e8a28bca","b7":"1739248022343","b6":"6cfbd8188067f072206b406e551a45cb5f17fc2e"}
  • 还记得吗?真机的b1、2、3、5是不变的;
真机: {"b1":"6bbc8976-d0fe-44ef-9776-ab9da0c127e8","b2":"3.2.8.4_0","b3":"2.1","b4":"PYWts9KIZxuWCxltyWLX8iBg7HnRN2BOvgaoCgsYtnEgjiUFrBqNhIGiOYhNHkj+G/SUOqOVJGFkZ3nWV7bnVNlJWv7HcT7flRtLjbNaLrcxyRJwC3AbX2B7l9Xk2L+dvYZQ0AgiUVSz6oa3dfz1HXhhN5MRJFfCxNyGsvBM1f7z6CnS4QzKFV9Lt7sOqVwJn07XKHPKGCDm38JFarly14Nzn/b7RBlizMcGxSRRW/1a4Xi8NLSF4I9zAf8KaWhy+l8QE7mRwG+cXl3Pb4qtBx81GPBGs/JROl3ChJqNylWmwd8qJ32rHA+QYbqo1HG7NzcPvxKekC5NPNniBe7y0uHUppRCttTDxOqSeUGnFo7Lob4wGFRkWLufq81ka5n9dl3nwLplGFZtVRipD8kclrJ95fr7wnAwWaU6X1lV","b5":"79154e49c97f5d8e36bc36614755281a9b393246","b7":"1774319857360","b6":"8daac1d15d2c3c0dd35d8ea1aa6ba11f28998f6f"}
  • b7是一个时间戳,在unidbg中固定为1739248022343;
  • 这里几乎都是不一样的,不过由于固定了时间戳,所以结果是固定的,那b5对不上就说不过去了,应该是还有暗桩,我们还需要接着排查;
  • 这里为何是这种思想?真机我们说了,b1、2、3、5是不变的,不管是什么时间戳他都不变,所以它与时间戳没有关系,但是unidbg同一份入参跑出来结果不一样就肯定是有问题的;
  • 我们对比一下:
uni:dbce5813d079c83c6eb5f9ba29dea1b8e8a28bca
真机:79154e49c97f5d8e36bc36614755281a9b393246
  • 怎么分析?找它怎么生成出来的,大概率是在某个位置走了异常的分支所以返回了错误的结果;

4.1 排除暗桩

  • 先trace一份日志吧,由于我们是在排除暗桩,所以只需要trace函数调用这一块就好了,前面的环境先不管;把trace放在两次init调用的后面,不然日志会多上千万行;
private void traceLog() {
    String traceFile = "src/test/java/com/Samples/jd/trace/trace1.log";
    PrintStream traceStream = null;
    try {
        traceStream = new PrintStream(new FileOutputStream(traceFile), true);
    } catch (FileNotFoundException e) {
        throw new RuntimeException(e);
    }
    emulator.traceCode(module.base, module.base + module.size).setRedirect(traceStream);
}
  • 这一部分是23万行,我们去找b5的生成,首先我肯定认为它是一个hash函数,40位大概率是sha1函数,我们搜一下4字节的结果,因为可能是hash,而大部分hash函数都是4字节运算;

image-20260327150454463

  • 确实有结果,找对应的偏移:0x16b40,跳过去是一个hash函数;

image-20260327150553811

  • 特征还是比较明显的,搜一下其中的常量是一个sha1函数;它是sub_15974函数,hook看看入参;

image-20260327150737030

  • 看起来是经过了两次调用,我们可以尝试hook一下这个函数,但是我不是很建议,因为这是一个trasform函数,太里层了,容易有多次调用,我们可以去看交叉引用找外面一点的;

image-20260327164541670

  • 我们hook 1554c 这个函数,它的x0就是目标;

image-20260327170211640

  • 我们把这一部分拿去做一个sha1,看看结果是不是b5;

image-20260327170240559

  • 确实是b5,所以它的来源是一个sha1运算,明文的话大部分是我们传的参数,只是尾部有0x20字节的附加;
  • 我们需要对比真机,同样hook这个函数然后主动调用,看看它的明文会是什么;
function hook_0x1554C() {
    let soAddr = Module.findBaseAddress("libjdg.so");
    let funcAddr = soAddr.add(0x15654)  //32位的话记得+1
    let num = 0
    Interceptor.attach(funcAddr, {
        onEnter: function (args) {
            num += 1
            if (num === 1) {
                console.log('onEnter arg[0]: ', hexdump(args[1], {length: 0x554}))
            }
        },
        onLeave: function (retval) {
        }
    });
}
  • 0x1554C这个地址不输出,我们往下hook一层,hook 0x15654这个函数,是一样的,主动调用如下:
[Pixel 6::com.jingdong.app.mall ]-> call_main()
onEnter arg[0]:               0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
71b508d7c0  50 4f 53 54 20 2f 63 6c 69 65 6e 74 2e 61 63 74  POST /client.act
71b508d7d0  69 6f 6e 20 61 76 69 66 53 75 70 70 6f 72 74 3d  ion avifSupport=
···················省略
71b508dcb0  54 36 33 33 4a 53 35 48 57 53 50 51 4e 35 4b 45  T633JS5HWSPQN5KE
71b508dcc0  37 50 4e 55 37 59 51 58 5a 37 56 50 4f 43 53 4c  7PNU7YQXZ7VPOCSL
71b508dcd0  5a 43 32 56 51 53 4b 32 53 59 54 49 42 45 59 43  ZC2VQSK2SYTIBEYC
71b508dce0  34 50 4f 35 4f 41 4b 56 45 36 47 59 30 31 32 33  4PO5OAKVE6GY0123
71b508dcf0  34 35 36 37 21 97 45 07 96 9e f8 31 cb ec c0 48  4567!.E....1...H
71b508dd00  7c fb 5a 01 e5 4d c2 b3 c3 e3 01 d8 0e 59 40 d5  |.Z..M.......Y@.
71b508dd10  46 ff ec 4f                                      F..O
call_main res: {"b1":"6bbc8976-d0fe-44ef-9776-ab9da0c127e8","b2":"3.2.8.4_0","b3":"2.1","b4":"78dhbW55N61Ahf8rIPPwC42ul1YmuGxYmb08MXYiQLNEow1K6kVrCeqq2AtwhaeFzyeTMk0S0V58KsPh9Ngr/Jxd0QpyWstMlPrO0VUlhXFJTji5jZgtcE2541j94dNoYa2moQ
Bl85IGOaCQ9Juxtd3J+Dt4O3bIhlxNSRIRA6vszslcioFEaPZPTQTL18Q4YNIxEUe8evaaYxggyFmxolUPPf9efcayL6yHVLpcf05YijiXTZhYSaqyi7I0a/DD6i2jRK/A131kid8jOrDNkdpzwYF4obOR2qxZ2s8j2gmbcbNYfcY21j1p70qQZU2ckNxHjfsFv4N9Vfsjk0u0TtzgVptJnbO68AsVj/sj+mObwAqmj7qqdBbxIV7oI3wtO1425W1Qn/UJ8dIeiXNCCAlCQdUvrlOpZEujTbA=","b5":"79154e49c97f5d8e36bc36614755281a9b393246","b7":"1774602942936","b6":"ae28968527690971e1aa35849b53a88b861bc4bc"}
  • 这里的b5是正确的,你可以把这段去做一下sha1,是这个结果,所以我们要看它们之间的差异;
uni:6f747953711cf7ae1a741be15fa96b02d751280030d0faf2ab66854e0edd4e46
真机:21974507969ef831cbecc0487cfb5a01e54dc2b3c3e301d80e5940d546ffec4f
  • 现在就是找出它们之间的差异就好了,搜一下这个值看看有没有收获;

image-20260327172207577

  • 有收获,去找这个地址的写入吧;
emulator.traceWrite(0x40420bc0, 0x40420bc0 + 16);

image-20260327173546309

  • 去对应的偏移看看吧;

image-20260327173609363

  • 在sub_142F4函数里,看起来是一个查表的东西,我们去搜一下这个表里的内容;

image-20260327175617926

image-20260327175644167

  • 应该是一个aes的查表法,尝试hook一下sub_142F4函数;
emulator.attach().addBreakPoint(module.base + 0x142F4);
  • 它有三个函数,而这个函数的原型大概是这样:
void aes_encrypt_block(const uint8_t *in, uint8_t *out, const AES_KEY *key);
  • 看一下参数:
mx0

>-----------------------------------------------------------------------------<
[17:57:51 925]x0=unidbg@0xbfffee70, md5=f7556bcf9b8f2cb957e164d86f09bc74, hex=9d6cfa34217b5601536ec83fe9bfb88130313032303330343035303630373038b0efffbf00000000000000000000000000000b400000000000000b400000000000000b400000000000000b400000000000f7ffbf00000000800b424000000000dbe009400000000060f0ffbf00000000
size: 112
0000: 9D 6C FA 34 21 7B 56 01 53 6E C8 3F E9 BF B8 81    .l.4!{V.Sn.?....
0010: 30 31 30 32 30 33 30 34 30 35 30 36 30 37 30 38    0102030405060708
0020: B0 EF FF BF 00 00 00 00 00 00 00 00 00 00 00 00    ................
0030: 00 00 0B 40 00 00 00 00 00 00 0B 40 00 00 00 00    ...@.......@....
0040: 00 00 0B 40 00 00 00 00 00 00 0B 40 00 00 00 00    ...@.......@....
0050: 00 F7 FF BF 00 00 00 00 80 0B 42 40 00 00 00 00    ..........B@....
0060: DB E0 09 40 00 00 00 00 60 F0 FF BF 00 00 00 00    ...@....`.......
^-----------------------------------------------------------------------------^
mx2 280

>-----------------------------------------------------------------------------<
[17:59:18 292]x2=unidbg@0xbfffef10, md5=b6a29bca9e5955009a9dd7a0744fddf8, hex=6daa32e24b3a1efd2ed220df9f3edd76557180221e4b9edf3099be00afa763766d08dcdb7343420443dafc04ec7d9f722dc623045e8561001d5f9d04f12202761567b07b4be2d17b56bd4c7fa79f4e09143b6b445fd9ba3f0964f640aefbb8492fdf64087006de3779622877d799903e9dd18a28edd7541f94b57c68432cec562ccbfb66c11caf7955a9d31116853f478c8c6c084d90c371183910600ebc2f274027092b0db7ca5a158eda3a1b32f51d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000dbfd7842000000000000000000000b4000000000000000000000000000f7ffbf00000000
size: 280
0000: 6D AA 32 E2 4B 3A 1E FD 2E D2 20 DF 9F 3E DD 76    m.2.K:.... ..>.v
0010: 55 71 80 22 1E 4B 9E DF 30 99 BE 00 AF A7 63 76    Uq.".K..0.....cv
0020: 6D 08 DC DB 73 43 42 04 43 DA FC 04 EC 7D 9F 72    m...sCB.C....}.r
0030: 2D C6 23 04 5E 85 61 00 1D 5F 9D 04 F1 22 02 76    -.#.^.a.._...".v
0040: 15 67 B0 7B 4B E2 D1 7B 56 BD 4C 7F A7 9F 4E 09    .g.{K..{V.L...N.
0050: 14 3B 6B 44 5F D9 BA 3F 09 64 F6 40 AE FB B8 49    .;kD_..?.d.@...I
0060: 2F DF 64 08 70 06 DE 37 79 62 28 77 D7 99 90 3E    /.d.p..7yb(w...>
0070: 9D D1 8A 28 ED D7 54 1F 94 B5 7C 68 43 2C EC 56    ...(..T...|hC,.V
0080: 2C CB FB 66 C1 1C AF 79 55 A9 D3 11 16 85 3F 47    ,..f...yU.....?G
0090: 8C 8C 6C 08 4D 90 C3 71 18 39 10 60 0E BC 2F 27    ..l.M..q.9.`../'
00A0: 40 27 09 2B 0D B7 CA 5A 15 8E DA 3A 1B 32 F5 1D    @'.+...Z...:.2..
00B0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00C0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
····
0100: 00 00 0B 40 00 00 00 00 00 00 00 00 00 00 00 00    ...@............
0110: 00 F7 FF BF 00 00 00 00                            ........
^-----------------------------------------------------------------------------^
  • x0应该是明文,分组是16字节也就是第一排,x2的话是key,这里看起来应该是秘钥编排的结果;在分析aes的时候可以多多留意这样的数据,它是11排的,就有可能是秘钥编排的结果,我们可以把第一排拿去本地编排一下,看看结果是否一样,这里我不试了,确实是,但是要注意是小端序,我们读的时候注意了;

image-20260327180040634

  • 仔细看,是小端序的,所以这里的key是e232aa6dfd1e3a4bdf20d22e76dd3e9f,明文就是9d6cfa34217b5601536ec83fe9bfb881这一部分,加密出来确实是目标0x20的前16字节,后续的也是一致的,明文是7f646943610ce7be0a640bf14fb97b12;
  • 接下来看真机,我hook了0x142F4这个函数,但是压根没走;

image-20260327182549559

  • 所以问题大概出在这里,我们把unidbg的调用栈输出出来,一个一个排查;
bt
[0x040000000][0x040014d20][    libjdg.so][0x14d20]
[0x040000000][0x04001da70][    libjdg.so][0x1da70]
[0x040000000][0x040022b00][    libjdg.so][0x22b00]
[0x040000000][0x0400215c0][    libjdg.so][0x215c0] 1
[0x040000000][0x040021000][    libjdg.so][0x21000]
[0x040000000][0x0400205c8][    libjdg.so][0x205c8]
[0x040000000][0x04002b8b8][    libjdg.so][0x2b8b8]
  • 接下来我们要做的就是一个一个尝试真机上hook,看看到哪里和unidbg出现分岔了;
  • 先是0x14d20、0x1da70,真机不走,再是0x22b00,真机依旧不走;到0x215c0位置真机走了,当前函数是sub_21460;

image-20260327184317915

  • 我们对这个函数进行分析就好了,看看unidbg走的哪里;

image-20260327184343154

  • 我们要知道真机走了哪里,所以我把涉及到v11的函数hook上了,因为最后返回值是v11;

image-20260327184524713

  • 最后发现走的是sub_27688这个函数,我们hook看看出入参;
function hook_0x1554C() {
    let soAddr = Module.findBaseAddress("libjdg.so");
    let funcAddr = soAddr.add(0x27688)
    let num = 0
    Interceptor.attach(funcAddr, {

        onEnter: function (args) {
            console.log('onEnter arg[0]: ', hexdump(args[0],{length:args[1].toInt32()}))
            console.log('onEnter arg[1]: ', (args[1]))
            this.res = args[2]
        },
        onLeave: function (retval) {
            console.log('retval: ', hexdump(this.res, {length: 0x20}))
        }
    });
}
[Pixel 6::京东 ]-> call_main()
onEnter arg[0]:               0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
703145e568  5c 0f 60 b8 4e 02 8b 63 30 38 7d 0f 56 7e b4 c7  \.`.N..c08}.V~..
onEnter arg[1]:  0x10
retval:               0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
70c50bf9d0  21 97 45 07 96 9e f8 31 cb ec c0 48 7c fb 5a 01  !.E....1...H|.Z.
70c50bf9e0  e5 4d c2 b3 c3 e3 01 d8 0e 59 40 d5 46 ff ec 4f  .M.......Y@.F..O
onEnter arg[0]:               0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
703145e6f8  a9 b0 16 d8 29 6a 06 ff 2d 29 c3 69 9a ff 22 ab  ....)j..-).i..".
onEnter arg[1]:  0x10
retval:               0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
70c50bf9d0  e6 28 78 59 68 99 55 c7 25 f0 41 38 3c 2b 90 af  .(xYh.U.%.A8<+..
70c50bf9e0  f4 af 9b 56 d4 e9 46 27 85 c2 bf f6 48 73 01 f8  ...V..F'....Hs..
  • 看第一次的结果就好了,这里确实是真机走的位置,那我们要着手分析为何会出现分岔;
  • 这里我将全部伪代码放出来,截图的话都太长了;
void *__fastcall sub_22850(__int64 a1, __int64 a2, unsigned int a3, size_t *a4)
{
  v18 = v4;
  v19 = v5;
  v17 = *(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  dword_B0154 = 0;
  dword_B0150 = 0;
  if ( !a1 || !a2 )
    goto LABEL_15;
  v10 = sub_22E74(a3);
  dword_B0154 = 0;
  dword_B0150 = 0;
  if ( v10 <= 32 )
  {
    if ( !v10 )
    {
      dword_B0154 = 0;
      dword_B0150 = 0;
      v11 = sub_297FC(a1, a2, a4);
      goto LABEL_16;
    }
    if ( v10 == 1 )
    {
      dword_B0154 = 0;
      dword_B0150 = 0;
      v11 = malloc(0x20uLL);
      sub_27688(a1, a2, v11);                   // 真机走了这里
LABEL_14:
      *a4 = 32LL;
      goto LABEL_16;
    }
    goto LABEL_15;
  }
  if ( v10 != 149 )
  {
    if ( v10 == 33 )
    {
      dword_B0154 = 0;
      dword_B0150 = 0;
      v11 = malloc(0x20uLL);
      if ( (byte_AC920 & 1) == 0 )
        sub_19DDC(&unk_9E322, &unk_9E4A0, 32LL, 81LL);
      byte_AC920 = 1;
      memcpy(dest, &unk_9E322, sizeof(dest));
      v12 = 0LL;
      v13 = vdupq_n_s8(a3);
      dword_B0154 = 0;
      dword_B0150 = 0;
      do
      {
        *&dest[v12] = veorq_s8(*&dest[v12], v13);
        v12 += 16LL;
      }
      while ( v12 != 80 );
      dword_B0154 = 0;
      dword_B0150 = 0;
      sub_26E38(dest, 80LL, a1, a2, v11, 32LL); // 真机没走
      goto LABEL_14;
    }
LABEL_15:
    v11 = 0LL;
    *a4 = 0LL;
    goto LABEL_16;
  }
  dword_B0154 = 0;
  dword_B0150 = 0;
  sub_1D958(dest);
  if ( (byte_AC921 & 1) == 0 )
    sub_19DDC(&xmmword_9E373, &unk_9E500, 33LL, 17LL);
  byte_AC921 = 1;
  v15 = xmmword_9E373;
  v11 = sub_1D9C8(dest, a1, a2, &v15, a4);      // unidbg走这里了
  nullsub_13(dest);
LABEL_16:
  dword_B0154 = 0;
  dword_B0150 = 0;
  return v11;
}
  • 可以发现,关键点在于这里;
v10 = sub_22E74(a3);

if ( v10 == 1 )
{
  dword_B0154 = 0;
  dword_B0150 = 0;
  v11 = malloc(0x20uLL);
  sub_27688(a1, a2, v11);                   // 真机走了这里
  • v10控制分支,v10等于1的时候是真机的路径,而unidbg是0x95,你可以hook一下看看真机这里的值究竟是多少,我的第一想法就是把这个函数的返回值改成1;
  • 但是改了之后依然和真机不同,那我们就要考虑这个sub_22E74函数内部还有什么东西了,我们进去看看;
__int64 __fastcall sub_22E74(char a1)
{
  dword_B0154 = 0;
  dword_B0150 = 0;
  v2 = off_9E140();
  v3 = sub_200FC(v2);                           // 真机6次0 unidbg是0 以及5次1
  dword_B0154 = 0;
  dword_B0150 = 0;
  if ( v3||(dword_B0154 = 0, dword_B0150 = 0, v4 = atomic_load(&dword_AC900), ···, dword_B0150 = 0, v4 != 3) )
  {
    result = 149LL;
  }
  else
  {
    dword_B0154 = 0;
    dword_B0150 = 0;
    if ( (a1 & 0xF) == 1 )
    {
      result = 33LL;
      goto LABEL_7;
    }
    result = (a1 & 0xF) == 3;                   // 真机应该是这里返回的
  }
  dword_B0154 = 0;
  dword_B0150 = 0;
LABEL_7:
  dword_B0154 = 0;
  dword_B0150 = 0;
  return result;
}
  • 观察可以发现,我们得走这一部分;
    dword_B0154 = 0;
    dword_B0150 = 0;
    if ( (a1 & 0xF) == 1 )
    {
      result = 33LL;
      goto LABEL_7;
    }
    result = (a1 & 0xF) == 3;                   // 真机应该是这里返回的
  • 也就是if判断的else分支,这关键点就在于v3,它来自于这个语句;
v3 = sub_200FC(v2);                           // 真机6次0 unidbg是0 以及5次1
  • 我的注释写了真机和unidbg的区别,这里我hook验证一下;

image-20260327223409493

image-20260327223525783

  • 它们之间也是有差异的,所以我认为是这里在分岔,我们把这里全部返回0试试;
public void patch() {
    emulator.attach().addBreakPoint(module.base + 0x200FC, new BreakPointCallback() {
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            RegisterContext ctx = emulator.getContext();
            emulator.attach().addBreakPoint(ctx.getLRPointer().peer, new BreakPointCallback() {
                @Override
                public boolean onHit(Emulator<?> emulator, long address) {
                    Backend backend = emulator.getBackend();
                    backend.reg_write(Arm64Const.UC_ARM64_REG_X0, 0);
                    return true;
                }
            });
            return true;
        }
    });
}
  • 再次运行结果对得上了,可以开始算法分析了;
uni:
{"b1":"6bbc8976-d0fe-44ef-9776-ab9da0c127e8","b2":"3.2.8.4_0","b3":"2.1","b4":"Vdc/tUrcpww1IneAQUqwAE/jD7LKEWXSR6Q/uDxI7tY5UUTrcSxH3aY3Y4CKlZr/Pn2P8oIWsnr9L0TVGT6sIBucMPqC/eztdACv5o1kPvyCoNivHV16t9FkRmrkBHx8IBnPROvCTMuDnYwzUHaJjYODL4twpSSOrcz/fxXBj+4QBj20XeAv8JGvwkk6VkVdaJF78XS8pXcTtqDwg4LvKd5ZUGp3lKjXuYcNQ1obW4we+FPzCj/hkGhFiEgtoIHjoZDDsmHrcl8+se8QuXD+WdEkWvOfD7CE0BVn014v/TEiWmT2hHIMIywAVxIVii+kEy0aIaT8TQQnZh17hH4jkWeGX6nQ5nOFnvBXLOV+lQEbeKkDMgYpdhO6C3SZoRMuZlrAEibfLHdgh3gFKLkeX/TxW2/exS/ULNPWM60=","b5":"79154e49c97f5d8e36bc36614755281a9b393246","b7":"1739248022343","b6":"f635db8d38e36384f6f8f4c0d47834a9fd908f4d"}
真机:
{"b1":"6bbc8976-d0fe-44ef-9776-ab9da0c127e8","b2":"3.2.8.4_0","b3":"2.1","b4":"PYWts9KIZxuWCxltyWLX8iBg7HnRN2BOvgaoCgsYtnEgjiUFrBqNhIGiOYhNHkj+G/SUOqOVJGFkZ3nWV7bnVNlJWv7HcT7flRtLjbNaLrcxyRJwC3AbX2B7l9Xk2L+dvYZQ0AgiUVSz6oa3dfz1HXhhN5MRJFfCxNyGsvBM1f7z6CnS4QzKFV9Lt7sOqVwJn07XKHPKGCDm38JFarly14Nzn/b7RBlizMcGxSRRW/1a4Xi8NLSF4I9zAf8KaWhy+l8QE7mRwG+cXl3Pb4qtBx81GPBGs/JROl3ChJqNylWmwd8qJ32rHA+QYbqo1HG7NzcPvxKekC5NPNniBe7y0uHUppRCttTDxOqSeUGnFo7Lob4wGFRkWLufq81ka5n9dl3nwLplGFZtVRipD8kclrJ95fr7wnAwWaU6X1lV","b5":"79154e49c97f5d8e36bc36614755281a9b393246","b7":"1774319857360","b6":"8daac1d15d2c3c0dd35d8ea1aa6ba11f28998f6f"}
  • 但是其他两个参数实际上也还不一样,但是本身真机也会变,应该是与时间戳有关,我们可以往下进行算法分析了;
© 版权声明
THE END
喜欢就支持一下吧
点赞 0 分享 收藏
评论 抢沙发
OωO
取消
SSL