adworld-mobile-loopcrypto学习记录(1)

发布于 7 分钟前  1 次阅读


依然用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);

这下去解决一下sub_87FC

这里遇到一个问题,参数不明,并且在继续RegisterNatives需要得到v4的内存地址,需要进行动态调试。

反调试(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

  1. windows上python -m pip install frida python -m pip install fridatools
  2. adb连接后查看cpu型号,getprop ro.product.cpu.abi
  3. 下载对应版本Frida-server https://github.com/frida/frida/releases
  4. 通过adb将其上传到手机adb push .\\frida-server /data/local/tmp
  5. 再给其授予777权限chmod 777 frida-server
  6. 在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 函数来调用执行。

未完待续。。。


间桐桜のお菓子屋さん