首页
社区
课程
招聘
[分享]分析libmsaoaidsec.so的frida反调试
发表于: 2天前 1075

[分享]分析libmsaoaidsec.so的frida反调试

2天前
1075

首先frida启动app,发现进程终止。所以要找到在哪个so文件中做的反调试。so文件由java层的System.loadLibrary或者System.load进行加载。以安卓11为例,先分析一下System.loadLibrary的加载流程。


  • so文件加载流程分析

从nativeLoad就会进入c层

native函数的函数名都很规整,可以直接搜索Runtime_nativeLoad定位到

在vm->LoadNativeLibrary中调用代码如下,函数内容有点多截不全

大家喜欢hook的android_dlopen_ext就在这里


do_dlopen有点多,只截图部分

find_libary和si->call_constructors是重点。find_libary通过命名空间寻址、mmap 内存映射、递归加载依赖库以及 GOT/PLT 重定位,将磁盘上的 SO 文件完整转化为内存可执行状态并交付 soinfo 句柄。而call_constructors中会执行init_proc函数和init_array数组,如下图。


  • 定位哪个so文件进行的检测

我喜欢hook call_constructors函数来获取刚刚加载的so文件,所以花篇幅写了下so文件的加载流程。再多一嘴,安卓11的call_function函数好像被内联优化了,导出符号中找不到call_function。如果想要hook安卓11的call_function,可以直接用偏移。下面是用ida打开的linker64中的call_constructors函数,可以对比一下。

框起来的部分应该就是call_function函数,v9应该就是上图的function,也就是要执行的函数。call_array函数中也是一样的。

linker中的get_realpath函数可以获取到so文件的名称。


所以获取so文件加载的frida脚本如下:


打印内容如下:


  • 定位检测函数

肯定不是系统库进行的反调试,所以优先找最后一个加载的so文件,即libmsaoaidsec.so。

然后要定位到反调试的位置,我已JNI_Onload为起始来找,如果JNI_Onload能成功执行完毕,说明反调试的位置只能在init_proc或者init_array。

代码如下:


可以看到毛都没打印,所以一定在JNI_Onload之前。


那么我尝试hook下init_proc. 把上面代码的JNI_onload的偏移改为init_proc的偏移0x1360c

可以看到只有进入,没有离开。那么肯定就是在init_proc里面做的反调试。init_proc部分截图如下,可以看到有控制流平坦化

不过不要紧,我们把真实块的起始地址hook一下,看看执行顺序,也可以看到是执行到哪里突然崩溃的。

真实块其实就是代码有用的部分,不需要看控制流图,肉眼扫一下就可以看出来哪些代码是有用的。如下。

代码如下:

可以看到执行流程如下,执行到0x137f4就退出了。我们从下往上看。

先分析0x137f4处


有两个函数调用,hook地址0x137f4和0x137f8,结果就是只走到了0x137f4,没有执行到0x137f8进程就退出了

所以现在要进入到函数0x12c28去看

还是和上面一样的方法,收集真实块,然后看看最后执行到哪里

去看0x12d38处

只调用了函数0x12dc8,去看0x12dc8,有点长就不截图了,可以知道的是虽然有混淆,但很容易就可以看出并没有什么检测。

我们回到上面分析init_proc处

0x137f4这个块就分析完了,没有进行frida检测,于是看上一个,即看0x13974

进入0x1a8a0内部

进入0x1a5b0,太长,截图截不全,直接贴代码了


__int64 __fastcall sub_1A5B0(__int64 a1)

{

  __int64 result; // x0

  int v2; // w9

  int v3; // w8

  __int64 v4; // x0

  int v5; // [xsp+Ch] [xbp-74h]

  int v6; // [xsp+10h] [xbp-70h]

  char v7; // [xsp+16h] [xbp-6Ah]

  bool v8; // [xsp+17h] [xbp-69h]


  _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));

  result = sub_C0A4(a1);

  v2 = 1890888035;

  v5 = result;

  while ( 1 )

  {

    while ( 1 )

    {

      while ( 1 )

      {

        while ( 1 )

        {

          v3 = v2;

          if ( v2 <= 672937260 )

            break;

          if ( v2 <= 1421965345 )

          {

            if ( v2 == 672937261 )

            {

              result = sub_C024();

              v8 = (_DWORD)result == 167;

              v2 = -1849752058;

            }

            else if ( v2 == 928581447 )

            {

              result = pthread_create(&qword_45650, 0LL, (void *(*)(void *))sub_1A574, 0LL);

              v2 = 932419925;

            }

            else

            {

              v2 = 672937261;

            }

          }

          else if ( v2 > 1525727782 )

          {

            if ( v2 == 1525727783 )

            {

              if ( v7 )

                v2 = -1962676898;

              else

                v2 = 119870274;

            }

            else if ( v5 == 248 )

            {

              v2 = 1421965346;

            }

            else

            {

              v2 = -1962676898;

            }

          }

          else if ( v2 == 1421965346 )

          {

            v4 = sub_16B00();

            result = sub_1229C(v4);

            v7 = result & 1;

            v2 = 1525727783;

          }

          else

          {

            result = pthread_create(qword_45658, 0LL, (void *(*)(void *))sub_18C88, 0LL);

            v2 = -562034154;

          }

        }

        if ( v2 <= -1169886676 )

          break;

        if ( v2 == -1169886675 )

        {

          result = sub_1A234();

          v2 = 672937261;

        }

        else

        {

          v2 = -1826849722;

          if ( v3 != -562034154 )

          {

            v2 = v3;

            if ( v3 == 119870274 )

            {

              result = sub_1B88C();

              v2 = -1962676898;

            }

          }

        }

      }

      if ( v2 > -1826849723 )

        break;

      if ( v2 == -1962676898 )

      {

        result = sub_C0E4();

        v6 = result;

        v2 = -1668225964;

      }

      else if ( v8 )

      {

        v2 = 1475023738;

      }

      else

      {

        v2 = -1826849722;

      }

    }

    if ( v2 != -1668225964 )

      break;

    if ( v6 == 249 )

      v2 = 928581447;

    else

      v2 = -1169886675;

  }

  return result;

}


用同样的方法,看执行流

0x1a824处没做什么,于是看0x1a840

pthread_create是创建线程的函数,c定义为

int pthread_create(pthread_t *thread,   const pthread_attr_t *attr,  void *(*start_routine) (void *),  void *arg);


第一个参数:这是一个指向线程标识符的指针,当线程创建成功后,系统会将新线程的 ID 写入这个指针指向的内存区域。你可以通过这个 ID 在后续调用 pthread_join(等待线程)或 pthread_detach(分离线程)时指定目标。

第二个参数:指向线程属性对象的指针。

第三个参数:这是新线程启动后立即执行的函数指针。该函数必须接收一个 void * 类型的参数,并返回一个 void * 类型的结果。当这个函数返回时,线程也就结束了。

第四个参数:传递给 start_routine 的唯一参数。


其实可以简单把pthread_create函数理解为

int pthread_create(pthread_t *thread,   const pthread_attr_t *attr,  void *(*start_routine) (void *),  void *arg){

...

    start_routine(arg);

...

}


所以重点是第三个参数,点进去看下

先看0x19cfc,代码有点长,不截图了,其实只做了一件事,读取文件并检查文件中有无特定字符串,其他的都是一些字符串解密的操作。关键代码如下:

关键在于fopen函数的第一个参数,用frida动态的看一下是什么


还有strstr函数的第二个参数:


结论:打开的文件是/proc/pid/status 。检测的字符串内容为"TracerPid:"

但他只是检测了,并没有让进程退出的操作,于是我们回到函数0x1a574

hook看一下0x19cfc的返回值是什么


返回值是0,看下面这个if判断,如果返回值为0的话,那么只会执行sub_1A3D0() == 777

接下来去看0x1a3d0函数,以下是该函数,并且我问ai给每行代码写了注释,总结就是遍历当前进程的所有线程(通过 /proc/[pid]/task/ 目录),读取每个线程的 stat 状态文件。如果发现任何一个线程的状态为 Tt(代表 Stopped 或 Traced,即被调试器挂起或追踪状态),则返回 777,以此判断自身正在被动态分析或调试。


__int64 sub_1A3D0()

{

  unsigned int v0; // w0,存储当前进程的 PID

  DIR *v1; // x0,指向打开的目录流的指针

  DIR *v2; // x19,目录流指针的备份

  struct dirent *v3; // x0,指向目录项(dirent)的结构体指针

  unsigned __int8 v4; // w9,用于检查目录名字符

  const char *d_name; // x21,当前读取到的目录名(线程ID)

  char *v6; // x8,用于遍历目录名字符串的指针

  int v7; // t1,临时存放读取的字符

  unsigned int v8; // w0,再次获取的当前进程 PID

  __int64 result; // x0,存储各种系统调用的返回值(如 open, read 的 fd 或长度)

  int v10; // w21,存储打开的 stat 文件的文件描述符 (fd)

  __int64 v11; // x24,用于遍历 stat 文件内容的索引(寻址下标)

  int v12; // w8,读取 stat 文件中的单个字符

  char v13[1024]; // [xsp+8h] [xbp-A48h] BYREF,用于存放从 stat 文件中读取的文本内容

  char name[1024]; // [xsp+408h] [xbp-648h] BYREF,存放 "/proc/[pid]/task" 目录路径

  char s[512]; // [xsp+808h] [xbp-248h] BYREF,存放 "/proc/[pid]/task/[tid]/stat" 文件路径

  __int64 v16; // [xsp+A08h] [xbp-48h],Stack Canary(栈保护值),防止缓冲区溢出


  // 1. 设置栈保护 (Stack Cookie/Canary)

  v16 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);

  

  // 2. 初始化缓冲区,清零

  memset(s, 0, sizeof(s));

  memset(name, 0, sizeof(name));

  memset(v13, 0, sizeof(v13));

  

  // 3. 获取当前进程的 PID

  v0 = getpid();

  

  // 格式化字符串,生成当前进程的 task 目录路径,例如 "/proc/1234/task"

  // 该目录下包含了当前进程所有的线程 ID (TID)

  _sprintf_chk(name, 0LL, 1024LL, "/proc/%d/task", v0);

  

  // 打开这个 task 目录

  v1 = opendir(name);

  if ( !v1 )

    return 0xFFFFFFFFLL; // 如果打开失败(通常是没有权限或路径不存在),返回 -1

    

  v2 = v1; // 备份 DIR 指针

  

  // 4. 遍历 task 目录下的所有子目录(每个子目录代表一个线程)

  v3 = readdir(v1); // 读取第一个目录项

  if ( v3 )

  {

    while ( 1 ) // 开始遍历循环

    {

      d_name = v3->d_name; // 获取目录名

      v4 = v3->d_name[0];  // 获取目录名的第一个字符

      

      if ( v4 ) // 如果字符不为空

      {

        v6 = &v3->d_name[1]; // 指针指向第二个字符

        

        // 5. 校验目录名是否为纯数字(过滤掉 "." 和 ".." 等非线程目录)

        // (v4 - 48 > 9) 相当于判断字符是不是不在 '0' (48) 到 '9' (57) 之间

        while ( (unsigned int)v4 - 48 > 9 ) 

        {

          v7 = (unsigned __int8)*v6++; // 读取下一个字符

          v4 = v7;

          if ( !v7 ) // 如果遇到字符串结尾 '\0' 都没有找到数字

            goto LABEL_14; // 跳过这个目录项,读取下一个

        }

        

        // 如果目录名是数字(代表线程 ID),则拼接进入该线程的 stat 文件路径

        v8 = getpid();

        // 格式化生成: "/proc/[pid]/task/[tid]/stat"

        _sprintf_chk(s, 0LL, 512LL, "/proc/%d/task/%s/stat", v8, d_name);

        

        // 6. 打开当前线程的 stat 状态文件(O_RDONLY = 0)

        result = open(s, 0);

        if ( (_DWORD)result == -1 ) // 如果打开失败

          return result;            // 直接返回 -1 报错退出

          

        v10 = result; // 保存文件描述符 fd

        

        // 读取 stat 文件内容到 v13 缓冲区,最多读 1024 字节

        result = _read_chk(result, v13, 1024LL, 1024LL);

        if ( result == -1 ) // 如果读取失败

          return result;

          

        v11 = 0LL; // 初始化索引为 0

        

        // 7. 解析 stat 文件的文本内容

        // stat 文件的标准格式类似: "1234 (thread_name) S 123 123 ..."

        // 第一个字段是 PID,第二个是括在括号里的线程名,第三个字段是状态字符。

        // 这里通过寻找右括号 ')' 来定位线程名的结束位置。

        do

          v12 = (unsigned __int8)v13[v11++]; // 逐字节读取

        while ( v12 != 41 ); // 41 是 ')' 的 ASCII 码

        

        close(v10); // 读取并找到右括号后,关闭 stat 文件

        

        // 8. 检查线程状态 (核心反调试逻辑)

        // 此时 v11 刚好越过 ')',v13[v11] 是括号后面的空格

        // v13[v11 + 1] 是状态字符 (如 'R', 'S', 'D', 'Z', 'T', 't')

        // v13[v11 + 2] 是状态字符后面的空格

        // 

        // 运算 `v13[v11 + 1] | 0x20` 是一个将大写字母转为小写字母的位运算技巧。

        // 116 是 't' 的 ASCII 码。32 是空格 ' ' 的 ASCII 码。

        // 所以这里是在严格判断:状态字符是不是 'T' 或 't',并且后面紧跟一个空格。

        // 在 Linux 中,'T' 或 't' 代表 "Traced" (被追踪/调试) 或 "Stopped" (暂停)。

        if ( ((unsigned __int8)v13[(unsigned int)(v11 + 1)] | 0x20) == 116 && v13[(unsigned int)(v11 + 2)] == 32 )

          return 777LL; // 如果发现任何一个线程处于 Traced 状态,立刻返回 777,表示检测到调试器!

      }

      

LABEL_14:

      // 读取下一个目录项 (下一个线程)

      v3 = readdir(v2);

      if ( !v3 ) // 如果没有更多目录了,退出循环

        goto LABEL_15;

    }

  }

  else

  {

LABEL_15:

    // 所有线程都检查完毕,没有发现处于 'T'/'t' 状态的线程

    closedir(v2); // 关闭 task 目录

    return 0LL;   // 返回 0,表示安全,没有被调试

  }

}



  • 检测点分析

hook看一下该函数的返回值,结果是0,if判断的三个条件全为假,结果就意味着不会进入0x114b0函数。在0x1a574里面只看到检测了,并没有退出进程的操作。所以还得回到0x1a5b0里面

刚才分析了0x1a840,没发现进程退出的操作,往上分析,0x1a7d4处没做什么,接下来就来到了0x1a720处

有一个函数跳转,进入函数0x1b88c

进入函数0x1aee4,代码太多不贴了,基本上都是在做字符串解密的操作,但发现有一处

看一下这四个函数,先看0x1a940,以下是ai写的注释,总结就是打开byte_45020,遍历该目录下的所有文件,逐行读取每个文件的第一行内容。如果第一行内容中包含byte_4504A或者byte_45056,就会直接调用 exit(0) 强行关闭当前进程

DIR *sub_1A940()

{

  DIR *result; // x0,用于存放 opendir 返回的目录流指针

  DIR *v1; // x19,目录流指针的备份

  struct dirent *v2; // x0,指向读取到的目录项结构体

  struct dirent *v3; // x25,目录项结构体的备份

  const char *d_name; // x25,当前遍历到的文件/目录名称

  int v5; // w0,openat 返回的文件描述符 (fd)

  unsigned int v6; // w25,保存文件描述符 (fd)

  unsigned __int64 i; // x29,循环计数器,用于逐字节读取文件

  char v8[4]; // [xsp+14h] [xbp-46Ch] BYREF,单字节读取缓冲区(虽然大小是4,但每次只读1个字节)

  char haystack[512]; // [xsp+18h] [xbp-468h] BYREF,用于存放从文件中读取的第一行内容

  char s[520]; // [xsp+218h] [xbp-268h] BYREF,用于拼接完整的文件路径


  // 1. 读取系统寄存器,设置栈保护(Stack Canary),防止缓冲区溢出

  _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));

  

  // 2. 打开目标目录。byte_45020 是全局变量,存放目录路径(例如 "/proc/self/task" 或 "/proc/self/maps")

  result = opendir(byte_45020);

  

  if ( result ) // 如果目录打开成功

  {

    v1 = result; // 备份 DIR 指针

    v2 = readdir(result); // 读取目录下的第一个条目

    

    if ( v2 ) // 如果目录不为空

    {

      v3 = v2;

      do // 开始遍历目录

      {

        memset(s, 0, 0x200u); // 清空路径拼接缓冲区 s (512字节)

        d_name = v3->d_name;  // 获取当前遍历到的文件或文件夹名称

        

        // 3. 忽略当前目录 "." 和 父目录 ".."

        if ( strcmp(d_name, ".") )

        {

          if ( strcmp(d_name, "..") )

          {

            // 4. 拼接完整的文件路径。byte_45030 是格式化字符串 (例如 "%s/status" 或 "%s/cmdline")

            _snprintf_chk(s, 512LL, 0LL, 512LL, byte_45030, d_name);

            

            // 5. 打开拼接好的文件路径

            // -100 代表 AT_FDCWD (当前工作目录),这里结合绝对路径使用

            // 0x80000 通常代表 O_CLOEXEC 或 O_NOFOLLOW 等特定标志位,0LL是mode

            v5 = openat(-100, s, 0x80000, 0LL); 

            

            if ( v5 ) // 如果文件打开成功 (获取到有效的文件描述符 fd)

            {

              v6 = v5; // 备份文件描述符

              memset(haystack, 0, sizeof(haystack)); // 清空用于保存文件内容的缓冲区

              

              // 6. 逐字节读取文件内容,最多读取 511 字节 (0x1FF)

              for ( i = 0LL; i < 0x1FF; ++i )

              {

                // 每次只读取 1 个字节到 v8 中

                if ( _read_chk(v6, v8, 1LL, 1LL) != 1 )

                  break; // 如果读取失败或到达文件末尾(EOF),跳出读取循环

                  

                // 10 是换行符 '\n' 的 ASCII 码

                // 这说明它只想读取文件的【第一行】内容

                if ( v8[0] == 10 )

                  break; 

                  

                haystack[i] = v8[0]; // 将读取到的字节追加到 haystack 缓冲区中

              }

              

              // 7. 核心检测逻辑:字符串匹配

              // byte_4504A 和 byte_45056 是全局特征字符串 (极有可能是 "frida", "xposed", "magisk" 等)

              // 检查读取到的第一行内容中,是否包含这些被拉黑的敏感词

              if ( strstr(haystack, byte_4504A) || strstr(haystack, byte_45056) )

              {

                // 如果发现了敏感词,说明存在注入或调试环境

                exit(0); // 直接强制退出当前进程 (闪退)

              }

              

              close(v6); // 检查完毕,关闭当前文件

            }

          }

        }

        // 读取目录中的下一个条目

        v3 = readdir(v1);

      }

      while ( v3 ); // 循环直到遍历完目录中的所有文件

    }

    

    // 遍历结束后关闭目录,并强制转换返回值

    return (DIR *)closedir(v1);

  }

  

  // 如果一开始目录就打开失败,直接返回 0 (NULL)

  return result;

}


所以要看一下byte_45020,byte_4504A,byte_45056,haystack分别是什么

可以看到的是成功被检测了,于是进程退出。

再看0x1aaec

所以要知道byte_4505C,和strstr的两个参数。对了,由于上一个函数进行检测并退出了,所以代码不会走到现在这里,要想办法让代码走到现在这里,我直接把上一个函数exit(0)处替换成空函数了。

日志如下:

可以看到并没有检测成功,且没走到exit(0处)。

再来看下一个函数,即sub_1AC00,直接定位到exit(0)处,hook一下发现没走到这里

于是注意到他在else里面

那就hook一下if ( v20 == &v18 )里面,看看是不是走到了if ( v20 == &v18 )里面

好吧,既然没走到,就看下一个函数了。

可以看到该函数中没看到有啥检测,猜测在sub_232C4进行的检测。先不去看,先看看这个动态内存分配是啥。hook v5(0LL)处

妙啊,他不仅用了svc #0,还用了动态内存分配。就算想动态扫描内存段中的svc指令,他恰好也就退出了。

有少侠可能不知道svc #0,我贴一下ai讲的。


系统调用号网址:d44K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0K9s2u0G2L8h3W2#2L8g2)9J5k6h3N6G2L8$3N6D9k6i4y4G2N6i4u0U0k6g2)9J5k6h3y4G2L8g2)9J5c8X3y4Z5M7X3!0E0K9i4g2E0L8%4y4Q4x3V1k6V1L8$3y4K6i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4K6N6r3g2J5i4K6u0r3j5$3!0F1M7%4c8S2L8Y4c8K6i4K6u0r3M7%4W2K6j5$3q4D9L8s2y4Q4x3X3g2E0k6l9`.`.

简单来说就是,比如我想执行exit_group系统调用让进程退出,那么我可以直接

 mov x8, #0x5e

  svc #0

exit函数底层就是exit_group系统调用。


接下来去看sub_232C4,有点多,首先上来根据v1值的不同,他走了三个不同的分支



hook看一下,我的v1是0x1e,即十进制30,然后我想到我的系统是安卓11,api 即 30,那v1可能就是系统 API 级别。

那我直接去看if ( v1 >= 26 )分支了

最终会走到箭头处,然后跳到LABEL_36去执行,如果sub_17CD8的返回值右移1位等于0x2C000028(十进制为738197544)的话,那么就返回真,如果返回真,那么外层函数就会走到sv #0处退出进程。


hook一下sub_17CD8,看看入参和返回值

打印的有点看不懂,进入sub_17CD8看看,断点


接下来分析一下a1+328是从哪来的,发现是参数,那就往外找,可以看到是sub_17BC4的返回值

进入sub_17BC4看看,发现他new了v2,最后返回的也是v2,可以hook验证一下这里的v2+328也就是上面的a1+328,于是我们去找v1[1],看伪代码看不出来

看v1 = (char **)ptr[0];对应的汇编代码,x20是v1,那么x20+8就是v1[1]了,x20来自于[SP,#0x30+ptr],且注意到sp给到x8,然后调用了一个函数。

在 ARM64 的函数调用约定中,参数通常通过 X0X7 传递。而 X8 是一个非常特殊的寄存器,被称为 间接结果位置寄存器 。当一个函数需要返回一个大于 16 字节的结构体或对象时,它无法通过 X0 返回。此时,调用者会提前在自己的栈上分配好一块内存(这里就是 SP 指向的 ptr 数组),然后把这块内存的地址放在 X8 里传过去。


于是我们去看sub_179AC,伪代码看着太乱了,直接看汇编吧,框起来的是关键指令,x8给到x19, x0给到x19寄存器中的地址, x0给到栈顶, x8 x19给到x0寄存器中的地址,此时,x0中存放的是一个地址, 该地址处存放的是"libart.so"和一个对象(记为fd),再调用sub_17868


去看sub_17868

hook看看a1+24偏移处是哪个函数

去看0x17a4c,明了了,辛辛苦苦找的a1+328就是libart.so在内存中的的起始地址


然后回到sub_17CD8,有了a1+328=libart.so在内存中的的起始地址和sub_16D80的入参和出参。其实大致就可以猜出sub_16D80就相当于dlsym。(分析这个函数要对so文件的格式很了解,我目前火候还不够,抱歉)


而sub_17CD8的功能就是返回入参a2在内存的绝对地址,即libart.so中的PrettyMethod在内存的绝对地址。

可以hook验证一下,确实是这样。

然后在看到这里

*v0 >> 1 == 738197544相当于*v0==738197544<<1,即*v0==0x58000050,对应的汇编指令为LDR X16, [PC, #8]。

所以只要用frida或者其他inline hook,那么PrettyMethod函数前就会多出这么一个特征。就可以被检测到。

至于绕过,hook pthread_create然后把检测线程nop掉就可以,本篇主要以分析为主。


得到12.18.0


完结。


参考文章:https://bbs.kanxue.com/thread-268586.htm

https://bbs.kanxue.com/thread-289545-7.htm

https://bbs.kanxue.com/thread-277034.htm


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2天前 被伊苏尔德编辑 ,原因:
收藏
免费 12
支持
分享
最新回复 (5)
雪    币: 3916
活跃值: (2008)
能力值: (RANK:100 )
在线值:
发帖
回帖
粉丝
2
给文章规划一下目录结构就更好了
2天前
0
雪    币: 9
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
fyrlove 给文章规划一下目录结构就更好了
谢谢指正
2天前
0
雪    币: 197
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
在 ARM64 指令集中,机器码 0x58000050 对应的汇编指令确实是:
LDR X16, [PC, #0x8]

    这条指令的作用:将距离当前 PC 地址偏移 8 字节处的数据加载到 X16 寄存器中。

    为什么它是 Hook 的特征?:
    这是 Inline Hook 最经典的“跳板”(Trampoline)代码的开头。当你使用 Frida 或其他工具 Hook 一个函数时,工具会修改函数的前 8 或 16 字节,通常逻辑如下:
    ARM assembler

    LDR X16, #8     ; 对应机器码 0x58000050
    BR X16          ; 跳转到 X16 指向的地址
    .quad 0x1234... ; 这里存的是 Hook 后的目标地址
2天前
0
雪    币: 9
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
菜鸟9527 在 ARM64 指令集中,机器码 0x58000050 对应的汇编指令确实是: LDR X16, [PC, #0x8] 这条指令的作用:将距离当前 PC 地址偏移 8 字节处的数据加载 ...
谢谢指正
2天前
0
雪    币: 104
活跃值: (8432)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
tql
1天前
0
游客
登录 | 注册 方可回帖
返回