依然用GDA直接找到入口点
protected void onCreate(Bundle p0){
super.onCreate(p0);
this.setContentView(0x7f04001b);
Button bId = this.findViewById(0x7f0b0057);
bId.setText(Decode.a(new byte[5]{0x4e,0xbf,0x49,0xd3,0x67}, 116));
EditText eId = this.findViewById(0x7f0b0056);
eId.setHint(Decode.a(new byte[15]{0xb8,0xc9,0x23,0xd5,0x94,0x94,0x5d,0xff,0xa5,0x5c,0xd9,0xe2,0x2c,0x6e,0x81}, 170));
bId.setOnClickListener(new a(eId));
}
apk打开也没有任何交互,应该就是个纯加密的逆向了。

GDA已经把两个加密函数显示出来了,返回值也不一样
(Decode.a(new byte[5]{0x4e,0xbf,0x49,0xd3,0x67}, 116))

decode中加载了一个库,库名被加密,
native public String check(String p0,String p1){}
可以看到check函数是存在在native原生类中的。
那么还是需要首先处理a(byte,int)
这个函数(下简称ai)
ai套娃了al,


通过解密得到,
System.loadLibrary("check");
button.setText("Check");
editText.setHint("Input your flag");
去查看一下check库

根据经验,直接找到JNI_ONLOAD函数,

虽然并没有像illusion动态注册,但是在if中调用了sub_88C4函数,相当需要注意.
&a1对于GetEnv来说是env参数

sub_8740(a1, &unk_BD9B, 30, 87, v7);
sub_8740(a1, &unk_BDBA, 5, 122, v6);
sub_8740(a1, &unk_BDC0, 56, 49, v5);
//A2参数为数组

【Android NDK 开发】JNI 方法解析 ( JNIEnv *env 参数 )-阿里云开发者社区 (aliyun.com)
通过上面对env函数的解释我们可以知道env是一个结构体,而(env+X)指向的是env中的函数,即使IDA不能直接识别,我们也可以发现这里的函数没有识别是IDA没有识别出JNIEnv结构体,导致反汇编及其难读。
选择a1按Y改名为JNIEnv*,同样的,把上一个函数中的env也改一下


这样就可以明显看出JNI的动态注册了,GetStaticMethodID: 获取静态方法的ID,在这里相当于在decode类中找到a函数的ID,v9就是通过FindClass得到的类的实例
v17是有参数传入的,v12和a5作为函数的返回值
关注到SetByteArrayRegion函数,将一些参数进行了调整
//sub_8740(a1, &unk_BD9B, 30, 87, v7);
//sub_8740(a1, &unk_BDBA, 5, 122, v6);
//sub_8740(a1, &unk_BDC0, 56, 49, v5);
//SetByteArrayRegion(env, byteArray, from, size,nextmapping_map);
//将nextmapping_map数组的第<from>的元素开始复制<size>个元素到byteArray数组中去
(*env)->SetByteArrayRegion(env, v13, 0, 30, &unk_BD9B);
(*env)->SetByteArrayRegion(env, v13, 0, 5, &unk_BDBA);
(*env)->SetByteArrayRegion(env, v13, 0, 56, &unk_BDC0);
实际上的三个数组长度也只有表示的size的长度

v14 = (*env)->CallStaticObjectMethod(env, v10, v11, v13, v17);
static function CallStaticObjectMethod (clazz : IntPtr, methodID : IntPtr, args : jvalue[]) : IntPtr
AndroidJNI.CallStaticObjectMethod用于调用静态方法,v10为类,v11为调用的a方法,v13和v17为参数,即
v14 = (*env)->CallStaticObjectMethod(env, v10, v11, &v13_BD9B, 87);
// com/a/sample/loopcrypto/Decode
v14 = (*env)->CallStaticObjectMethod(env, v10, v11, &v13_BDBA, 122);
// check
v14 = (*env)->CallStaticObjectMethod(env, v10, v11, &v13_BDC0, 49);
// (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;



然后几个函数就是对刚刚得到字符串的一些复制与释放操作,再回到sub_88C4函数
v4[0] = v6; ;check
v4[1] = v5; ;(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
v4[2] = sub_87FC;
v2 = (*a1)->FindClass(a1, v7);
return v2 && (*env)->RegisterNatives(env, v2, v4, 1) >= 0;
; 注意methods有三个元素,在这里sub_87FC其实是str check(str,str)传入的变量
; v4=["check","(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",return(sub_87FC)]
而check()在java层传入的变量可以理解

在这里就可以看到是通过signature的每一位用md5加密后获得digest字节数组,并对<16的数做特殊处理
这下就需要去解决一下sub_87FC

可以看到a3和a4参数是经过处理的,那么就是之前的input和sign_MD5,跟进sub_8690

1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
这里的运行逻辑就成为了:fork后由于子进程返回0;进入if;ptrace调用父进程附加到子进程(自身);close关闭pipe一端;v8被函数赋值;通过v8检查signature,并写入pipe;

MOV将SP放置于R0中,BLX切换了Thumb状态(ADDS指令),BLX会跳转到R1=SP+1,这里最难理解的地方,是Thumb模式下以R13寄存器(也就是SP)作为基址,相当于进入“函数”(在汇编中,if条件后的会被理解为“函数”)(其实本质也只是地址),if后是一个writepipe,将用户输入input作为参数传送到管道。
然后子进程退出

父进程在读取前关闭写入端,读取256长度,字符串结尾置0,将字符串保存到a3中返回
反调试(init段+ptrace)
在so的加载时,会存在一个init段,段内的函数会首先执行,在illusion中提到过JNI_LOAD中会有一些指令跳转到反调试函数,但是这里是另一种反调试手段


可以看到sub_83DC函数

当调试Android应用程序时候,必须调用ptrace(PTRACE_TRACEME)
来附加进程。如果进程在启动时,就调用ptrace PTRACE_TRACEME跟踪了自己。那么这个进程将无法被其他进程附加。
首先我们来解读一下这个反调试是如何实现的
int sub_83DC()
{
int i; // r0
__pid_t v1; // r5
__pid_t v2; // r0
FILE *v4; // r5
int v5; // r8
__pid_t pid; // [sp+4h] [bp-194h]
char v7[10]; // [sp+8h] [bp-190h] BYREF
char v8[118]; // [sp+12h] [bp-186h] BYREF
char s[128]; // [sp+88h] [bp-110h] BYREF
char format[128]; // [sp+108h] [bp-90h] BYREF
int v11; // [sp+188h] [bp-10h]
qmemcpy(format, &unk_F130, sizeof(format));
format[0] = 47;
for ( i = 1; i != 128; ++i )
format[i] ^= 0xE9u;
//获取当前的进程pid
v1 = getpid();
sprintf(s, format, v1);
//创建子进程
v2 = fork();
//子进程
if ( !v2 )
{
//PID变量存放父进程
pid = v1;
//getppid获取父进程
if ( v1 == getppid() )
{
//ptrace从名字上看是用于进程跟踪的,它提供了父进程可以观察和控制其子进程执行的能力,
//并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。
//其基本原理是: 当使用了ptrace跟踪后,所有发送给被跟踪的子进程的信号(除了SIGKILL),都会被转发给父进程
//PTRACE_TRACEME:这个是被调试的进程使用的,使用之后父进程才可以去跟踪子进程
ptrace(PTRACE_TRACEME);
v4 = fopen(s, &format[16]);
if ( v4 )
{
while ( 1 )
{
while ( !fgets(v7, 128, v4) )
{
LABEL_11:
sleep(2u);
v4 = fopen(s, &format[16]);
if ( !v4 )
goto LABEL_12;
}
if ( !strncmp(v7, &format[18], 9u) )
{
v5 = atoi(v8);
fclose(v4);
if ( v5 )
break;
goto LABEL_11;
}
}
}
LABEL_12:
kill(pid, 9);
}
LABEL_13:
exit(1);
}
if ( v2 == -1 )
goto LABEL_13;
return _stack_chk_guard - v11;
}
接下来,安装frida
- windows上
python -m pip install frida
python -m pip install fridatools
- adb连接后查看cpu型号,
getprop ro.product.cpu.abi
- 下载对应版本Frida-server https://github.com/frida/frida/releases
- 通过adb将其上传到手机
adb push .\\frida-server /data/local/tmp
- 再给其授予777权限
chmod 777 frida-server
- 在window上执行
Frida-ps -U
有回显即成功
首先解决一下format存放了什么
format=[0xC6, 0x99, 0x9B, 0x86, 0x8A, 0xC6, 0xCC, 0x8D, 0xC6, 0x9A, 0x9D, 0x88, 0x9D, 0x9C, 0x9A, 0xE9, 0x9B, 0xE9, 0xBD, 0x9B, 0x88, 0x8A, 0x8C, 0x9B, 0xB9, 0x80, 0x8D, 0xE9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
format[0] = chr(47)
for i in range(1,128):
format[i] = chr(0xE9 ^ format[i])
print(''.join(format))
# /proc/%d/statusrTracerPid 注意是有\\x00字符
# &format[16] = r
# &format[18 =
这sprintf(s, format, v1);
也就变成了
sprintf(s, “/proc/%d/statusrTracerPid”, PID);
即读取了当前进程(相对下面来说)父进程的进程状态
v4 = fopen(s, 'r')
并且从文件流中读取128字符流向v7

这里随便找了一个pid查看一下文件
(但是这里的v7只有10,fgets限定长度之后的字符串会被丢弃。)然后通过while循环不断读取10字符,并且与“TracerPid:”进行比较,我们调用atoi()
就可以获取到需要的标志 ,为0返回继续检测,为1则直接break到kill了。
在进行hook之前,我们还需要知道so的初始化流程。
关于so的加载流程(简单了解)

当然,了解到这里还不够,我们再往call_constructors()里面追踪一下

对于
call_array()
函数,则是对初始化函数列表中的每个函数进行遍历,并最终通过call_function
函数来调用执行。
frida获取指定so文件的函数:
通过so遍历模块Process.findModuleByName("linker");
enumerateSymbols遍历函数的符号,返回对象数组。每个对象有isGlobal,type,section,name,address,size。



Interceptor.replace(target, replacement)用于实现完全替换原函数,replacement参数使用JavaScript形式的一个NativeCallback来实现。
new NativeCallback(func, returnType, argTypes[, abi])
argTypes数组指明了参数类型。returnType在这里是int
Frida官方手册 - JavaScript API(篇二) (kanxue.com)
etImmediate将自动重新运行你的脚本
function hook_init() {
var call_function_addr = 0;
var linker = Process.findModuleByName("linker");
// 遍历linker的符号,返回对象数组
// 每个对象有isGlobal,type,section,name,address,size
var symbols = linker.enumerateSymbols();
for (var i=0; i<symbols.length; i++){
var name=symbols[i].name;
//取到call_function
if (name.indexOf("call_function") >= 0) {
call_function_addr = symbols[i].address;
break;
}
}
console.log("call_function_addr->", call_function_addr);
Interceptor.attach(call_function_addr,{
onEnter:function(args){
// 进入函数的时候判断要执行的函数地址末尾是否是3dd(sub_83DC+1)
// 不过为啥是用args[1]就不太清楚了
if(String(args[1]).indexOf("3dd")>=0){
console.log("found FUNCTION sub_83DC+1");
// replace用于实现完全替换原函数
Interceptor.replace(
args[1],
new NativeCallback(
function (s, addr, rp) {
console.log("destory")
},
"int",
[]
)
);
}
},
onLeave: function(retval){
}
})
}

这里出了点问题,貌似不太行的样子,明天继续。。。
这里本来是想偷懒用hook把解密直接拿出来的,但是发现自己手机无法运行这个app,没法动态调试了。
Comments | NOTHING