分析一道简单安卓中级题
2022-4-28 17:58:40 Author: mp.weixin.qq.com(查看原文) 阅读量:7 收藏


本文为看雪论坛精华文章
看雪论坛作者ID:白云精灵

安装app后打开,点击验证提示我们flag格式错误,请重试。
我们打开jeb分析工具进行分析。
 
红色图部分为源码目录,也就是dex文件反编译后的源码。
 
R文件存储了资源相关的id,比如图片资源,按钮,文字等信息都存储在这个R文件里。

在需要使用时就用R.xxx.xxx调用即可。例如R.anim.abc_fade_in,因为都是static修饰的,
所以可以直接打点直接调用,不需要创建对象。
 
 
如何知道当前类是什么呢?
执行如下命令即可获取,方便进行分析。
adb shell dumpsys activity top
 
因为手机没有root,所以这里以虚拟机演示。然后虚拟机又不支持26版本的sdkapp,所以只好以mt管理器为例子。
 
bin.mt.plus为包
.Main为activity,也就是类
bin.mt.plus/.Main
因为这里类就怎么一个MainActivity所以我直接分析了。
如下MainActivity为关键类,里面有点击按钮后弹出的相关信息。
源码刨析
package cn.pojie52.cm01;//当前类MainActivity所在的包,也就是文件夹 import android.os.Bundle;//导入Bundle相关方法,变量import android.view.View.OnClickListener;//导入按钮点击事件相关方法,变量import android.view.View;//导入视图相关方法,变量。import android.widget.EditText;//导入编辑框相关方法,变量import android.widget.Toast;//导入吐司弹窗相关方法,变量import androidx.appcompat.app.AppCompatActivity;//主类,一个activity必须要继承一个activity父类  //加载so文件的方法。//static{}为静态代码块,在MainActivity创建时会优先执行里面的方法//调用System对象里面的LoadLibrary方法,传入so文件的名字,去除lib,.so,后的名字//在调用LoadLibrary方法后,底层会把lib,so进行拼接,然后去lib目录里寻找相关的so进行加载static {        System.loadLibrary("native-lib");} //如下是一个native方法,返回值为Boolean类型,也就是true和false,//方法名为check,参数为String类型//因为是native方法,所以实现逻辑必定在so里面,而so里面的代码是由c或者c++编译的//所以要执行这个System.loadLibrary("native-lib");代码,把so文件提前加载到内存中public native boolean check(String arg1) {} @Override  // androidx.appcompat.app.AppCompatActivity//@Override说明这是一个重写方法,//这个方法OnCreate在AppCompatActivity类里//因为主类MainActivity后面加了extends AppCompatActivity//说明AppCompatActivity里的方法,变量都会继承过来,也就是说我们可以直接使用//参数为Bundle    protected void onCreate(Bundle arg3) {        super.onCreate(arg3);//调用父类的OnCreate方法放入arg3参数//父类也就是AppCompatActivity        this.setContentView(0x7F0A001C);  // layout:activity_main//调用setContentView方法初始化界面//也就是我们在开始的时候看到的那个界面,//有个编辑框,一个验证按钮        EditText v3 = (EditText)this.findViewById(0x7F070058);  // id:flag//调用findViewById方法传入编辑框的id,然后转为EditText,//因为获取到的是一个view对象,这个view对象范围太大了//我们直接转为EditText方便些        this.findViewById(0x7F070045).setOnClickListener(new View.OnClickListener() {  // id:check//这个是一种链式编程写法,简便,省去了定义变量来接收他//这个是调用findViewById传入按钮的id,//this指的是当前activity,因为继承了AppCompatActivity类//所以我们可以直接使用里面的findViewById方法//调用这个方法后返回的是一个对象,然后调用setOnclickListener方法//传入匿名内部类对象,给按钮绑定一个监听事件//onClick方法是重写的            @Override  // android.view.View$OnClickListener            public void onClick(View arg4) {             }        });} 在Onclick方法里的代码逻辑         String v4 = v3.getText().toString().trim();//v3为编辑框,获取编辑框里面的信息,转为string,去除空格,//赋值为string变量v4                if(v4.length() != 30) {//判断v4的长度是否等于30//如果不等于就提示flag格式错误,请重试//return为返回                    Toast.makeText(MainActivity.this, "flag格式错误,请重试", 0).show();//调用Toast对象里面的makeText方法,放入MainActiviy//因为onclick方法在内部类里面,所以是MainActivity.this,而不是this.MainActiviy                    return;                }                 if(MainActivity.this.check(v4)) {//调用native层的check方法,传入v4字符串                    Toast.makeText(MainActivity.this, "恭喜你,验证正确!", 0).show();//如果check方法的返回值为true那么弹出恭喜你,验证正确的提示                    return;                }                 Toast.makeText(MainActivity.this, "flag错误,再接再厉", 0).show();//否则提示flag错误,再接再厉            }

我们已经知道了具体逻辑是在so层,且密码长度是30位的。
接下来我们进入so层进行分析,我们需要用到的工具是ida。
在拖入ida前,我们需要把apk包进行解压,获取lib目录里面的so文件。

 
拖入64位的ida。
我们默认即可,点击ok。
 
 
因为这个check方法不是系统方法,所以在export就可以看到。
 
Java_cn_pojie52_cm01_MainActivitycheck
在ida中显示为Java类名_方法名。
 
 
我们双击进入。
 
 
按tab键转为c伪代码。
 
 
我们改一下参数,第一个参数固定为JNIEnv*。
 
在改之前,我们需要导入jni的头文件,Jni头文件里存放了相关的jni方法,方便分析。
 
 
 
 
第二个参数要么是jclass,要么是jobject。
如果是类就是jclass,如果是对象就是jobject。
 
可以看到这个check方法并没有static修饰,也就是说要用对象才能调用,也就是说是jobject。
 
 
我们对着int64的参数右键,把他修改成jobject。
 
 
这个check方法的参数是一个string类型的,所以这个native方法第三个参数也是string类型的,在c语言中string被定义为jstring。
 
 
查找jni头文件可以知道。这个jstring是一个jstring,而jstring是jobject,所以这个jstring是jobject类型。
Typedef是给一个变量进行重定义。*这个代表这是一个一级指针,多少个星花代表几级指针。

指针是一个地址,地址里面有一块空间,用来存放变量的数据。
比如int a=0;这个a是一个地址,给这个地址取名为a,地址里面存放了0这个数据。当我们&a时就是获取a的地址,相当于把a还原成一个地址,我们可以用Printf(“%x”,&a);来打印这个地址。
我们修改一下第三个参数。
 
 
 
如下是改完后的完整代码:
__int64 __fastcall Java_cn_pojie52_cm01_MainActivity_check(_JNIEnv *a1, jobject a2, jstring a3){  const char *v5; // x21  size_t v6; // w0  int v7; // w0  __int64 v8; // x0  _BYTE *v9; // x0  int8x16_t v10; // q0  int8x16_t v11; // q4  int8x16_t v12; // q2  int8x16_t v13; // q5  int8x16_t v14; // q1  int8x16_t v15; // q0  __int64 v16; // x8  unsigned int v17; // w19  _BYTE v19[33]; // [xsp+0h] [xbp-A0h]  int v20; // [xsp+21h] [xbp-7Fh]  char v21; // [xsp+25h] [xbp-7Bh]  char v22; // [xsp+26h] [xbp-7Ah]  char v23; // [xsp+27h] [xbp-79h]  char v24; // [xsp+28h] [xbp-78h]  char dest[16]; // [xsp+38h] [xbp-68h] BYREF  __int128 v26; // [xsp+48h] [xbp-58h]  __int128 v27; // [xsp+58h] [xbp-48h]  __int128 v28; // [xsp+68h] [xbp-38h]  __int64 v29; // [xsp+78h] [xbp-28h]   v29 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);  if ( a1->functions->GetStringUTFLength((JNIEnv *)a1, a3) == 30 )  {    v5 = a1->functions->GetStringUTFChars(a1, a3, 0LL);    v28 = 0u;    v27 = 0u;    v26 = 0u;    *(_OWORD *)dest = 0u;    v6 = strlen(v5);    strncpy(dest, v5, v6);    a1->functions->ReleaseStringUTFChars((JNIEnv *)a1, a3, v5);    v7 = strlen(dest);    sub_B90((int)dest, v7, "areyousure??????");    v8 = strlen(dest);    v9 = (_BYTE *)sub_D90(dest, v8);    *(_OWORD *)v19 = unk_11A1;    *(_OWORD *)&v19[16] = unk_11B1;    *(_QWORD *)&v19[25] = unk_11BA;    v10.n128_u64[0] = 0xB2B2B2B2B2B2B2B2LL;    v10.n128_u64[1] = 0xB2B2B2B2B2B2B2B2LL;    v11.n128_u64[0] = 0xFEFEFEFEFEFEFEFELL;    v11.n128_u64[1] = 0xFEFEFEFEFEFEFEFELL;    v19[0] = 53;    v12 = veorq_s8(            vaddq_s8(veorq_s8(vaddq_s8(*(int8x16_t *)&v19[1], v10), (int8x16_t)xmmword_1130), (int8x16_t)xmmword_1140),            v11);    v13.n128_u64[0] = 0x101010101010101LL;    v13.n128_u64[1] = 0x101010101010101LL;    v14.n128_u64[0] = 0x3E3E3E3E3E3E3E3ELL;    v14.n128_u64[1] = 0x3E3E3E3E3E3E3E3ELL;    *(int8x16_t *)&v19[1] = vaddq_s8(                              veorq_s8(                                vsubq_s8(v13, vorrq_s8(vshrq_n_u8(v12, 7uLL), vshlq_n_s8(v12, 1uLL))),                                (int8x16_t)xmmword_1150),                              v14);    v20 = 1782990162;    v15 = veorq_s8(            vaddq_s8(veorq_s8(vaddq_s8(*(int8x16_t *)&v19[17], v10), (int8x16_t)xmmword_1160), (int8x16_t)xmmword_1170),            v11);    v21 = ((1          - ((2 * ((((unk_11C6 - 78) ^ 0xB2) - 117) ^ 0xFE)) | ((((unsigned __int8)(((unk_11C6 - 78) ^ 0xB2) - 117) ^ 0xFE) & 0x80) != 0))) ^ 0x25)        + 62;    v16 = 0LL;    v22 = ((1          - ((2 * ((((unk_11C7 - 78) ^ 0xB1) - 118) ^ 0xFE)) | ((((unsigned __int8)(((unk_11C7 - 78) ^ 0xB1) - 118) ^ 0xFE) & 0x80) != 0))) ^ 0x26)        + 62;    *(int8x16_t *)&v19[17] = vaddq_s8(                               veorq_s8(                                 vsubq_s8(v13, vorrq_s8(vshrq_n_u8(v15, 7uLL), vshlq_n_s8(v15, 1uLL))),                                 (int8x16_t)xmmword_1180),                               v14);    v23 = ((1          - ((2 * ((((unk_11C8 - 78) ^ 0xB0) - 119) ^ 0xFE)) | ((((unsigned __int8)(((unk_11C8 - 78) ^ 0xB0) - 119) ^ 0xFE) & 0x80) != 0))) ^ 0x27)        + 62;    v24 = ((1          - ((2 * ((((unk_11C9 - 78) ^ 0xBF) - 120) ^ 0xFE)) | ((((unsigned __int8)(((unk_11C9 - 78) ^ 0xBF) - 120) ^ 0xFE) & 0x80) != 0))) ^ 0x28)        + 62;    while ( v9[v16] == v19[v16] )    {      if ( v9[v16] )      {        if ( ++v16 != 41 )          continue;      }      v17 = 1;      goto LABEL_9;    }    v17 = 0;LABEL_9:    free(v9);  }  else  {    return 0;  }  return v17;}

为了方便分析,我们把参数的名字改一下。
 
 
 
重复前面的操作。
//判断password的长度是否为30if ( env->functions->GetStringUTFLength((JNIEnv *)env, password) == 30 )//这个env是一个指针,指针里面有一个函数指针,函数指针里面有一个GetStringUTFLength函数//这个函数用于判断字符串的长度//这个涉及到了结构体相关知识//把password转为c语言中的char类型env->functions->GetStringUTFChars(env, password, 0LL);

为了方便,我提前改好了相关名字。
 
password_length = strlen(password_);//获取password_的长度//把password_拷贝到password__里面,拷贝长度为password_lengthstrncpy(password__, password_, password_length);  //释放password_指向的字符串空间//也就是归还空间,让其他程序使用env->functions->ReleaseStringUTFChars((JNIEnv *)env, password, password_);//获取password__的长度password__length = strlen(password__);

细心的朋友可以知道这个16是有问题的,因为我们的password是30位的,然而这个password确是16位的。
char password__[16]

这里我改为了32。
 
我们看一下下面的这个函数,可以看到传入了我们password__,password_length,密码与长度,还有"areyousure??????"。
 
 
我们双击进入这个函数,然后修改一下如下参数。
 
//获取threeStr的长度threeStrLen = strlen(threeStr);do  {v9 = *((unsigned __int8 *)v20 + v7); //v7加上转换为无符号int类型的指针v20,也就是加上无符号int类型的步长。然后取*取出相加后里面空间的值v10 = v8 + v9 + (unsigned __int8)threeStr[v7 % threeStrLen];//v9加上v8然后加上无符号整型的threeStr[v7%threeStrlen];//对v7以threeStrlen进行取余,然后threeStr[取余的值]取出threeStr里面的字符串数据    v11 = v10 + 255;//v10加上255赋值给v11    if ( v10 >= 0 )//如果v10大于等于0就执行v10赋值给v11的操作      v11 = v10;把v10赋值给v11    v8 = v10 - (v11 & 0xFFFFFF00);//对v11与0XFFFFFF00进行与运算,然后v10减去运算结果,赋值给v8*((_BYTE *)v20 + v7++) = *((_BYTE *)v20 + v8);//把v20转为Byte*类型然后加上v8,步长为Byte *的长度,然后*取值//然后把值赋值给把v20转为Byte*类型加上自增的v7,获得值后进行取*,赋值给这个取*花后的值*((_BYTE *)v20 + v8) = v9;//把v9赋值给把v20转为Byte*类型然后加上v8个Byte*步长里面的值  }  while ( v7 != 256 );如果v7不等于256就一直循环 //判断如果password__length是否为空//在c语言中,非0就是true,也就是说可以用于判断是否为空if ( password__length )  {    v12 = 0;//给v12赋值0    v13 = 0;给v13赋值0v14 = password__length;//把密码长度赋值给v14    do    {      v15 = v12 + 1; //把v12加上1的值赋值给v15      if ( v12 + 1 >= 0 )//判断v12+1是否大于等于0        v16 = v12 + 1;//把v12加1的值赋值给v16      else        v16 = v12 + 256;//把v12加上256赋值给v16      v12 = v15 - (v16 & 0xFFFFFF00);//对v16与0XFFFFFF00进行与运算,然后v15减去运算结果,赋值给v12      v17 = *((unsigned __int8 *)v20 + v12);//把v20转为无符号int整型指针然后加上这个指针的步长,//对这个值进行取星花赋值给v17      v18 = v13 + v17;//把v13加上v17的值赋值给v18      v19 = v18 + 255;//把v18加上255的值赋值给v19      if ( v18 >= 0 )//判断v18是否大于0        v19 = v18;//如果大于0就把v18赋值给v19      v13 = v18 - (v19 & 0xFFFFFF00);//对v19与0XFFFFFF00进行与运算,然后v18减去运算结果,赋值给v13      --v14;//v14减一      *((_BYTE *)v20 + v12) = *((_BYTE *)v20 + v13);//把v20转为Byte*类型加上v13个Byte*的步长,然后取*取出里面的值,//把这个值赋值给v20转为Byte*类型加上v12个Byte*步长的地址空间       *((_BYTE *)v20 + v13) = v17;//把v17的值赋值给v20转为Byte*类型加上v13个Byte*步长的地址空间       *password__++ ^= *((_BYTE *)v20 + (unsigned __int8)(*((_BYTE *)v20 + v12) + v17));//取出password里面的值//把v20转为Byte*类型,加上把V20转为Byte*类型后加上v12进行取*加上v17后转为无符号int类型//然后进行取*取出里面的值,把值与password进行异或然后赋值给password    }    while ( v14 );//如果v14不为空就一直循环  }

这两块代码分别操作着threeStr,还有password。
 
 
如下代码是关键:
while ( v9[v16] == v19[v16] ) //循环对比v9[v16]的值与v19[v16]的值    {      if ( v9[v16] ) //判断v9[16]里是否有值      {        if ( ++v16 != 41 )//如果v16加一后不等于41就continue跳过代码          continue;      }      v17 = 1;//如果为1那么flag正确      goto LABEL_9;//跳到LABEL_9的位置    }    v17 = 0;//如果为0那么flag错误LABEL_9:    free(v9);//释放内存  }  else  {    return 0;  }return v17;//影响验证结果

下面hook一下这个函数。看看这个password变成了什么。
import frida, sys//因为是frida hook,所以要导入frida模块,//读取系统输入需要sys模块 jscode = ''' function inline_hook() { var so_addr = Module.findBaseAddress("libnative-lib.so");     if (so_addr) {        console.log("so_addr:", so_addr);        var addr_b90 = so_addr.add(0xb90);        var sub_b90 = new NativeFunction(addr_b90 , 'int', ['pointer', 'int','pointer']);        var arg1 = Memory.allocUtf8String('111111111111111111111111111111');        var arg2 = 30;        var arg3 = Memory.allocUtf8String('areyousure??????');        var ret_b90 = sub_b90(arg1,arg2,arg3);        console.log(Memory.readByteArray(arg1,64));          var addr_d90 = so_addr.add(0xd90);        var sub_d90 = new NativeFunction(addr_d90 , 'pointer', ['pointer', 'int' ]);        var arg1 = Memory.allocUtf8String('111111111111111111111111111111');        var arg2 = 30;        var ret_d90 = sub_d90(arg1,arg2);        console.log(Memory.readByteArray(ret_d90,64));      } }setImmediate(inline_hook) '''def on_message(message, data):    if message['type'] == 'send':        print(" {0}".format(message['payload']))    else:        print(message)pass#print(frida.enumerate_devices())# 查找USB设备并附加到目标进程device =  frida.get_remote_device()#pid = device.spawn(["com.live.xctv"]) #session = device.attach(pid)session =device.attach('cn.pojie52.cm01') #这里是要注入的apk包名# 在目标进程里创建脚本script = session.create_script(jscode)# 注册消息回调script.on('message', on_message)print(' Start attach')# 加载创建好的javascript脚本script.load()# 读取系统输入sys.stdin.read()  var so_addr = Module.findBaseAddress("libnative-lib.so");  //查找so的基址。

在ida中基址为0000000000000,在动态调试时基址会变,所以我们需要通过基址加上函数的偏移来定位一个函数。
在这里我们需要hook的函数是sub_B90()。
 
 
我们按tab键查看函数偏移。
 
console.log("so_addr:", so_addr);//打印so的基址 var addr_b90 = so_addr.add(0xb90);//so的基址加上b90就是这个函数的地址 if (so_addr) //判断基础是否获取到了  var sub_b90 = new NativeFunction(addr_b90 , 'int', ['pointer', 'int','pointer']);//创建一个本地函数,类似于指针函数,给指针函数赋值函数的地址。//然后进行调用,参数为_BYTE *password__, unsigned int password__length, char *threeStr//返回值为unsigned __int64//返回值对应'int',参数对应着['pointer', 'int','pointer']下面为封装函数的参数,进行调用//分配内存创建111111111111111111111111111111字符串var arg1 = Memory.allocUtf8String('111111111111111111111111111111');var arg2 = 30;//长度//分配内存创建areyousure??????字符串var arg3 = Memory.allocUtf8String('areyousure??????');//调用sub_b90函数,传入arg1,arg2,arg3var ret_b90 = sub_b90(arg1,arg2,arg3);//返回值为ret_b90Memory.readByteArray(arg1,64)//读取arg1内存中数据,也就是password,读64位byte的数据console.log(Memory.readByteArray(arg1,64));//打印读取出来的password

这里还有一个函数传入了password还有密码的长度。
 
var addr_d90 = so_addr.add(0xd90);//基址加上0xb90的偏移就是sub_d90函数的地址 var sub_d90 = new NativeFunction(addr_d90 , 'pointer', ['pointer', 'int' ]);//创建一个本地函数,类似于指针函数,给指针函数赋值函数的地址。//然后进行调用,参数为char *a1, __int64 a2//返回值为void * 万能指针,任何指针都可以赋值//返回值对应'pointer',参数对应着['pointer', 'int'] //分配内存创建111111111111111111111111111111字符串var arg1 = Memory.allocUtf8String('111111111111111111111111111111');//密码长度var arg2 = 30;//调用sub_d90()函数,输入arg1,arg2参数var ret_d90 = sub_d90(arg1,arg2);//返回值赋值给ret_d90读取sub_d90返回值地址里面的空间的64byte的数据console.log(Memory.readByteArray(ret_d90,64));

以下是dump的数据。
 
我们输入的密码是111111111111111111111111111111。当执行完异或操作后变为了下面的数据。
30个1对应着16进制为30个0x31。在ASCII码中,49的ASCII码为‘1’,这个49是10进制的,转为16进制就是0x31。
 
e0,6b,37,a1,75,d7,f6,d4,ef,19,c6,c3,57,a0,f9,b4
73,ee,c8,d1,b3,30,1a,0a,09,52,06,8c,1f,7c
 
在计算机中,异或运算是可以进行解密的。
10 xor 5 =15
 
15 xor 10 = 5
 
把10当成我们输入的密码,把5当成要异或的值,把15当成异或后的值
我们知道了异或后的值,当我们再次异或我们的密码时,就可以得到要异或的值。
 
e0,6b,37,a1,75,d7,f6,d4,ef,19,c6,c3,57,a0,f9,b4
73,ee,c8,d1,b3,30,1a,0a,09,52,06,8c,1f,7c
 
与30个0x31进行异或即可得到密码。
public static  void Xor(){     int xorData[]={0xe0,0x6b,0x37,0xa1,0x75,0xd7,0xf6,0xd4,0xef,0x19,0xc6,0xc3,0x57,0xa0,0xf9,0xb4,    0x73,0xee,0xc8,0xd1,0xb3,0x30,0x1a,0x0a,0x09,0x52,0x06,0x8c,0x1f,0x7c};    int xorDataMy[]={0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,            0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31};    System.out.print("[");    for (int i = 0; i < xorData.length; i++) {         System.out.print(xorData[i]^xorDataMy[i]);        if(i<xorData.length-1){        System.out.print(",");        }     }    System.out.print("]"); }
 
得到结果如下:
[209,90,6,144,68,230,199,229,222,40,247,242,102,145,200,133,66,223,249,224,130,1,43,59,56,99,55,189,46,77]
 
如下是sub_d90函数的返回进行地址空间dump的数据,可以看到全是字母。
 
我们可以尝试推断一下是不是某种加密算法,比如aes,base64,md5加密。

aes加密是需要秘钥的,有时还需要iv偏移,base64加密不需要秘钥,也不需要iv偏移。

Md5加密的话,要么全是大写,全是小写,aes加密后缀有个等于,很复杂。
而这个sub_d90函数返回值不可能的aes加密,因为sub_d90中并没有任何的秘钥,iv偏移相关。

Md5也不太可能,因为md5要么全是大写字母,或者全是小写字母,并且不规则。
public static void baseEncode(){    System.out.println();    byte xorDataMy[]={0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,            0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31};    String by=Base64.getEncoder().encodeToString( xorDataMy);     System.out.println(by); }public static void md5(){    System.out.println(MD5Utils.stringToMD5("111111111111111111111111111111"));}
可以看到base64加密的值与sub_d90的值一样。
 
 
我们回到这里。V9[v16]这个是返回的base64编码的数据,V19[16]是真码,也是base64的。只要我们把这个真码的base64得到,然后通过解密base64,然后把解密后的数据与前面的key进行异或即可获得flag。
while ( v9[v16] == v19[v16] )    {      if ( v9[v16] )      {        if ( ++v16 != 41 )          continue;      }      v17 = 1;      goto LABEL_9;    }    v17 = 0;LABEL_9:    free(v9);  }  else  {    return 0;  }  return v17;如何获取v19呢?如下是v19的定义,地址为xsp+0_BYTE v19[33]; // [xsp+0h] [xbp-A0h] while ( v9[v16] == v19[v16] )

我们在v9[v16]这个位置按一下tab键即可看到偏移。
这块代码的偏移的是b2c。
 
我们需要在v19[v16]初始化后才能进行hook拦截,也就是获取数据
最好的时机是在sub_b90执行后进行截取。我们可以看到第一个参数是xsp+38h,也就是说xsp+0是xsp+38-38即可。
我们需要拦截第一个参数获取到地址,然后减去38就能获取到xsp也就是v19。
 
 
下面进行分析frida hook代码。
import frida, sys jscode = ''' var destAddr = '';  //定位xsp地址 function inline_hook() {    var so_addr = Module.findBaseAddress("libnative-lib.so");//寻找基址 //在ida中基址为0000000000000//在动态调试时基址会变//所以我们需要通过基址加上函数的偏移来定位一个函数      if (so_addr) {//是否获取到基址,如果没有获取到里面的代码就不会执行        console.log("so_addr:", so_addr);//打印so的地址         var addr_b90 = so_addr.add(0xB90);//获取sub_b90的函数地址        var sub_b90 = new NativeFunction(addr_b90 , 'int', ['pointer', 'int', 'pointer']);//创建一个本地函数,类似于指针函数,给指针函数赋值函数的地址。//然后进行调用,参数为_BYTE *password__, unsigned int password__length, char *threeStr//返回值为unsigned __int64//返回值对应'int',参数对应着['pointer', 'int','pointer'] //下面是一个拦截器,用于拦截函数的sub_b90的进入,与结束        Interceptor.attach(sub_b90, {            onEnter: function(args) //进入函数时执行的代码            {             destAddr = args[0];//把第一个参数赋值给destAddr            console.log('onEnter B90'); //打印'onEnter B90,用于确认函数是否进入             },            //在进入函数之后执行的语句           onLeave:function(retval)            {//retval为函数的返回值            console.log('onLeave B90');//打印onLeave B90,用于确认函数是否离开            }        });          var addr_b2c = so_addr.add(0xb2c);//0xb2c为偏移,也就是说拦截 b2c的地址         console.log("The addr_b2c:", addr_b2c);//打印addr_b2c的地址        Java.perform(function() {            Interceptor.attach(addr_b2c, {//拦截b2c地址                onEnter: function(args) { //arg为参数                console.log("addr_b2c OnEnter :",  Memory.readByteArray(destAddr.sub(0x38),64) ); // Memory.readByteArray(destAddr.sub(0x38),64)//读取xsp的内容,也就v19的值//也就是base64编码的真码     }            })        })    }}setImmediate(inline_hook)  ''' //用于接收消息的函数//第一个参数为需要打印的信息def on_message(message, data):    if message['type'] == 'send':        print(" {0}".format(message['payload']))    else:        print(message)Pass//跳过#print(frida.enumerate_devices())# 查找USB设备并附加到目标进程device =  frida.get_remote_device()//调用frida的函数,获取设备#pid = device.spawn(["com.live.xctv"]) #session = device.attach(pid)//session =device.attach('cn.pojie52.cm01') #这里是要注入的apk包名//这个包名可以通过frida-ps -U 来进行获取# 在目标进程里创建脚本script = session.create_script(jscode)# 注册消息回调script.on('message', on_message)print(' Start attach')# 加载创建好的javascript脚本script.load()# 读取系统输入sys.stdin.read()
下面的数据是左边的十六进制。
{0x35, 0x47, 0x68, 0x32, 0x2f, 0x79, 0x36, 0x50, 0x6f, 0x71, 0x32, 0x2f, 0x57, 0x49, 0x65, 0x4c, 0x4a, 0x66,0x6d, 0x68, 0x36, 0x79, 0x65, 0x73, 0x6e, 0x4b, 0x37, 0x6e, 0x64, 0x4b, 0x37, 0x6e, 0x64, 0x6e, 0x4a, 0x65, 0x57, 0x52, 0x45, 0x46, 0x6a, 0x52, 0x78, 0x38}
 
下面的数据是十六进制对应的ASCII码。
 
5Gh2/y6Poq2/WIeLJfmh6yesnK7ndnJeWREFjRx8
import base64//导入base64的包,因为我们要用到base64的解密函数 //xorkey是我们前面通过异或解密得出的数据xorkey = [209, 90, 6, 144, 68, 230, 199, 229, 222, 40, 247, 242, 102, 145, 200, 133, 66, 223, 249, 224, 130, 1, 43, 59,          56, 99, 55, 189, 46, 77] //第一个参数为base64解密后的数据//第二个参数为base64解密后的数据的长度def sub_B90(data, l):    ret = []    for i in range(l):        ret.append(data[i] ^ xorkey[i])//异或解密,把结果拼接到ret里面    s = ''    for i in ret://便利ret里面数据        s += chr(i) //把ret的解密结果转换成字符,如何进行拼接。    print(s)//打印拼接后的解密数据    return ret//返回ret  def resv(data):    data = base64.b64decode(data,)//解密base64数据     t = sub_B90(data, len(data))//把解密后的base64数据传入sub_b90进行异或解密    return (t) data="5Gh2/y6Poq2/WIeLJfmh6yesnK7ndnJeWREFjRx8"//base64数据 resv(data)//调用这个函数进行解密base64数据与异或解密获得真吗

 
输入真码后,提示我们恭喜你,验证正确。

看雪ID:白云精灵

https://bbs.pediy.com/user-home-814281.htm

*本文由看雪论坛 白云精灵 原创,转载请注明来自看雪社区

# 往期推荐

1.由浅入深理解Kerberos协议

2.虎符网络安全赛道 2022-pwn-vdq-WP解题分析

3.为IDA架设私人lumen服务器

4.分析一个安卓简单CrackMe

5.Docker-remoter-api渗透

6.Writeup-ROP Emporium fluff

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458439798&idx=1&sn=7514ca7327b0a881c5042a91fe2af960&chksm=b18fe0fc86f869ea1b612e0c0df0b45cac890a6c7bded69de1c6b197b8d9254db4988cbf244b#rd
如有侵权请联系:admin#unsafe.sh