利用Frida 分析OLLVM字符串加密算法还原

主要通过一些辅助技术来还原加密算法,也能熟悉Frida在native 层面的利用

利用Frida 分析OLLVM字符串加密算法还原

最近在网上学习OLLVM字符串加密算法还原这个技术,这里简单记录下,以这方面的样本为例,来分析下

代码审计java层

public class HelloJni extends AppCompatActivity {
    TextView tv;

    public native String sign1(String str);

    public native String stringFromJNI();

    /* JADX INFO: Access modifiers changed from: protected */
    @Override // android.support.v7.app.AppCompatActivity, android.support.v4.app.FragmentActivity, android.support.v4.app.SupportActivity, android.app.Activity
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(com.example.hellojni_sign2.R.layout.activity_hello_jni);
        this.tv = (TextView) findViewById(com.example.hellojni_sign2.R.id.hello_textview);
        this.tv.setText(stringFromJNI());
        ((Button) findViewById(com.example.hellojni_sign2.R.id.button_sign1)).setOnClickListener(new View.OnClickListener() { // from class: com.example.hellojni.HelloJni.1
            @Override // android.view.View.OnClickListener
            public void onClick(View view) {
                HelloJni.this.tv.setText(HelloJni.this.sign1(RandomStringUtils.randomAscii(16)));
            }
        });
    }

    static {
        System.loadLibrary("hello-jni");
    }
}

其中HelloJni.this.tv.setText(HelloJni.this.sign1(RandomStringUtils.randomAscii(16)));为主要java层代码分析,发现这里调用了sign1函数,sign1在native层,按照常规操作,我们去看native层,如下(由于本人太懒,这里不截图了贴出关键代码):

第一步我们先看JNI_OnLoad,可能一开始不会像一下代码的样子,这里修复了一些环境变量:

jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
  jint v2; // w19
  JNIEnv *v4; // x20
  jclass v5; // x0
  JNIEnv *v6; // [xsp+8h] [xbp-48h] BYREF
  __int128 v7; // [xsp+10h] [xbp-40h] BYREF
  __int64 (__fastcall *v8)(); // [xsp+20h] [xbp-30h]
  __int64 v9; // [xsp+28h] [xbp-28h]

  v9 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  v6 = 0LL;
  v2 = 65540;
  if ( (*vm)->GetEnv(vm, (void **)&v6, 65540LL) )
    return -1;
  v4 = v6;
  v8 = sub_E76C;
  v7 = *(_OWORD *)&sign1;
  v5 = (*v6)->FindClass(v6, &xmmword_37050);
  if ( !v5
    || (((__int64 (__fastcall *)(JNIEnv *, jclass, __int128 *, __int64))(*v4)->RegisterNatives)(v4, v5, &v7, 1LL) & 0x80000000) != 0 )
  {
    return -1;
  }
  return v2;
}

其中|| (((**int64 (**fastcall *)(JNIEnv *, jclass, int128 *, int64))(*v4)->RegisterNatives)(v4, v5, &v7, 1LL) & 0x80000000) != 0 ) 这部分注册了一个native v7,起初v7并没有分析出就是sign1函数,因为ollvm字符串加密的缘故,这里贴出脚本,用来辅助分析ollvm字符串的加密

stringollvmen.js:

function print_hex(addr) {
    var base_hello_jni = Module.findBaseAddress("libhello-jni.so");
    console.log(hexdump(base_hello_jni.add(addr)));   //sign1  打印内存
}

其实直接打印对应的内存地址就可以了

第二部我们需要找出sign1的具体函数地址,由于ollvm字符串的加密原因加上代码多很难找到,这时候我们依靠frida辅助去找具体sign1的函数地址,通过以下脚本即可找到:

findSign1.js:

function hook_sign1() { //寻找registernative在库里的地址并查询sign1在so的函数地址

    var module_libart = Process.findModuleByName("libart.so");
    console.log(module_libart);
    var addr_RegisterNatives = null;
    var addr_GetStringUTFChars = null;
    var addr_NewStringUTF = null;
    //枚举模块的符号
    var symbols = module_libart.enumerateSymbols();
    for (var i = 0; i < symbols.length; i++) {
        var name = symbols[i].name;
        if (name.indexOf("CheckJNI") == -1 && name.indexOf("JNI") > 0) {
            if (name.indexOf("RegisterNatives") > 0) {
                console.log(name);
                addr_RegisterNatives = symbols[i].address;
            }

        }
    }
    console.log(addr_RegisterNatives)
    if (addr_RegisterNatives) {
        Interceptor.attach(addr_RegisterNatives, {
            onEnter: function (args) {
                var java_class = Java.vm.tryGetEnv().getClassName(args[1]);
                console.log(java_class);
                var methods = args[2];
                var method_count = parseInt(args[3]);
                console.log("addr_RegisterNatives java_class:", java_class, "method_count:", method_count);
                for (var i = 0; i < method_count; i++) {
                    console.log(methods.add(i * Process.pointerSize * 3).readPointer().readCString());
                    console.log(methods.add(i * Process.pointerSize * 3 + Process.pointerSize).readPointer().readCString());
                    var fnPtr = methods.add(i * Process.pointerSize * 3 + Process.pointerSize * 2).readPointer();
                    var module_so = Process.findModuleByAddress(fnPtr);
                    console.log(module_so);
                    console.log(module_so.name + "addr: " + fnPtr.sub(module_so.base));  //sign1函数地址

                }
            }, onLeave: function (retval) {

            }
        })
    }

}

通过以下命令运行:

frida -U -f xxxx(包名) -l findSign1.js

找到了具体的sign1的地址位置在0xE76C 处,我们分析下sign1的代码逻辑,分析在注释里,按照逆向逻辑从小当上分析:

jstring __fastcall sin1(JNIEnv *env, __int64 a2, void *a3)
{
  const char *v5; // x21
  unsigned __int64 v6; // x0
  __int64 v7; // x22
  char *v8; // x23
  unsigned __int64 v9; // x24
  char *v10; // x0
  unsigned __int64 v11; // x1
  jstring v12; // x19
  unsigned __int64 v14; // [xsp+8h] [xbp-98h] BYREF
  __int64 v15; // [xsp+10h] [xbp-90h]
  void *ptr; // [xsp+18h] [xbp-88h]
  char s[16]; // [xsp+20h] [xbp-80h] BYREF
  __int128 v18; // [xsp+30h] [xbp-70h] BYREF
  __int64 value; // [xsp+48h] [xbp-58h] BYREF
  __int64 v20; // [xsp+50h] [xbp-50h]
  __int64 v21; // [xsp+58h] [xbp-48h]

  v21 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  v5 = (*env)->GetStringUTFChars(env, a3, 0LL); //获取传入的值
  v15 = 0LL;
  ptr = 0LL;
  v14 = 0LL;
  v6 = strlen(v5);
  v7 = v6;
  if ( v6 >= 0x17 )
  {
    v9 = (v6 + 16) & 0xFFFFFFFFFFFFFFF0LL;
    v8 = (char *)operator new(v9);
    v14 = v9 | 1;
    v15 = v7;
    ptr = v8;
    goto LABEL_5;
  }
  v8 = (char *)&v14 + 1;
  LOBYTE(v14) = 2 * v6;
  if ( v6 )
LABEL_5:
    memcpy(v8, v5, v7);
  v8[v7] = 0;
  (*env)->ReleaseStringUTFChars(env, a3, v5);
  if ( (v14 & 1) != 0 )
    v10 = (char *)ptr;
  else
    v10 = (char *)&v14 + 1;
  if ( (v14 & 1) != 0 )
    v11 = (unsigned int)v15;
  else
    v11 = (unsigned __int64)(unsigned __int8)v14 >> 1;
  value = 0LL;
  v20 = 0LL;
  sub_103F0(v10, v11, &value);
  *(_OWORD *)s = 0u;
  v18 = 0u; 
  sprintf(s, &byte_37040, (unsigned __int8)value);   //这一串继续了复制打印  
  sprintf(&s[2], &byte_37040, BYTE1(value)); //这一串继续了复制打印
  sprintf(&s[4], &byte_37040, BYTE2(value));//这一串继续了复制打印
  sprintf(&s[6], &byte_37040, BYTE3(value));//这一串继续了复制打印
  sprintf(&s[8], &byte_37040, BYTE4(value));//这一串继续了复制打印
  sprintf((char *)((unsigned __int64)s | 0xA), &byte_37040, BYTE5(value));//这一串继续了复制打印
  sprintf((char *)((unsigned __int64)s | 0xC), &byte_37040, BYTE6(value));//这一串继续了复制打印
  sprintf((char *)((unsigned __int64)s | 0xE), &byte_37040, HIBYTE(value));//这一串继续了复制打印
  sprintf((char *)&v18, &byte_37040, (unsigned __int8)v20);//这一串继续了复制打印
  sprintf((char *)&v18 + 2, &byte_37040, BYTE1(v20));//这一串继续了复制打印
  sprintf((char *)&v18 + 4, &byte_37040, BYTE2(v20));//这一串继续了复制打印
  sprintf((char *)&v18 + 6, &byte_37040, BYTE3(v20));//这一串继续了复制打印
  sprintf((char *)&v18 + 8, &byte_37040, BYTE4(v20));//这一串继续了复制打印
  sprintf((char *)&v18 + 10, &byte_37040, BYTE5(v20));//这一串继续了复制打印
  sprintf((char *)&v18 + 12, &byte_37040, BYTE6(v20));//这一串继续了复制打印
  sprintf((char *)&v18 + 14, &byte_37040, HIBYTE(v20));//这一串继续了复制打印
  v12 = (*env)->NewStringUTF(env, s);   //进行了最后的结果
  if ( (v14 & 1) != 0 )
    operator delete(ptr);
  return v12;
}

这里方便分析我们利用frida hook打印出GetStringUTFChars和NewStringUTF的值,脚本如下:

1.js

function hook_libart() { //寻找registernative在库里的地址并查询sign1在so的函数地址

    var module_libart = Process.findModuleByName("libart.so");
    console.log(module_libart);
    var addr_RegisterNatives = null;
    var addr_GetStringUTFChars = null;
    var addr_NewStringUTF = null;
    //枚举模块的符号
    var symbols = module_libart.enumerateSymbols();
    for (var i = 0; i < symbols.length; i++) {
        var name = symbols[i].name;
        if (name.indexOf("CheckJNI") == -1 && name.indexOf("JNI") > 0) {
            if (name.indexOf("RegisterNatives") > 0) {
                console.log(name);
                addr_RegisterNatives = symbols[i].address;
            }
            else if (name.indexOf("GetStringUTFChars") > 0) {
                console.log(name);
                addr_GetStringUTFChars = symbols[i].address;
            } else if (name.indexOf("NewStringUTF") > 0) {
                console.log(name);
                addr_NewStringUTF = symbols[i].address;
            }

        }
    }
    console.log(addr_RegisterNatives)
    if (addr_RegisterNatives) {
        Interceptor.attach(addr_RegisterNatives, {
            onEnter: function (args) {
                var java_class = Java.vm.tryGetEnv().getClassName(args[1]);
                console.log(java_class);
                var methods = args[2];
                var method_count = parseInt(args[3]);
                console.log("addr_RegisterNatives java_class:", java_class, "method_count:", method_count);
                for (var i = 0; i < method_count; i++) {
                    console.log(methods.add(i * Process.pointerSize * 3).readPointer().readCString());
                    console.log(methods.add(i * Process.pointerSize * 3 + Process.pointerSize).readPointer().readCString());
                    var fnPtr = methods.add(i * Process.pointerSize * 3 + Process.pointerSize * 2).readPointer();
                    var module_so = Process.findModuleByAddress(fnPtr);
                    console.log(module_so);
                    console.log(module_so.name + "!" + fnPtr.sub(module_so.base));  //sign1函数地址

                }
            }, onLeave: function (retval) {

            }
        })
    }
    if (addr_GetStringUTFChars) {
        Interceptor.attach(addr_GetStringUTFChars, {
            onLeave : function(retval) {
                console.log("[GetStringUTFChars] : ", ptr(retval).readCString());
            }
        })
    }
    if (addr_NewStringUTF) {
        Interceptor.attach(addr_NewStringUTF, {
            onEnter : function(args) {
                console.log("[NewStringUTF] : ", ptr(args[1]).readCString());
            }
        })
    }
}

通过打印发现,我们可以看出GetStringUTFChars是获取的值(也就是java层传入的结果)即HelloJni.this.tv.setText(HelloJni.this.sign1(RandomStringUtils.randomAscii(16))); 这里是随机化的值,NewStringUTF值是通过一系列运算后的结果,也是主要我们分析算法的逻辑,这里我们先把java传入的值给它固定下来,hook下java层代码,脚本如下:

function hook_java(){
    Java.perform(function(){
        var hellojni=Java.use("com.example.hellojni.HelloJni");
        hellojni.sign1.implementation=function(args){
            // var result=args;
            // result="0123456789abcdef";
            // console.log("args: ",args);
            return this.args("0123456789abcdef");
        }
    })
}

然后再测试下运行上面的1.js脚本,发现GetStringUTFChars的值已经被固定了,接下来就通过最后的NewStringUTF结果来具体还原算法,继续回过头看native层代码

sprintf(s, &byte_37040, (unsigned __int8)value);
  sprintf(&s[2], &byte_37040, BYTE1(value));
  sprintf(&s[4], &byte_37040, BYTE2(value));
  sprintf(&s[6], &byte_37040, BYTE3(value));
  sprintf(&s[8], &byte_37040, BYTE4(value));
  sprintf((char *)((unsigned __int64)s | 0xA), &byte_37040, BYTE5(value));
  sprintf((char *)((unsigned __int64)s | 0xC), &byte_37040, BYTE6(value));
  sprintf((char *)((unsigned __int64)s | 0xE), &byte_37040, HIBYTE(value));
  sprintf((char *)&v18, &byte_37040, (unsigned __int8)v20);
  sprintf((char *)&v18 + 2, &byte_37040, BYTE1(v20));
  sprintf((char *)&v18 + 4, &byte_37040, BYTE2(v20));
  sprintf((char *)&v18 + 6, &byte_37040, BYTE3(v20));
  sprintf((char *)&v18 + 8, &byte_37040, BYTE4(v20));
  sprintf((char *)&v18 + 10, &byte_37040, BYTE5(v20));
  sprintf((char *)&v18 + 12, &byte_37040, BYTE6(v20));
  sprintf((char *)&v18 + 14, &byte_37040, HIBYTE(v20));

这里我们看下value的交叉引用按X键,看下谁在用它,通过查看发现 sub_103F0(v10, v11, &value);在调用,我们进去看下具体代码实现逻辑,在里面具体代码如下:

void __fastcall sub_1005C(char *a1, signed int a2, __int64 result_buufer)
{
  unsigned __int64 v6; // x0
  __int64 v7; // x22
  char *v8; // x23
  unsigned __int64 v9; // x24
  __int64 v10; // x8
  size_t v11; // w0
  size_t v12; // w0
  size_t v13; // w0
  unsigned __int64 v14; // x9
  bool v15; // w8
  unsigned __int64 i; // x24
  char *v17; // x8
  unsigned __int8 v18; // w9
  __int64 v19; // x8
  _BYTE *v20; // x0
  unsigned __int64 v21; // x9
  __int64 v22; // x24
  unsigned __int64 v23; // x8
  __int64 v24; // x22
  int j; // w19
  __int64 _result_bufefr; // [xsp+8h] [xbp-188h]
  __int64 v27[3]; // [xsp+10h] [xbp-180h] BYREF
  unsigned __int64 v28; // [xsp+28h] [xbp-168h] BYREF
  __int64 v29; // [xsp+30h] [xbp-160h]
  void *ptr; // [xsp+38h] [xbp-158h]
  __int128 v31; // [xsp+40h] [xbp-150h] BYREF
  __int128 v32; // [xsp+50h] [xbp-140h]
  __int128 v33; // [xsp+60h] [xbp-130h]
  __int128 v34[12]; // [xsp+70h] [xbp-120h] BYREF
  __int64 v35; // [xsp+130h] [xbp-60h]

  v35 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  v31 = 0uLL;
  v32 = xmmword_2B310;
  v33 = xmmword_2B320;
  v29 = 0LL;
  ptr = 0LL;
  v28 = 0LL;
  v6 = strlen((const char *)&qword_37110);
  v7 = v6;
  _result_bufefr = result_buufer;
  if ( v6 >= 0x17 )
  {
    v9 = (v6 + 16) & 0xFFFFFFFFFFFFFFF0LL;
    v8 = (char *)operator new(v9);
    v28 = v9 | 1;
    v29 = v7;
    ptr = v8;
    goto LABEL_5;
  }
  v8 = (char *)&v28 + 1;
  LOBYTE(v28) = 2 * v6;
  if ( v6 )
LABEL_5:
    memcpy(v8, &qword_37110, v7);
  v8[v7] = 0;
  v10 = lrand48() % 3;  //随机数,由于每次点击结果不一样可能使用了rand,也是我们主要hook的位置固定下即可
  switch ( v10 )
  {
    case 2LL:
      v13 = strlen(&byte_3712C);
      sub_103F4((int)&v28, &byte_3712C, v13);
      break;
    case 1LL:
      v12 = strlen(&byte_37124);
      sub_103F4((int)&v28, &byte_37124, v12);
      break;
    case 0LL:
      v11 = strlen(&byte_3711C);
      sub_103F4((int)&v28, &byte_3711C, v11);
      break;
  }
  v14 = v29;
  if ( (v28 & 1) == 0 )
    v14 = (unsigned __int64)(unsigned __int8)v28 >> 1;
  v15 = (v28 & 1) == 0;
  if ( v14 )
  {
    for ( i = 0LL; i < v21; ++i )
    {
      v27[1] = 0LL;
      v27[2] = 0LL;
      v27[0] = 2LL;
      if ( v15 )
        v17 = (char *)&v28 + 1;
      else
        v17 = (char *)ptr;
      v18 = v17[i];
      v19 = v31 & 0x3F;
      *(_WORD *)((char *)v27 + 1) = v18;
      *(_QWORD *)&v31 = (unsigned int)(v31 + 1);
      if ( !(_DWORD)v31 )
        ++*((_QWORD *)&v31 + 1);
      v20 = (char *)v34 + v19;
      if ( v19 && (unsigned int)(64 - v19) <= 1 )
      {
        memcpy(v20, (char *)v27 + 1, 64 - v19);
        sub_F008(&v31, v34);
      }
      else
      {
        *v20 = BYTE1(v27[0]);
      }
      v21 = v29;
      if ( (v28 & 1) == 0 )
        v21 = (unsigned __int64)(unsigned __int8)v28 >> 1;
      v15 = (v28 & 1) == 0;
    }
  }
  if ( a2 >= 1 )
  {
    v22 = v31 & 0x3F;
    v23 = (unsigned int)(v31 + a2);
    *(_QWORD *)&v31 = v23;
    if ( v23 < a2 )
      ++*((_QWORD *)&v31 + 1);
    if ( v22 )
    {
      v24 = (unsigned int)(64 - v22);
      if ( (int)v24 <= a2 )
      {
        memcpy((char *)v34 + v22, a1, 64 - v22);
        sub_F008(&v31, v34);
        v22 = 0LL;
        a1 += v24;
        a2 -= v24;
      }
    }
    if ( a2 >= 64 )
    {
      for ( j = a2; j > 63; j -= 64 )
      {
        sub_F008(&v31, a1);
        a1 += 64;
      }
      a2 &= 0x3Fu;
    }
    if ( a2 >= 1 )
      memcpy((char *)v34 + v22, a1, a2);
  }
  sub_FD90(&v31, _result_bufefr);
  memset(v34, 0, sizeof(v34));
  v32 = 0u;
  v33 = 0u;
  v31 = 0u;
  if ( (v28 & 1) != 0 )
    operator delete(ptr);
}

我们继续用交叉引用看下result_buufer的位置,按照逆向逻辑步骤来我们在最后面发现 sub_FD90(&v31, _result_bufefr);在使用这个变量,我们进去分析下

void *__fastcall sub_FD90(unsigned __int64 *a1, __int64 result_buufer)
{
  __int64 v2; // x10
  unsigned __int64 v4; // x8
  unsigned __int64 v5; // x9
  unsigned __int64 v7; // x26
  unsigned __int64 v8; // x12
  unsigned __int64 v9; // x25
  unsigned __int64 v10; // x9
  __int64 v11; // x22
  __int64 **v12; // x21
  unsigned __int64 v13; // x8
  unsigned __int64 v14; // x9
  __int64 v15; // x22
  char *v16; // x21
  size_t v17; // w25
  void *result; // x0
  char v19[4]; // [xsp+0h] [xbp-50h] BYREF
  __int16 v20; // [xsp+4h] [xbp-4Ch]
  char v21; // [xsp+6h] [xbp-4Ah]
  char v22; // [xsp+7h] [xbp-49h]
  __int64 v23; // [xsp+8h] [xbp-48h]

  v2 = 120LL;
  v23 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  v5 = *a1;
  v4 = a1[1];
  v7 = *a1 & 0x3F;
  v8 = (8 * v4) | (*a1 >> 29);
  v19[0] = 8 * *a1;
  if ( v7 < 0x38 )
    v2 = 56LL;
  v19[1] = v5 >> 5;
  v9 = v2 - v7;
  v19[2] = v5 >> 13;
  v21 = BYTE2(v8);
  v19[3] = v5 >> 21;
  v20 = v8;
  v22 = BYTE3(v8);
  if ( (int)v2 - (int)v7 < 1 )
    goto LABEL_11;
  v10 = (unsigned int)(v9 + v5);
  *a1 = v10;
  if ( v10 < v9 )
    a1[1] = v4 + 1;
  if ( !v7 )
  {
    v12 = &off_2B330;
    if ( (int)v9 >= 64 )
      goto LABEL_23;
LABEL_9:
    if ( (int)v9 < 1 )
      goto LABEL_11;
    goto LABEL_10;
  }
  v11 = (unsigned int)(64 - v7);
  v12 = &off_2B330;
  if ( (int)v11 > (int)v9 )
  {
    if ( (int)v9 < 64 )
      goto LABEL_9;
    goto LABEL_23;
  }
  memcpy((char *)a1 + v7 + 48, &off_2B330, 64 - v7);
  sub_F008(a1, a1 + 6);                         // 貌似md5 0xD76AA478
  v7 = 0LL;
  v12 = (__int64 **)((char *)&off_2B330 + v11);
  LODWORD(v9) = v9 - v11;
  if ( (int)v9 < 64 )
    goto LABEL_9;
LABEL_23:
  sub_F008(a1, v12);
  v12 += 8;
  LODWORD(v9) = v9 & 0x3F;
  if ( (int)v9 >= 1 )
LABEL_10:
    memcpy((char *)a1 + v7 + 48, v12, v9);
LABEL_11:
  v13 = *a1 & 0x3F;
  v14 = (unsigned int)*a1 + 8;
  *a1 = v14;
  if ( (unsigned int)v14 <= 7 )
    ++a1[1];
  if ( !v13 )
  {
    v16 = v19;
    v17 = 8;
    goto LABEL_18;
  }
  v15 = (unsigned int)(64 - v13);
  v16 = v19;
  v17 = 8;
  if ( (unsigned int)v15 > 8 )
  {
LABEL_18:
    result = memcpy((char *)a1 + v13 + 48, v16, v17);
    goto LABEL_19;
  }
  memcpy((char *)a1 + v13 + 48, v19, 64 - v13);
  result = (void *)sub_F008(a1, a1 + 6);
  v17 = 8 - v15;
  if ( 8 - (int)v15 >= 1 )
  {
    v13 = 0LL;
    v16 = &v19[v15];
    goto LABEL_18;
  }
LABEL_19:
  *(_WORD *)result_buufer = a1[2];
  *(_BYTE *)(result_buufer + 2) = BYTE2(a1[2]);
  *(_BYTE *)(result_buufer + 3) = BYTE3(a1[2]);
  *(_WORD *)(result_buufer + 4) = a1[3];
  *(_BYTE *)(result_buufer + 6) = BYTE2(a1[3]);
  *(_BYTE *)(result_buufer + 7) = BYTE3(a1[3]);
  *(_WORD *)(result_buufer + 8) = a1[4];
  *(_BYTE *)(result_buufer + 10) = BYTE2(a1[4]);
  *(_BYTE *)(result_buufer + 11) = BYTE3(a1[4]);
  *(_WORD *)(result_buufer + 12) = a1[5];
  *(_BYTE *)(result_buufer + 14) = BYTE2(a1[5]);
  *(_BYTE *)(result_buufer + 15) = BYTE3(a1[5]);
  return result;
}

这里看下最后结果是什么,脚本如下:

function hook_native() {  //sub_FD90(unsigned __int64 *a1, __int64 result_buufer)   
    var base_hello_jni = Module.findBaseAddress("libhello-jni.so");
    Interceptor.attach(base_hello_jni.add(0xFD90), {
        onEnter : function(args) {
            this.arg0 = args[0];    //hook sub_FD90函数的第一个参数
            this.arg1 = args[1];
        }, onLeave : function(retval) {  //将返回结果打印
            console.log("0xFD90:\r\n", hexdump(this.arg0), "\r\n", hexdump(this.arg1));
        }
    });
}

看到传入的参数值,这里再往上分析发现一个类似md5加密的函数sub_F008 貌似md5 0xD76AA478,那么我们去hook打印这个加密的参数值,由于前面的lrand48随机化的函数,。我们先把随机化的返回结果给固定了,这里hook脚本如下(由于lrand48是libc的函数,直接hooklibc的返回值去触发它即可):

function hook_libc(){
    var lrand48=Module.findExportByName("libc.so","lrand48");
    Interceptor.attach(lrand48,{
        onLeave : function(retval){
            console.log("lrand48_value: ",retval);
            retval.replace(0xAAAAAAAA);  //固定值替换
            console.log("defind_lrand48: ",retval);
        }
    })
}

下面我们就可以去hook打印sub_F008 (即加密函数值),脚本如下:

function hook_native() {  //sub_FD90(unsigned __int64 *a1, __int64 result_buufer)   
    var base_hello_jni = Module.findBaseAddress("libhello-jni.so");
    Interceptor.attach(base_hello_jni.add(0xFD90), {
        onEnter : function(args) {
            this.arg0 = args[0];    //hook sub_FD90函数的第一个参数
            this.arg1 = args[1];
        }, onLeave : function(retval) {  //将返回结果打印
            console.log("0xFD90:\r\n", hexdump(this.arg0), "\r\n", hexdump(this.arg1));
        }
    });

    Interceptor.attach(base_hello_jni.add(0xF008), {
        onEnter : function(args) {
            this.arg0 = args[0];    
            this.arg1 = args[1];
        }, onLeave : function(retval) {
            console.log("0xF008:\r\n", hexdump(this.arg0), "\r\n", hexdump(this.arg1));
            console.log("0xF008:", ptr(this.arg1).readCString());
        }
    });
}

我们发现打印了一串名文字符串"+++++++++salt2+0123456789abcdef",把这个字符串拿去md5加密,发现和我们的app应用程序结果是一样的,至此还原算法结束,此app应用程序的整题逻辑如下:

.datadiv_decode OLLVM混淆的字符串的解密函数=======》md5("+++++++++salt2+" + "0123456789abcdef") = 5a6ecb4b69e035e521bf582135281509

结语:通过frida可以帮助我们分析ollvm 字符串加密算法的还原,也熟悉了frida在so层更深的利用技巧

  • 发表于 2024-06-18 09:37:05
  • 阅读 ( 38002 )
  • 分类:漏洞分析

0 条评论

请先 登录 后评论
Azd
Azd

4 篇文章

站长统计