unidbg教程前置知识之NDK静态、动态注册
声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责。
unidbg是什么?
unidbg 是一款基于 unicorn 和 dynarmic 的逆向工具,一个标准的 java 项目,可以黑盒调用Android和 iOS 中的 so 文件,无论是黑盒调用 so 层算法,还是白盒 trace 输出 so 层寄存器值变化都是一把利器~ 尤其是动态 trace 方面堪比 ida trace。
github 地址 https://github.com/zhkl0228/unidbg
unidbg项目的由来
由于现在的大多数 app 把签名算法已经放到了 so 文件中,所以要想破解签名算法,必须能够破解 so 文件。但是我们知道,C++ 的逆向远比 Java 的逆向要难得多了,所以好多时候是没法破解的,那么这个时候还可以采用 hook 的方法,直接读取程序中算出来的签名,但是这样的话,需要实际运行这个应用,需要模拟器或者真机,效率又不是很高。
unidbg 就是一个很巧妙地解决方案,他不需要直接运行 app,也无需逆向 so 文件,而是通过在 app 中找到对应的 JNI 接口,然后用 unicorn 引擎直接执行这个 so 文件,所以效率也比较高。
更重要的是它可以和springBoot一起做成web服务。
在安卓开发中,可以在Native层利用反射进行Java层方法的调用。它再和NDK动态注册进行配合能实现让逆向人员在so文件逆向中找不到与加密函数直接相关的函数名。这样能拦住一部分安卓逆向人员。
下面主要介绍静态注册和动态注册。
静态注册
原理
public class MainActivity extends AppCompatActivity{
...
public native String stringFromJNI(); //Java 层Native方法
...
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplicationndk_MainActivity_stringFromJNI( // so层方法
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
- 也就是说,Java_com_example_ndk_MainActivity_stringFromJNI方法对应的是Java层包名为com.example.myapplicationndk的MainActivity类的stringFromJNI方法。
CPP代码预览
新建一个Android项目,项目名为myapplicationndk,包名为com.example.myapplicationndk,选择语言为Java,并选择项目模板为“Native C++”,如下图所示:
图片1
图片2
项目创建完成后,其工程结构目录如下:
图片3
这里主要关注的是cpp文件夹,这是系统自动生成的,该文件夹下包含一个CMakeLists.txt,其文件内容也是系统自动生成的,其内容如下:
add_library( # Sets the name of the library.
myapplicationndk
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
native-lib.cpp)
其中myapplicationndk是由native-lib.cpp文件编译后生成的so文件名,该文件名在MainActivity.java中作为参数被加载到应用中:
public class MainActivity extends AppCompatActivity {
// 应用启动时加载native-lib.so库
static {
System.loadLibrary("myapplicationndk");
}
...
}
在MainActivity.java中定义一个native方法myFunc(),该方法包含一个int型的参数,且返回值类型为String,如下图所示:
虽然报错了但是没有多大的问题,是因为我们还没有注册这个方法。将鼠标移动到报错处,按下快捷键Alt+Shift+Enter。如下所示:
操作结束,在native层自动创建了对应的JNI方法,如下所示:
可以看出,上图中的方法上方有JNIEXPORT和JNICALL两个宏定义声明,且其命名符合native方法和so方法的对应规则。其中JNIEnv类型代表了Java环境,通过这个JNIEnv* 指针,就可以对Java端的代码进行操作。jobject thiz代表该native方法的类实例或者这个native方法的类的class对象实例。然后实现其函数功能:
在MainActivity.java中调用native方法:
将以上程序运行到模拟器或者真机上,可以看到在程序启动以后会弹出如下图所示的提示:
优点
- 理解和使用方式简单, 属于傻瓜式操作, 使用相关工具按流程操作就行, 出错率低
缺点
- 必须遵循某些规则
- JNI方法名过长
- 运行时根据函数名查找对应的JNI函数,程序效率不高
- 当需要更改类名,包名或者方法时, 需要按照之前方法重新生成头文件, 灵活性不高
动态注册
原理
在调用System.loadLibrary()时会在so层调用一个名为JNI_OnLoad()的函数,我们可以提供一个函数映射表,再在JNI_Onload()函数中通过JNI中提供的RegisterNatives()方法来注册函数。这样Java就可以通过函数映射表来调用函数,而不必通过函数名来查找对应函数。
实现流程
- 利用结构体JNINativeMethod数组记录native方法与JNI方法的对应关系,即函数映射表
- 实现JNI_OnLoad方法,在加载动态库后,执行动态注册
- 调用FindClass方法,获取java对象
- 调用RegisterNatives方法,传入java对象、JNINativeMethod数组以及注册数目完成注册
其中JNINativeMethod结构体如下所示:
typedef struct {
const char* name; // native方法名
const char* signature; // 方法签名,例如()Ljava/lang/String;
void* fnPtr; // 函数指针
} JNINativeMethod;
这里关注一下第二个参数,其类型为字符串,由一对小括号和若干签名符号组成,其中括号内写传入参数的签名符号,没有参数则不写,括号外写返回参数的签名符号。
签名符号 |
C/C++ |
java |
V |
void |
void |
Z |
jboolean |
boolean |
I |
jint |
int |
J |
jlong |
long |
D |
jdouble |
double |
F |
jfloat |
float |
B |
jbyte |
byte |
C |
jchar |
char |
S |
jshort |
short |
[Z |
jbooleanArray |
boolean[] |
[I |
jintArray |
int[] |
[J |
jlongArray |
long[] |
[D |
jdoubleArray |
double[] |
[F |
jfloatArray |
float[] |
[B |
jbyteArray |
byte[] |
[C |
jcharArray |
char[] |
[S |
jshortArray |
short[] |
L+完整包名+类名 |
jobject |
class |
例如:Java层函数String getText(int a,byte[] b)
的方法签名就是(I[B)Ljava/lang/String;
。
其实没有必要去深究其签名的生成,将App编译好之后,用jadx或jeb反编译查看dex文件能获得smile汇编,在这里面有函数的名字,以及签名信息。
CPP代码预览
还是在该项目中,对native层代码做一定的修改即可,修改之后的动态注册的代码。
#include <jni.h>
#include <string>
std::string stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
//env->FindClass()
return reinterpret_cast<const char *>(env->NewStringUTF(hello.c_str()));
}
std::string myFunc(JNIEnv *env, jobject thiz, jint i_param) {
// TODO: implement myFunc()
std::string retValue="input value is :===> ";
std::string temp;
temp=std::to_string(i_param);
retValue+=temp;
return reinterpret_cast<const char *>(env->NewStringUTF(retValue.c_str()));
}
JNINativeMethod methods ={
{"stringFromJNI","()Ljava/lang/String;",(void*)stringFromJNI},
{"myFunc","(I)Ljava/lang/String;",(void*)f1}//
};
jint JNI_OnLoad(JavaVM* vm,void* reserved){
JNIEnv *env=NULL;
if(vm->GetEnv((void**)&env,JNI_VERSION_1_6)!=JNI_OK){
return JNI_ERR;
}
jclass clazz = env->FindClass("com/example/myapplicationndk/MainActivity");
if(clazz==NULL){
return JNI_ERR;
}
jint iMethod=sizeof(methods)/ sizeof(methods[0]);
jint result=env->RegisterNatives(clazz,methods,iMethod);
if(result<0){
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
由此可见,JNI方法上方不再有JNIEXPORT和JNICALL两个宏定义声明,方法名也不需要遵循某些规则,而是通过RegisterNatives方法完成动态注册。添加完上述代码以后,MainActivity.java中的两个native方法也不再报错了。
优点
- 通过函数映射表来查找对应的JNI方法,运行效率高
- 不需要遵循命名规则,灵活性更好
缺点
- 实现起来相对复杂
- 对新手来说稍微有点难理解, 同时会由于搞错签名, 方法, 导致注册失败
动态注册JNI函数失败原因
此类问题一般有两种情况:
-
包名,类名或者函数签名写错了, 这个解决方法就仔细检查一下就行, 当然如果一些签名不知道怎么写, 可以用静态注册方法通过javah生成头文件, 然后看下头文件里面注释, 把注释的签名复制过来就行
-
由于加入了混淆机制, 导致无法通过类名来找到对应方法了, 这种情况我遇到过两次, 表现稍有差异, 但本质问题都是混淆引起的.
- Java中定义的native方法, 在代码中其他地方用到了就能注册上, 没用到的只声明了的函数就会注册失败, 原因是有些没有用到的函数, 混淆是会自动剔除, 所以实际运行的代码中就没有相关方法了, 动态注册就会失败
- 所有方法都注册不上, 同时也确定相关包名类名签名没有错, 这种情况就是即使你用到了相关方法, 但经过过混淆后已经不是原来的名称了, 所以注册失败
这两种错误解决方法也简单:
- 禁用混淆, 这样做不是太好, 混淆对防止反编译和apk瘦身有很大帮助
- 添加混淆白名单, 和JNI相关的类或者方法不做混淆,
如何定位crash原因
crash问题是开发中比较常见的, Java由于其特性, Crash问题我们直接看AndroidRuntime的Log就行, C/C++的就要麻烦些了.
说明: 如果Crash对应的so没有符号表, so库是别人编译的, 额外去除了符号表(Releas版本), 这种情况是没法定位crash代码具体是在哪个位置的.
对于有源码并且是自己可以编译的情况下, 可通过如下方式对Crash代码进行定位:
-
通过ndk-stack定位
ndk-stack是NDK开发工具包中自带的, 配置好NDK后即可使用, 使用方式有如下两种:
adb logcat |ndk-stack -sym 带符号表的so库路径
, 如果是Android源码方式编译, 带符号表的so库路径为(32位)out/target/product/xxx/symbols/system/lib/
, 64位就在lib64目录想, 如果使用ndk-build编译的代码, 带符号表的so就在和libs同级的obj目录里面. 执行adb命令后, 只需复现crash即可看到Log中输出的Crash栈对应的代码行数和位置
- 如果Log是以文件方式存储的, 可通过
ndk-stack -sym [带符号表的so库路径] -dump [log文件路径]
进行查看
-
通过addr2line定位
addr2line相当于缩减版的ndk-stack, 每次只能看一个地址的位置, 使用非常简单addr2line -f -e [带符号表的so路径] [crash地址0xxxx]
注意事项: 如果源码和so库不是完全对应的, 即so库发布后, 源码有过修改,这样会导致定位的行数有些偏移, 不完全准确, 需要额外注意下.
小技巧: 只看native层crash log, 可直接 adb logcat *:F
C/C++中打印Log
这个比较常见, 教程也比较多, 我这里也做下记录:
-
在C/C++中引入系统Log头文件 #include <android/log.h>
-
在log.h中定义了相关的日志输出函数 ,例如 int __android_log_print(int prio, const char* tag, const char* fmt, ...)
参数prio代表了不同日志级别,不同日志级别如下所示
typedef enum android_LogPriority {
/** For internal use only. */
ANDROID_LOG_UNKNOWN = 0,
/** The default priority, for internal use only. */
ANDROID_LOG_DEFAULT, /* only for SetMinPriority() */
/** Verbose logging. Should typically be disabled for a release apk. */
ANDROID_LOG_VERBOSE,
/** Debug logging. Should typically be disabled for a release apk. */
ANDROID_LOG_DEBUG,
/** Informational logging. Should typically be disabled for a release apk. */
ANDROID_LOG_INFO,
/** Warning logging. For use with recoverable failures. */
ANDROID_LOG_WARN,
/** Error logging. For use with unrecoverable failures. */
ANDROID_LOG_ERROR,
/** Fatal logging. For use when aborting. */
ANDROID_LOG_FATAL,
/** For internal use only. */
ANDROID_LOG_SILENT, /* only for SetMinPriority(); must be last */
} android_LogPriority;
在这个枚举类型中只需要注意
- ANDROID_LOG_VERBOSE
- ANDROID_LOG_DEBUG
- ANDROID_LOG_INFO
- ANDROID_LOG_WARN
- ANDROID_LOG_ERROR
这五个就行,因为这5个是常用的日志输出级别
unidbg讲解函数流程
在之前对unidbg和ndk开发介绍之后相信你对unidbg这个项目诞生的原因了。下面主要介绍环境搭建以及unidbg调用的流程。
环境搭建
unidbg 项目用 Java 编写,并且官网下载的下来的代码使用的是标准的 maven 构建的,所以在使用 unidbg 之前需要修改先安装好 JDK 环境和 Maven 环境。将下载的 unidbg-master.zip 进行解压,然后使用 IDEA 导入项目。【File】 –> 【New】–> 【Project from Existing Sources】
IntelliJ IDEA
官网:https://www.jetbrains.com.cn/idea/ 下载社区版即可,一直下一步即可
Maven 环境
官网:https://maven.apache.org/download.cgi
在官网下载对应系统的maven 版本,maven 版本不闭合我一致,能用就行
下载解压。
配置环境变量
设置环境变量时,可以创建一个叫做”MAVEN_HOME“的系统变量名称,值是maven文件夹路径
在path系统变量中引用MAVEN_HOME变量,指向MAVEN_HOME中的bin目录。
测试maven是否配置完毕
输入mvn -v命令,如果出现maven版本号,就表明安装成功。如下所示
配置maven
Maven的环境变量配置之后,接下来我们还需要对Maven进行必要的配置,尤其是要配置Maven仓库。
1. 配置settings.xml文件
这个settings.xml文件很重要,里面可以配置maven的仓库,私服,jdk等。
随便在maven 解压目录下,创建一个文件夹,作为本地仓库。
然后在settings.xml文件中,在本地仓库路径配置本地仓库路径。
在IDEA中关联maven
至此,Maven的安装配置就做完了,接下来接下来我们就可以在idea中利用maven了
unidbg 代码讲解
从https://github.com/zhkl0228/unidbg 下载项目代码
用IDEA打开项目
file ->open 然后选择unidbg 解压后的文件夹
项目打开大体结构
框架的目录 unidbg-android/src/test/java 放置了很多示例,足以支撑入门
unidbg 基本流程
- 创建32位模拟器实例,
emulator = AndroidEmulatorBuilder.for32Bit().build();
- 创建模拟器内存接口
final Memory memory = emulator.getMemory();
- 设置系统类库解析
memory.setLibraryResolver(new AndroidResolver(23));
- 创建 Android 虚拟机
vm = emulator.createDalvikVM();
- 加载 so 到虚拟内存,第二个参数的意思表示是否执行动态库的初始化代码
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/xxx/xxx.so"),true);
- 获取 so 模块的句柄
module = dm.getModule();
- 设置 JNI 需要继承
AbstractJni
vm.setJni(this);
- 打印日志
vm.setVerbose(true);
- 调用 JNI_Onload
dm.callJNI_OnLoad(emulator);
- 创建 jobject, 如果没用到的话可以不写 ,要用需要调用函数所在的Java类完整路径,比如a/b/c/d等等,注意.需要用/代替
cNative = vm.resolveClass("com/xxx/xxx")
下面是完整的代码
package com.testUnidbg.testCode;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.LibraryResolver;
import com.github.unidbg.arm.backend.DynarmicFactory;
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.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;
import java.io.File;
public class MainActivity extends AbstractJni {
public static void main(String[] args) {
long start = System.currentTimeMillis();
com.kanxueClass.testCode.MainActivity mainActivity = new com.kanxueClass.testCode.MainActivity();
System.out.println("load offset=" + (System.currentTimeMillis() - start) + "ms");
mainActivity.crack();
}
private final AndroidEmulator emulator;
private final VM vm;
private final DvmClass dvmClass ;
private MainActivity() {
emulator = AndroidEmulatorBuilder
.for32Bit()
.addBackendFactory(new DynarmicFactory(true))
.build();
Memory memory = emulator.getMemory();
LibraryResolver resolver = new AndroidResolver(23);
memory.setLibraryResolver(resolver);
vm = emulator.createDalvikVM();
vm.setVerbose(true);
vm.setJni(this);
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/armeabi-v7a/lib2.so"), true);
dm.callJNI_OnLoad(emulator);
dvmClass = vm.resolveClass("com/xxx/xxxx/MainActivity");
}
private void crack() {
String result = (String) dvmClass.callStaticJniMethodObject(emulator,"Sign(Ljava/lang/String;)Ljava/lang/String;","21123").getValue();
int i= 1000;
System.out.println("result " + result); ;
}
}
参数构建
基本方式
List<Object> args = new ArrayList<>(10);
兼容格式
// 参数1:JNIEnv *env
args.add(vm.getJNIEnv());
jobject 或 jclass
DvmObject<?> cnative = cNative.newObject(null);
args.add(cnative.hashCode());
如果用不到直接填0即可
args.add(0);
字符串对象
String input = "abcdef";
args.add(vm.addLocalObject(new StringObject(vm, input)));
bytes 数组
String str= "abcdef";
byte[] str_bytes = str.getBytes(StandardCharsets.UTF_8);
ByteArray str_bytes _array = new ByteArray(vm,str_bytes );
args.add(vm.addLocalObject(str_bytes _array));
bool
// false 填 0,true 填 1
args.add(1);
AndroidEmulator 实例
使用 AndroidEmulatorBuilder 可以来帮助你快速创建一个 AndroidEmulator 的实例。
AndroidEmulator 创建
AndroidEmulator emulator = AndroidEmulatorBuilder
//指定32位CPU
.for32Bit()
//添加后端,推荐使用Dynarmic,运行速度快,但并不支持某些新特性
.addBackendFactory(new DynarmicFactory(true))
//指定进程名,推荐以安卓包名做进程名
.setProcessName("com.github.unidbg")
//设置根路径
.setRootDir(new File("target/rootfs/default"))
//生成AndroidEmulator实例
.build();
AndroidEmulator 使用
AndroidEmulatorBuilder 构造了一个 AndroidEmulator 实例之后,就可以直接来操作这个实例,常使用的一些API
//获取内存操作接口
Memory memory = emulator.getMemory();
//获取进程pid
int pid = emulator.getPid();
//创建虚拟机
VM dalvikVM = emulator.createDalvikVM();
//创建虚拟机并指定APK文件
VM dalvikVM = emulator.createDalvikVM(new File("apk file path"));
//获取已创建的虚拟机
VM dalvikVM = emulator.getDalvikVM();
//显示当前寄存器状态 可指定寄存器
emulator.showRegs();
//获取后端CPU
Backend backend = emulator.getBackend();
//获取进程名
String processName = emulator.getProcessName();
//获取寄存器
RegisterContext context = emulator.getContext();
//Trace读内存
emulator.traceRead(1,0);
//Trace写内润
emulator.traceWrite(1,0);
//Trace汇编
emulator.traceCode(1,0);
//是否正在运行
boolean running = emulator.isRunning();
Memory 实例
Memory memory = emulator.getMemory();
//指定Android SDK 版本,目前支持19和23两个版本
memory.setLibraryResolver(new AndroidResolver(23));
//拿到一个指针,指向内存地址,通过该指针可操作内存
UnidbgPointer pointer = memory.pointer(address);
//获取当前内存映射情况
Collection<MemoryMap> memoryMap = memory.getMemoryMap();
//根据模块名来拿到某个模块
Module module = memory.findModule("module name");
//根据地址拿到某个模块
Module module = memory.findModuleByAddress(address);
VM 操作
//推荐指定APK文件,Unidbg会自动做许多固定的操作
VM vm = emulator.createDalvikVM();
//是否输出JNI运行日志
vm.setVerbose(true);
//加载SO模块 参数二设置是否自动调用init函数
DalvikModule dalvikModule = vm.loadLibrary(new File("so 文件路径"), true);
//设置JNI交互接口 参数需实现Jni接口,推荐使用this继承AbstractJni
vm.setJni(this);
//获取JNIEnv指针,可作为参数传递
Pointer jniEnv = vm.getJNIEnv();
//获取JavaVM指针,可作为参数传递
Pointer javaVM = vm.getJavaVM();
//调用JNI_OnLoad函数
vm.callJNI_OnLoad(emulator,dalvikModule.getModule());
//向VM添加全局对象,返回该对象的hash值
int hash = vm.addGlobalObject(dvmObj);
//获取虚拟机中的对象,参数为该对象的hash值
DvmObject<?> object = vm.getObject(hash);
unidbg hook
hook 代码是逆向最基本的功能之一,frida 的 hook 代码都不陌生,Unidbg 还内置了多种 HOOK 框架,unidbg 底层用的是分析So比较实用的 HookZz 框架, hook 的代码的demo。
//unidbg集成了HookZz框架
int address=0x11111;
HookZz hookZz = HookZz.getInstance(emulator);
hookZz.replace(address, new ReplaceCallback() {
@Override
public HookStatus onCall(Emulator<?> emulator, long originFunction) {
return super.onCall(emulator, originFunction);
}
@Override
public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
//R2和R3才是参数,R0是env,R1是object
System.out.println(String.format("R2: %d, R3: %d",context.getIntArg(2),context.getIntArg(3)));
//把第二个参数R3改成5
emulator.getBackend().reg_write(Unicorn.UC_ARM_REG_R3,5);
return super.onCall(emulator, context, originFunction);
}
@Override
public void postCall(Emulator<?> emulator, HookContext context) {
emulator.getBackend().reg_write(Unicorn.UC_ARM_REG_R0,10);
//返回值放R0,这里直接修改返回值
super.postCall(emulator, context);
}
},true);
so文件算法流程分析
在通过unidbg主动调用算法成功计算出结果。下面通过IDA 分析so文件,还原算法的流程。
用IDA打开so文件分析,打开导出表,找到JNI_Load()函数。点进去,然后可以看到
从上图中看出,native层是通过动态加载的。加载的方法名在method_table
中,之前的流程不知到做了什么事情,不用管,不会影响到后续的算法分析。
method_table 中可以看到在native层中重新定义的名字是 fuck
,接下来到该函数中去看看。
if ( !input_str )
return 0;
//将一个输入的jstring 转化成 UTF8的sting
input_str_src = (unsigned __int8 *)_JNIEnv::GetStringUTFChars(env, input_str, 0);
//查找build类,去获得手机相关指纹信息
clazz = _JNIEnv::FindClass(env, "android/os/Build");
fieldID = _JNIEnv::GetStaticFieldID(env, clazz, "FINGERPRINT", "Ljava/lang/String;");
_JNIEnv::GetStaticObjectField(env, clazz, fieldID);
//对输入的字符串追加REAL 字符
strcat((char *)input_str_src, "REAL");
//对拼接后的字符串再进一步处理
obj = (_jobject *)j_o0OoOOOO(env, input_str_src);
_android_log_print(4, "roysuejni", "before entering aes => %s", (const char *)input_str_src);
//引入Java MD5算法
Class = _JNIEnv::FindClass(env, "java/security/MessageDigest");
methodID = _JNIEnv::GetStaticMethodID(env, Class, "getInstance", "(Ljava/lang/String;)Ljava/security/MessageDigest;");
method_str = j_o0OoOOOO(env, "MD5");
v14 = _JNIEnv::CallStaticObjectMethod(env, Class, methodID, method_str);
v13 = _JNIEnv::GetMethodID(env, Class, "digest", "([B)[B");
v12 = _JNIEnv::FindClass(env, "java/lang/String");
v11 = _JNIEnv::GetMethodID(env, v12, "getBytes", "()[B");
v10 = _JNIEnv::CallObjectMethod(env, obj, v11);
array = (_jbyteArray *)_JNIEnv::CallObjectMethod(env, v14, v13, v10);
//最终结果 被ByteArrayElements记录
ByteArrayElements = _JNIEnv::GetByteArrayElements(env, array, 0);
//对最终的结果格式化
for ( i = 0; i <= 15; ++i )
sprintf((char *)&v24[i], "%02x", (unsigned __int8)ByteArrayElements[i]);
//对输入的字符串加个REAL之后的字符在进行一遍处理
v23 = (char *)j_ll11l1l1ll(input_str_src);
//字符拼接到v23中
strcat(v23, (const char *)v24);
//获得最终加密结果
output_str = j_o0OoOOOO(env, (const unsigned __int8 *)v23);
_android_log_print(4, "roysuejni", "result is => %s ", v23);
//释放之前的指针资源
_JNIEnv::ReleaseStringUTFChars(env, input_str, input_str_src);
free(v23);
return output_str;