京东算法分析

# 1.首先charles抓包发现每个请求Url后都接了一个sign的参数且每次都不一样。也没有其他的一些别的特别参数,那么关键问题就是分析sign参数的生成了

# 2.jadx反编译,寻找sign的生成的位置

> 直接搜索sign参数匹配的出来的结果太多了,一时间不好区分哪个是真的。于是使用getSign为前缀搜索

> 运气不错搜索到一个native方法 前缀也是getSign的方法名,打开这个类。找下引用位置进一步确认下是否为计算sign的类

> 最终在BaseApplication里面找到这个类引用,可以确定这个就是计算sign的类

# 3.开始分析sign方法的入参

frida hook这个方法得到结果如下:

1

2

3

4

5

6

7

8

9

10

11

function main(){

     Java.perform(function(){

       var BitmapkitUtils =  Java.use("com.jingdong.common.utils.BitmapkitUtils")

       BitmapkitUtils.getSignFromJni.overload('android.content.Context''java.lang.String''java.lang.String''java.lang.String''java.lang.String''java.lang.String').implementation = function( context,  str,  str2,  str3,  str4,  str5){

             console.log("签名入参为: ","str         =>",str,"str2=>",str2,"str3=>",str3,"str4=>",str4,"str5=>",str5)

            var result = this.getSignFromJni(context,str,str2,str3,str4,str5)

             console.log('签名sign为 ',result)

            return result

      }

     })

 }

> 根据hook结果稍加分析大概知道各个参数代表的意义:

>context:上下文对象

>str:url路径

>str2:请求body信息

>str3:55a9c688729bb118  为KEY 16位长度,且每次请求都固定

>str4:android 为平台

>str5:版本号

> 查看源码大概得出str3 的为installationId生成规则是UUID 随机16位长度,在app第一次安装的时候创建保存在缓存中.

# 4.使用unidbg进行黑盒调用

> 1.第一步当然是补环境了:

> 需要补充返回一个application对象,偷个懒想从AbstractJni 这里copy一份

> 重新运行一下又报错了,这个错是找不到对应的methodId、于是想着把Application换成Activity也可以,毕竟Activity也是可以获取Application对象的

1

2

3

4

5

6

7

8

9

10

  @Override

     public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {

         if ("com/jingdong/common/utils/BitmapkitUtils->a:Landroid/app/Application;".equals(signature)) {

             //返回appliation

             return vm.resolveClass("android/app/Activity",

                    vm.resolveClass("android/content/ContextWrapper",

                             vm.resolveClass("android/content/Context"))).newObject(null);

        }

         return super.getStaticObjectField(vm, dvmClass, signature);

     }

> 重新运行就没问题了,接下来继续报错补环境,返回apk的路径。很简单

1

2

3

4

5

6

7

8

9

 @Override

 public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) {

     //sourceDir 代表当前apk目录

     if("android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;".equals(signature)){

         StringObject stringObject = new StringObject(vm, APK_PATH);

         return stringObject;

     }

     return super.getObjectField(vm, dvmObject, signature);

 }

> 图上这个返回刚开始不知道传具体什么参数,于是打算看看源码。结果没有反编译出来具体的函数实现。于是用frida hook了这个方法拿到图下的返回 

> 第一个参数为:app安装目录  第二个参数为META-INF 目录 第三个是RSA

> 既然这样就直接返回RSA公钥了

1

2

3

4

5

6

7

8

9

@Override

     public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {

         if ("com/jingdong/common/utils/BitmapkitZip->unZip(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)[B".equals(signature)) {

             byte[] unzip = vm.unzip("META-INF/xxxx.RSA");

             System.out.println("unzip " new String(unzip));

             return new ByteArray(vm, unzip);

         }

         return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);

     }

> 继续补充环境

1

2

3

4

5

6

7

8

9

10

11

12

13

14

 @Override

 public DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {

     if ("sun/security/pkcs/PKCS7-><init>([B)V".equals(signature)) {

         DvmObject<?> objectArg = varArg.getObjectArg(0);

         try {

             PKCS7 pkcs7 = new PKCS7((byte[]) objectArg.getValue());

             return vm.resolveClass("sun/security/pkcs/PKCS7").newObject(pkcs7);

         catch (ParsingException e) {

             e.printStackTrace();

         }

  

     }

     return super.newObject(vm, dvmClass, signature, varArg);

 }

> 继续补充环境,需补充如下

1

2

3

4

5

6

7

8

9

10

11

12

 @Override

     public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {

         if ("sun/security/pkcs/PKCS7->getCertificates()[Ljava/security/cert/X509Certificate;".equals(signature)) {

             PKCS7 pkcs7 = (PKCS7) dvmObject.getValue();

             X509Certificate[] certificates = pkcs7.getCertificates();

             DvmObject<?> object = ProxyDvmObject.createObject(vm, certificates);

             return object;

  

  

         }

         return super.callObjectMethod(vm, dvmObject, signature, varArg);

     }

> 继续补充环境,从反编译的源码中copy 一份objectToBytes方法即可然后构造ByteArray返回即可

1

2

3

4

5

 if("com/jingdong/common/utils/BitmapkitZip->objectToBytes(Ljava/lang/Object;)[B".equals(signature)){

     DvmObject<?> objectArg = varArg.getObjectArg(0);

     byte[] bytes = objectToBytes(objectArg.getValue());

     return new ByteArray(vm,bytes);

 }

# 5.开始黑盒调用计算sign的方法

1

2

3

4

5

6

7

8

9

10

11

 List<Object> params = new ArrayList<>();

        params.add(dalvikVM.getJNIEnv());

        params.add(0);

        DvmClass context = dalvikVM.resolveClass("android/content/Context");

        params.add(dalvikVM.addLocalObject(context.newObject(null)));

        params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, "personinfoBusiness")));

        params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, "{\"callCJH\":\"1\",\"callNPS\":\"1\",\"closeJX\":\"0\",\"headTaskRefresh\":\"1\",\"locationArea\":\"0_0_0_0\",\"menuStaticSource\":\"0\",\"menuTimeStamp\":\"1631586010000\"}")));

        params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, INSTALL_ID)));

        params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, PLAT_FROM)));

        params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, VERSION)));

        Number numbers = moduleModule.callFunction(androidEmulator, 0x028B4+1, params.toArray())[0];

> 运行报错,还缺少环境

1

2

3

4

5

6

7

8

 @Override

 public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {

     if("java/lang/StringBuffer-><init>()V".equals(signature)){

         StringBuffer stringBuffer = new StringBuffer();

         return vm.resolveClass("java/lang/StringBuffer").newObject(stringBuffer);

     }

     return super.newObjectV(vm, dvmClass, signature, vaList);

 }

> 重新继续补充环境 这部分环境补充比较简单 直接上最后运行结果

> 黑盒调用没问题了,接下来开始分析sign具体是怎么生成的。全部代码如下:

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

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

 package jd;

  

 import com.github.unidbg.AndroidEmulator;

 import com.github.unidbg.Module;

 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.apk.Apk;

 import com.github.unidbg.linux.android.dvm.array.ArrayObject;

 import com.github.unidbg.linux.android.dvm.array.ByteArray;

 import com.github.unidbg.linux.android.dvm.jni.ProxyClassFactory;

 import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;

 import com.github.unidbg.memory.Memory;

 import org.apache.log4j.Level;

 import org.apache.log4j.Logger;

 import sun.security.pkcs.PKCS7;

 import sun.security.pkcs.ParsingException; 

 import java.io.*;

 import java.security.cert.X509Certificate;

 import java.util.ArrayList;

 import java.util.List;

  

 public class JD extends AbstractJni {

  

  

     private static final String SO_PATH = "";

     private static final String APK_PATH = "";

  

     private static final String INSTALL_ID = "55a9c688729bb118";

     private static final String PLAT_FROM = "android";

     private static final String VERSION = "10.4.6";

     private AndroidEmulator androidEmulator;

  

     public static void main(String[] args) {

         Logger.getLogger("com.github.unidbg.AbstractEmulator").setLevel(Level.DEBUG);

         Logger.getLogger("com.github.unidbg.linux.android.dvm.DalvikVM").setLevel(Level.DEBUG);

         Logger.getLogger("com.github.unidbg.linux.android.dvm.BaseVM").setLevel(Level.DEBUG);

         Logger.getLogger("com.github.unidbg.linux.android.dvm").setLevel(Level.DEBUG);

         JD jd = new JD();

         jd.start();

     }

  

  

     public void start() {

         androidEmulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.jingdong.android")

                 .build();

          Memory androidEmulatorMemory = androidEmulator.getMemory();

          androidEmulatorMemory.setLibraryResolver(new AndroidResolver(23));

          VM dalvikVM = androidEmulator.createDalvikVM(new File(APK_PATH));

         DalvikModule module = dalvikVM.loadLibrary(new File(SO_PATH), false);

         dalvikVM.setJni(this);

         Module moduleModule = module.getModule();

         dalvikVM.callJNI_OnLoad(androidEmulator, moduleModule);

         List<Object> params = new ArrayList<>();

         params.add(dalvikVM.getJNIEnv());

         params.add(0);

         DvmClass context = dalvikVM.resolveClass("android/content/Context");

         params.add(dalvikVM.addLocalObject(context.newObject(null)));

         params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, "personinfoBusiness")));

         params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, "{\"callCJH\":\"1\",\"callNPS\":\"1\",\"closeJX\":\"0\",\"headTaskRefresh\":\"1\",\"locationArea\":\"0_0_0_0\",\"menuStaticSource\":\"0\",\"menuTimeStamp\":\"1631586010000\"}")));

         params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, INSTALL_ID)));

         params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, PLAT_FROM)));

         params.add(dalvikVM.addLocalObject(new StringObject(dalvikVM, VERSION)));

         Number numbers = moduleModule.callFunction(androidEmulator, 0x028B4 1, params.toArray())[0];

         DvmObject<?> object = dalvikVM.getObject(numbers.intValue());

         System.out.println("加密结果为:" + object.getValue());

     }

  

     @Override

     public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {

         if ("com/jingdong/common/utils/BitmapkitUtils->a:Landroid/app/Application;".equals(signature)) {

             //返回appliation

             return vm.resolveClass("android/app/Activity",

                     vm.resolveClass("android/content/ContextWrapper",

                             vm.resolveClass("android/content/Context"))).newObject(null);

         }

         return super.getStaticObjectField(vm, dvmClass, signature);

     }

  

     @Override

     public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {

         if ("com/jingdong/common/utils/BitmapkitZip->unZip(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)[B".equals(signature)) {

             byte[] unzip = vm.unzip("META-INF/xx.RSA");

             System.out.println("unzip " new String(unzip));

             return new ByteArray(vm, unzip);

         else if ("com/jingdong/common/utils/BitmapkitZip->objectToBytes(Ljava/lang/Object;)[B".equals(signature)) {

             DvmObject<?> objectArg = varArg.getObjectArg(0);

             byte[] bytes = objectToBytes(objectArg.getValue());

             return new ByteArray(vm, bytes);

         }

         return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);

     }

  

     public static byte[] objectToBytes(Object obj) {

         try {

             ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

             ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);

             objectOutputStream.writeObject(obj);

             objectOutputStream.flush();

             byte[] byteArray = byteArrayOutputStream.toByteArray();

             objectOutputStream.close();

             byteArrayOutputStream.close();

             return byteArray;

         catch (IOException e) {

             return null;

         }

     }

  

     @Override

     public DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {

         if ("sun/security/pkcs/PKCS7-><init>([B)V".equals(signature)) {

             ByteArray byteArray = varArg.getObjectArg(0);

             try {

                 PKCS7 pkcs7 = new PKCS7(byteArray.getValue());

                 return vm.resolveClass("sun/security/pkcs/PKCS7").newObject(pkcs7);

             catch (ParsingException e) {

                 e.printStackTrace();

             }

  

         }

         return super.newObject(vm, dvmClass, signature, varArg);

     }

  

     @Override

     public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {

         if ("sun/security/pkcs/PKCS7->getCertificates()[Ljava/security/cert/X509Certificate;".equals(signature)) {

             PKCS7 pkcs7 = (PKCS7) dvmObject.getValue();

             X509Certificate[] certificates = pkcs7.getCertificates();

             DvmObject<?> object = ProxyDvmObject.createObject(vm, certificates);

             return object;

  

  

         }

         return super.callObjectMethod(vm, dvmObject, signature, varArg);

     }

  

     @Override

     public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) {

         //sourceDir 代表当前apk目录

         if ("android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;".equals(signature)) {

             StringObject stringObject = new StringObject(vm, APK_PATH);

             return stringObject;

         }

         return super.getObjectField(vm, dvmObject, signature);

     }

  

  

     @Override

     public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {

         if ("java/lang/StringBuffer->append(Ljava/lang/String;)Ljava/lang/StringBuffer;".equals(signature)) {

             StringBuffer stringBuffer = (StringBuffer) dvmObject.getValue();

             DvmObject<?> objectArg = vaList.getObjectArg(0);

             stringBuffer.append(objectArg.getValue().toString());

             return vm.resolveClass("java/lang/StringBuffer").newObject(stringBuffer);

         else if ("java/lang/Integer->toString()Ljava/lang/String;".equals(signature)) {

             Integer integer = (Integer) dvmObject.getValue();

             return new StringObject(vm, integer.toString());

         }else if("java/lang/StringBuffer->toString()Ljava/lang/String;".equals(signature)){

             StringBuffer stringBuffer = (StringBuffer) dvmObject.getValue();

             return new StringObject(vm, stringBuffer.toString());

         }

         return super.callObjectMethodV(vm, dvmObject, signature, vaList);

     }

  

     @Override

     public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {

         if ("java/lang/StringBuffer-><init>()V".equals(signature)) {

             StringBuffer stringBuffer = new StringBuffer();

             return vm.resolveClass("java/lang/StringBuffer").newObject(stringBuffer);

         else if ("java/lang/Integer-><init>(I)V".equals(signature)) {

             int intArg = vaList.getIntArg(0);

             Integer integer = Integer.valueOf(intArg);

             return vm.resolveClass("java/lang/Integer").newObject(integer);

         }

         return super.newObjectV(vm, dvmClass, signature, vaList);

     }

 }

# 6.ida+unidbg分析sign的具体如何生成的

ida导入so 包  export导出中找到sign生成方法  截图如下:

F5生成伪代码

根据方法的具体逻辑修改了下方法名称 方便直观查看。初步观察是讲传入参数通过key=value 的方式拼接起来

通过unidbg的执行日志中也可以发现这点

接下来开始从函数返回的地方开始往上分析,单独调用执行返回如下:

st=1648374072802&sign=8102aefd41782d405174725cd433c5a0&sv=111

从上可知要分析的为st 、sign、sv这个三个值

1.st生成分析:生成一个时间戳 长度为13位的 用于计算sign

2.sign的生成:

分析伪代码找到sign的生成方法为sub_126AC,该方法上面的伪代码都是一些拼接字符串的操作,就是把入参拼接起来。

> 接下来开始unidbg hook该方法

> 然后开启无尽的S 单步执行往下走当程序走到switch判断时 发现bl指令跳转到sub_10de4函数  多次在这个位置debugger 到 发现 到 sv =111 走的是 case 2   还遇到过sv = 110 走的是case 1 、这部分我们只分析为2的情况也就是走sub_10DE4,至于为啥呢?就是因为这后面走的加密算法看起来简单些 像md5 

> 查看下函数sub_10de4入参,hook该函数

> 打印mr1,为一串固定的字符串

> mr2 为0x1 固定值

> mr3 打印如下,为前面拼接的参数串 长度为0x106

> 接着分析sub_10de4 这个函数,点击进入sub_12ECC函数 ,这里为啥不分析sub_12FF0 呢 因为具体看了下没啥特别的。

hook sub_12ECC函数分析下入参

> 参数1:打印看看 ,看不太出来是什么 但是是64位  看后面的伪代码这个值主要是 跟 md5魔数有关系

> 参数2: 固定字符串 80306f4370b39fd5630ad0529f77adb6

> 参数3: 0x1

> 参数4: 为明文的参数拼接串

> functionId=personinfoBusiness&body={"callCJH":"1","callNPS":"1","closeJX":"0","headTaskRefresh":"1","locationArea":"0_0_0_0","menuStaticSource":"0","menuTimeStamp":"1631586010000"}&uuid=55a9c688729bb118&client=android&clientVersion=10.4.6&st=1648455172760&sv=111

> 参数5:为长度 0x106

#开始分析代码

>  从上面分析 v27 应该就是 md5 魔数 数组咯 大致为

>  v27[1]= 0x68449237;

>  v27[2]= 0x7FCC3DA5;

>  v27[3] = 0x88D90FBB;

>  v27[4] = 0x5AE99AEE;

>  继续往下分析

这个区域是具体的实现的位置,flag 是之前传入的0x1 所以走的是if 判断  。这个do-while 大致是 从v27中取具体魔数  s 往下走 v18 = 0x37 猜测应该是不断从v27中取值  还原算法时v27 就是这个下面这个数组

1

2

 {0x37, 0x92, 0x44, 0x68, 0xA5, 0x3D, 0xCC, 0x7F, 0xBB, 0x0F, 0xD9, 0x88, 0xEE, 0x9A,

0xE9, 0x5A};

> 继续S 往下走 0x38 就是传入固定字符串的第一个字符   80306f4370b39fd5630ad0529f77adb6

>  r0 = 0x66 是从拼接的字符串中取出的第一个 

>  r0=0x66  r1=0x0 r2=0x37 r3=0x1 r4=0x38    

>  R0 = R0 ^ R2  = 0x51 

>  R0 = R0 ^ R4  = 0x69

> 按照汇编 分析猜测 后面就是 将前面异或的结果 加上R2 0x37  然后在 R2 = R2 ^ R0     

1

*(v5 - 1) = md5_init_key_index ^ *(_BYTE *)(key + v17);// EOR.W R2, R2, R1    STRB.W R2, [R10,#-1]

> 根据ida 上伪代码 结和汇编 大致可知 这部分是每执行一次do while 就修改一次v5的索引对应的值  一直循环到循环到v5数据长度,所以结合伪代码 用C还原 sign 的算法 如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

 void encryption(char *data) {

     uint32_t v1, v2, v3 = 0;

     unsigned char init_key[60] = {0x37, 0x92, 0x44, 0x68, 0xA5, 0x3D, 0xCC, 0x7F, 0xBB,    0x0F, 0xD9, 0x88, 0xEE, 0x9A,

                                   0xE9, 0x5A};

     char *sign = "80306f4370b39fd5630ad0529f77adb6";

     int data_length = strlen(data);

     unsigned char md5_init_key = 0;

     unsigned char sign_key = 0;

     do {

         v1 = v3 & 0xF;

         v2 = v3 & 7;

         v3 = v3 + 1;

         sign_key = sign[v2];

         md5_init_key = init_key[v1];

         md5_init_key = md5_init_key ^ ((md5_init_key ^ *data ^sign_key) + md5_init_key);

         *data = md5_init_key ^ sign_key;

         data++;

     while (v3 != data_length);

 }

> 用python 来调用C代码  跟unidbg 黑盒调用结果比对如下:

> 总结: 某东的sign有三个算法 这边只还原了其中比较简单的 sv =111 的这种 。还原这部分算法也花的时间挺多的,结合unidbg 来分析算法 真的方便。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值