安装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为关键类,里面有点击按钮后弹出的相关信息。

源码刨析
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 | 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);来打印这个地址

我们修改一下第三个参数


如下是改完后的完整代码
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 | __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;
}
|
为了方便分析,我们把参数的名字改一下


重复前面的操作

1 2 3 4 5 6 7 | / / 判断password的长度是否为 30
if ( env - >functions - >GetStringUTFLength((JNIEnv * )env, password) = = 30 )
/ / 这个env是一个指针,指针里面有一个函数指针,函数指针里面有一个GetStringUTFLength函数
/ / 这个函数用于判断字符串的长度
/ / 这个涉及到了结构体相关知识
/ / 把password转为c语言中的char类型
env - >functions - >GetStringUTFChars(env, password, 0LL );
|
为了方便,我提前改好了相关名字

1 2 3 4 5 6 7 8 9 10 | password_length = strlen(password_); / / 获取password_的长度
/ / 把password_拷贝到password__里面,拷贝长度为password_length
strncpy(password__, password_, password_length);
/ / 释放password_指向的字符串空间
/ / 也就是归还空间,让其他程序使用
env - >functions - >ReleaseStringUTFChars((JNIEnv * )env, password, password_);
/ / 获取password__的长度
password__length = strlen(password__);
|
细心的朋友可以知道这个16是有问题的,因为我们的password是30位的,然而这个password确是16位的
这里我改为了32

我们看一下下面的这个函数,可以看到传入了我们password__,password_length,密码与长度,
还有"areyousure??????"

我们双击进入这个函数,然后修改一下如下参数

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 | / / 获取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赋值 0
v14 = 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

如下代码是关键:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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变成了什么
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 | import frida, sys / / 因为是frida hook,所以要导入frida模块,
/ / 读取系统输入需要sys模块
jscode =
def on_message(message, data):
if message[ 'type' ] = = 'send' :
print ( " {0}" . format (message[ 'payload' ]))
else :
print (message)
pass
device = frida.get_remote_device()
session = device.attach( 'cn.pojie52.cm01' )
script = session.create_script(jscode)
script.on( 'message' , on_message)
print ( ' Start attach' )
script.load()
sys.stdin.read()
var so_addr = Module.findBaseAddress( "libnative-lib.so" );
/ / 查找so的基址。
|
在ida中基址为0000000000000
在动态调试时基址会变
所以我们需要通过基址加上函数的偏移来定位一个函数

在这里我们需要hook的函数是sub_B90()

我们按tab键查看函数偏移

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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,arg3
var ret_b90 = sub_b90(arg1,arg2,arg3);
/ / 返回值为ret_b90
Memory.readByteArray(arg1, 64 ) / / 读取arg1内存中数据,也就是password,读 64 位byte的数据
console.log(Memory.readByteArray(arg1, 64 )); / / 打印读取出来的password
|
这里还有一个函数传入了password还有密码的长度

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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返回值地址里面的空间的 64 位byte的数据
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进行异或即可得到密码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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要么全是大写字母,或者全是小写字母,并且不规则
1 2 3 4 5 6 7 8 9 10 11 12 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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代码
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 | import frida, sys
jscode =
/ / 用于接收消息的函数
/ / 第一个参数为需要打印的信息
def on_message(message, data):
if message[ 'type' ] = = 'send' :
print ( " {0}" . format (message[ 'payload' ]))
else :
print (message)
Pass / / 跳过
device = frida.get_remote_device() / / 调用frida的函数,获取设备
session = device.attach( 'cn.pojie52.cm01' )
/ / 这个包名可以通过frida - ps - U 来进行获取
script = session.create_script(jscode)
script.on( 'message' , on_message)
print ( ' Start attach' )
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
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 | 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数据与异或解密获得真吗
|

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

看雪招聘平台创建简历并且简历完整度达到90%及以上可获得500看雪币~
最后于 2022-4-3 12:15
被白云精灵编辑
,原因: