文章

尝试学习frida

尝试学习frida

尝试直接用frida看看能不能调试

1
frida -U -f com.leleketang.SchoolFantasy

预想之中, 所以得先反调试…啊, 好难啊,,360加固框架到底怎么反调试的??查看了一篇360加固分析的博客, 望而却步.. #2026年4月21日 先玩会儿吧.. 还是先弄懂frida对native层的调试接口吧. Interceptor是拦截器, 引出的问题是:

  • 插哪儿
  • 插完要做什么 学习这个博客的一个脚本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    function hook_dlopen(soName = '') {
      Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
          onEnter: function(args) {
              var pathptr = args[0];
              if (pathptr) {
                  var path = ptr(pathptr).readCString();
                  console.log("Loading: " + path);
                  if (path.indexOf(soName) >= 0) {
                      console.log("Already loading: " + soName);
                      // hook_system_property_get();
                  }
              }
          }
      });
    }
    setImmediate(hook_dlopen, "libmsaoaidsec.so");
    

    一看这是老版本的frida, 这个坑我踩过. 最新版api应该如下写

    1
    
    Module.getGlobalExportByName("android_dlopen_ext")
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function hook_dlopen(soName = '') {
    Interceptor.attach(Module.getGlobalExportByName("android_dlopen_ext"), {
        onEnter: function(args) {
            var pathptr = args[0];
            if (pathptr) {
                var path = ptr(pathptr).readCString();
                console.log("Loading: " + path);
                if (path.indexOf(soName) >= 0) {
                    console.log("Already loading: " + soName);
                    // hook_system_property_get();
                }
            }
        }
    });
}
setImmediate(hook_dlopen, "libmsaoaidsec.so");

真的看着好陌生的知识啊, 但是当初自学java也是这样一路摸索过来的, 我相信我自己能掌握这门技术. 我想看看android_dlopen_ext函数参数, 去看看对应的源码.

1
2
3
4
5
__attribute__((__weak__))
void* android_dlopen_ext(const char* filename, int flag, const android_dlextinfo* extinfo) {
  const void* caller_addr = __builtin_return_address(0);
  return __loader_android_dlopen_ext(filename, flag, extinfo, caller_addr);
}

问一下AI吧 https://www.qianwen.com/share/chat/a27f1b7eb6aa4dbea72d3c1b832e4d26 所以, 联系上面的脚本, 可以得知: attach只能是附加到对应的函数, 而onEnter里就取用了第一个参数args[0]: 加载路径. Module.getGlobalExportByName("android_dlopen_ext")的作用是找到这个函数在内存中的具体位置? 问AI核对一下.. 确实如此.. 接着的参数就是hook后的回调函数.好多未知的知识啊,疑问太多了.. 去看具体的Frida文档吧. 一句一句学! 不然学得模模糊糊的, 没有一点进展..

To be more productive, we highly recommend using our TypeScript bindings. This means you get code completion, type checking, inline docs, refactoring tools, etc.

  • 为了更高效, 我们强烈推荐使用我们的TypeScript配套. 这意味着你获得了代码补全, 类型检查, 内联文档, 重构工具等.

原来我一开始就错了, 对js有着偏爱情怀, 但是我决定改变, 我接受不了一点js的无类型判定! 我先去试试ts咋用的. 查看到一篇博客, 感觉像是打开了新大陆.. 确实, 自己的脚本对于大部分app应该是通用的, 重复编写浪费事件, 确实应该变成ide中可调用的模块.. 并且我电脑上也安装了node.js, 配置起来环境应该不是问题. visual studio code 的 自动补全就像糊弄鬼一样, 补全了, 但没有完全补全.. 博客里的ide是pycharm, 我不太熟悉, damn!! 才发现后面几句话就说了, 有模板库..

Clone this repo to get started.

那还说啥了, Git 启动!!

1
git clone https://github.com/oleavr/frida-agent-example.git

然后我尝试用IDEA打开..打开后, idea会自动提示: 发现package.json, 是否需要npm安装. 然后就没有报错了..随便一写, 嘿, 还真能主动提示了.. 真棒, 今天又学了新东西, 还可以利用ts的模块功能, 不用重复写脚本咯..接着看文档吧.

Table of contents

  1. Runtime information运行时信息
    1. FridaFrida
    2. Script脚本
  2. Process, Thread, Module and Memory进程, 线程, 模块, 内存
    1. Thread线程
    2. Process进程
    3. Module模块
    4. ModuleMap模块映射
    5. Memory内存
    6. MemoryAccessMonitor内存访问监控器
    7. CModuleC模块
    8. RustModuleRust模块
    9. ApiResolverAPI解释器
    10. DebugSymbol调试符号
    11. Kernel内核
  3. Data Types, Function and Callback数据类型, 函数, 回调
    1. Int6464位整型
    2. UInt64无符号64位整型
    3. NativePointerNative层指针
    4. ArrayBuffer队列缓冲
    5. NativeFunctionNative函数
    6. NativeCallbackNative回调
    7. SystemFunction符号函数
  4. Network网络
    1. Socket套接字
    2. SocketListener套接字监听器
    3. SocketConnection套接字连接
  5. File and Stream文件和流
    1. File文件
    2. IOStream输入输入流
    3. InputStream输入流
    4. OutputStream输出流
    5. UnixInputStreamUnix系统的输入流
    6. UnixOutputStreamUnix系统的输出流
    7. Win32InputStreamWin32系统的输入流
    8. Win32OutputStreamWin32系统的输出流
  6. Database数据库
    1. SqliteStatement数据库状态
  7. Instrumentation插桩
    1. Interceptor拦截器
    2. Stalker追踪器
    3. ObjC对象C
    4. Java对象Java
  8. CPU InstructionCPU工具
    1. X86Writerx86架构写入器
    2. X86Relocatorx86架构重定位器
    3. x86 enum typesx86架构枚举类型
    4. ArmWriterArm架构写入器
    5. ArmRelocatorArm架构重定位器
    6. ThumbWriterThumb架构写入器
    7. ThumbRelocatorThumb架构重定位器
    8. ARM enum typesArm架构枚举类型
    9. Arm64WriterArm64架构写入器
    10. Arm64RelocatorArm64架构重定位器
    11. AArch64 enum typesAArch64架构枚举类型
    12. MipsWriterMIPS架构写入器
    13. MipsRelocatorMIPS架构重定位器
    14. MIPS enum typesMIPS架构枚举类型
  9. Other其他
    1. Console控制台
    2. Hexdump16进制内存打印

#2026年4月22日

Frida

Frida.version

property containing the current Frida version, as a string.

  • 包含了当前Frida版本的属性, String类型

立马上手试试. 就选择MT管理器吧

1
frida -U -f bin.mt.plus -l agent\index.ts

Frida.heapSize

dynamic property containing the current size of Frida’s private heap, shared by all scripts and Frida’s own runtime. This is useful for keeping an eye on how much memory your instrumentation is using out of the total consumed by the hosting process.

  • 包含了当前Frida私有堆内存大小的动态属性, 共享于所有脚本及Frida的运行时. 有助于监测你的操作在宿主内存中总共占用了多少.

看了一下logger.js里面的导出函数

1
2
3
export function log(message: string): void {
    console.log(message);
}

案例代码里面封装的log不太行嘞, 直接用console.log才行..

我就这么一点脚本代码, 也占用这么多内存? 2-769-936bit? 大约2MB..算了管他呢..

Script

Script.runtime

string property containing the runtime being used. Either QJS or V8.

  • 包含了正在被使用的运行时, 要么QJS,要么V8.

QJS来路这么大嘛, 爱了爱了..

Script.evaluate(name, source)

evaluates the given JavaScript string source in the global scope, where name is a string specifying the script’s name, e.g. /plugins/tty.js. The provided name is a UNIX-style virtual filesystem path used in future stack traces.

  • 在全局作用域中执行给定的 JavaScript 代码字符串 source,其中 name 是一个指定脚本名称的字符串(例如 /plugins/tty.js)。该名称是一个类 Unix 虚拟文件系统路径,将用于后续的堆栈跟踪(stack traces)中。

这个我还不知道使用场景.. 这个name就是路径嘛? 是调试机上面的还是被调试的?是绝对的还是相对的? source具体又是什么?

插播一个错误: 文章内md格式的内部链接不能被jekyll顺利解析, 所以我还是删掉吧

1
Script.evaluate(name, source)

回到正题, AI的解释: name仅类似于起到一个标签作用, 只是让这个被执行的脚本能被溯源.. 真正的脚本位于后面的source,里面是js代码(或ts?),试试吧..

1
Script.evaluate("/virtual/path/to/script.js", 'console.log("Here is a piece of shit")');

脚本确实正常执行了..去看看有没有对应的目录

文件系统是没有滴..好了这个API就不深入理解了.

Useful for agents that want to support loading user-provided scripts inside their own script. The two benefits over simply using eval() is that the script filename can be provided, and source maps are supported — both inline and through Script.registerSourceMap().

  • 对于希望在脚本内部加载用户提供脚本的代理是有用的. 相比于简单实用eval()的两个优点是提供了脚本名称, 而且支持资源映射, 两者都是通过Script.registerSourceMap()内联的.

Returns the resulting value of the evaluated code.

  • 返回被执行代码的结果的值.

    Script.load(name, source)

    compiles and evaluates the given JavaScript string source as an ES module, where name is a string specifying the module’s name, e.g. /plugins/screenshot.js. The provided name is a UNIX-style virtual filesystem path used in future stack traces, and is visible to other modules, which may import it, either statically or dynamically.

  • 视为ES模型来编译执行source中的JavaScript字符串, name在这里是模块名称, 例如/plugins/screenshot.js. 这个名称是一个UNIX风格的虚拟系统路径, 用于将来的栈回溯, 并且对于其他无论是静态或动态引入它的模块可见.

也就是说, 如果我并没有把ts脚本写在文件系统上, 而是内存里, 依然能通过name来引入使用. 我想试试..先直接孵化时加载脚本

1
2
3
Script.evaluate("/virtual/path/to/vlog.js", 'export function log(message:any): void {\n' +
    '    console.log(message);\n' +
    '}');

哦吼? 难道是js里面不能又export这样的字符? 改改试试..

原来是函数名写错了..loadevaluate确实不一样哈, 一个是加载待用, 一个是立即执行.

如何验证呢? 我再创建一个ts文件, 在控制台加载就行了吧?

编写时已经开始报错了, 确实这个路径是找不到模块滴. frida控制台加载试试..

似乎并不能被正确识别..AI让我直接在index.ts中加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// agent/index.ts

// 1. 先注册 vlog 模块
Script.load("/virtual/path/to/vlog.ts", `
export function log(message: any): void {
  console.log("[LOG]", message);
}
`);

// 2. 再加载使用该模块的脚本(注意:source 是字符串!)
Script.load("/agent/load_test.js", `
import { log } from "/virtual/path/to/vlog.ts";
log("This is another piece of shit.");
`);

确实没报错, 但也没任何输出啊,,

根据AI的建议, 换一份代码..

1
2
3
4
5
6
7
8
9
10
Script.load("/lib/log.js", `
export function log(msg) {
  console.log(">>>", msg);
}
`);

Script.load("/main.js", `
import { log } from "/lib/log.js";
log("Hello from module!");
`);

居然被正确执行了, 看来load仅支持js代码而不是ts..有点鸡肋, 我为何不老老实实用 -l index.ts%load agent/index.ts

Useful for agents that want to support loading user-provided scripts inside their own script. This API offers the same benefits over eval() as Script.evaluate(), in addition to encapsulating the user-provided code in its own ES module. This means values may be exported, and subsequently imported by other modules. The parent script may also export values that can be imported from the loaded child script. This requires that the parent uses the new ES module bundle format used by newer versions of frida-compile.

  • 对于想要在代码中加载用户提供代码的代理有利, 相比于eval(),这个API同Script.evaluate()一样,提供相同的优点, 额外地把用户提供的代码囊括进它自己ES模块. 这意味着值会被导出, 随后被其他模块导入. 父级脚本也能导出由子级模块导入的值. 这需要父级脚本使用由较新版本frida编译配套的ES模块

Returns a Promise that resolves to the module’s namespace object.

返回一个处理模块命名空间对象的Promise…如图. IDE已经提示返回值了

Script.registerSourceMap(name, json)

registers a source map for the specified script name, given as a string with a UNIX-style virtual filesystem path, e.g. /plugins/screenshot.js. The source map json is a string containing the raw JSON representation of the source map. Should ideally be called before the given script gets loaded, so stack traces created during load can make use of the source map.

  • 为指定的脚本名称注册源表, 名称是给定的带着UNIX风格的虚拟文件系统路径的字符串, 例如/plugins/screenshot.js.源表json是一个包含代表源表的一行JSON格式的字符串. 应该在指定脚本被加载前被有意调用, 因此在加载期间创建的栈追踪能使用源表.

这个api也就需要调试ts代码时才能用, 对于现在的我来说没用…不学不学..

Script.nextTick(func[, ...params])

runs func on the next tick, i.e. when the current native thread exits the JavaScript runtime. Any additional params are passed to it.

  • 下一个时钟周期运行func, 例如, 当现在的native线程退出了JavaScript运行时.

AI: Script.nextTick(func[, ...params]) 是 Frida 提供的一个异步调度工具,它的核心作用是:将函数 func 的执行推迟到“当前 JavaScript 执行上下文结束后、但仍在同一线程中”的下一个事件循环 tick 中运行

我实在想不出来应用场景, 而且我的翻译不太搭边..

Script.pin()

temporarily prevents the current script from being unloaded. This is reference-counted, so there must be one matching unpin() happening at a later point. Typically used in the callback of bindWeak() when you need to schedule cleanup on another thread.

  • 临时阻止当前的脚本被卸载. 这是引用-计数的, 所以后续会有一个匹配的unpin()发生.

Script.unpin()

reverses a previous pin() so the current script may be unloaded.

  • 逆转前一个pin()让当前的脚本能被卸载.

Script.bindWeak(value, fn)

monitors value and calls the fn callback as soon as value has been garbage-collected, or the script is about to get unloaded. Returns an ID that you can pass to Script.unbindWeak() for explicit cleanup.

  • 监控value这个值, 然后调用fn这个回调函数, 当value已经被垃圾回收GC了, 或者脚本即将被卸载时. 返回一个ID让你传递给Script.unbindWeak()来彻底清除.

对于现阶段的我来说, 用处不大

This API is useful if you’re building a language-binding, where you need to free native resources when a JS value is no longer needed.

  • 这个API是有用的, 如果你正在构建一个语言绑定的环境, 在这里你需要释放native资源当一个JS值不再被需要的时候.

AI的翻译比我的看起来顺眼…

  • 如果你正在开发一种语言绑定(language binding),当某个 JavaScript 值不再被需要时,需要释放其对应的原生(native)资源,那么这个 API 就非常有用。

看样子是主句中的各种从句都往前提了,

  1. if
  2. where
  3. when

按照原来的顺序, 如果翻译为:

  • 如果你正在开发一种语言绑定(language binding),需要释放其对应的原生(native)资源,当某个 JavaScript 值不再被需要时,那么这个 API 就非常有用。

就很不顺口, 看来今后的翻译, 次序应该是 条件状语 > 事件状语 > 地点状语.

Script.unbindWeak(id)

stops monitoring the value passed to Script.bindWeak(value, fn), and call the fn callback immediately.

  • 停止监听传递给Script.bindWeak(value,fn)的值, 然后立即调用fn这个回调函数.

Script.setGlobalAccessHandler(handler | null)

installs or uninstalls a handler that is used to resolve attempts to access non-existent global variables. Useful for implementing a REPL where unknown identifiers may be fetched lazily from a database.

  • 安装或卸载用于解析访问不存在的全局变量的意图的句柄. 实现REPL时有用, 从数据库懒加载出未知符号.

AI还是更胜一筹…

  • 安装或卸载一个处理器(handler),用于拦截并处理对不存在的全局变量的访问。在实现 REPL 时特别有用,例如可以按需从数据库中懒加载未知标识符。

就是防止空指针咯.. 如果加载到了空的不存在的变量, 就会跳到handler处进行处理?? 可是我现在还到不了在frida中使用数据库的情况, 后续再回顾这一块知识吧..

The handler is an object containing two properties:

  • handler是一个包含两个属性的对象:

enumerate(): queries which additional globals exist. Must return an array of strings.

  • enumerate(): 查询存在的全局变量. 一定返回一个字符串数组.

get(property): retrieves the value for the given property.

  • get(property): 读取给定属性的值

具体的handle咋用我不想实践了, 感觉用处不大, 今后在弄这一块..

Process

Process.id

property containing the PID as a number

Process.arch

property containing the string ia32, x64, arm or arm64

Process.platform

property containing the string windows, darwin, linux, freebsd, qnx, or barebone

Process.pageSize

property containing the size of a virtual memory page (in bytes) as a number. This is used to make your scripts more portable.

Process.pointerSize

property containing the size of a pointer (in bytes) as a number. This is used to make your scripts more portable.

Process.codeSigningPolicy

property containing the string optional or required, where the latter means Frida will avoid modifying existing code in memory and will not try to run unsigned code. Currently this property will always be set to optional unless you are using Gadget and have configured it to assume that code-signing is required. This property allows you to determine whether the Interceptor API is off limits, and whether it is safe to modify code or run unsigned code.

Process.mainModule

property containing a Module representing the main executable of the process

Process.getCurrentDir()

returns a string specifying the filesystem path to the current working directory

Process.getHomeDir()

returns a string specifying the filesystem path to the current user’s home directory

Process.getTmpDir()

returns a string specifying the filesystem path to the directory to use for temporary files

Process.isDebuggerAttached()

returns a boolean indicating whether a debugger is currently attached

Process.getCurrentThreadId()

get this thread’s OS-specific id as a number

Process.enumerateThreads()

enumerates running threads, returning an array of Thread objects.

Process.attachThreadObserver(callbacks)

starts observing threads, calling the provided callbacks as threads are added, removed, and renamed.

The callbacks argument is an object containing one or more of:

  • onAdded(thread): callback function given the Thread that was just added. Called with all existing threads right away, so the initial state vs. updates can be managed easily without worrying about race conditions. When called with a brand new thread, the call happens synchronously from that new thread.
  • onRemoved(thread): callback function given the Thread that was just removed, i.e. is about to terminate. The call happens synchronously from the thread that is about to terminate.
  • onRenamed(thread, previousName): callback function given the Thread that was just renamed, with its new name property, and a second argument previousName that specifies its previous name. The previous name is either a string, or null if the thread was previously unnamed.

Note that the Thread objects lack the state and context properties, as those are highly volatile in nature, and their changes are not observed. Note that you can combine this API with Stalker to trace the execution of individual threads.

Returns an observer object that you can call detach() on.

Process.runOnThread(id, callback)

runs the JavaScript function callback without any arguments, on the thread specified by id. Returns a Promise that receives the value returned by your callback.

Must be used with extreme caution due to the thread potentially being interrupted in non-reentrant code. For example, you could be interrupting it while it’s in the middle of some delicate code, holding a specific non-recursive lock, which you then try to implicitly acquire again when you call some function.

Process.findModuleByAddress(address), Process.getModuleByAddress(address), Process.findModuleByName(name), Process.getModuleByName(name)

returns a Module whose address or name matches the one specified. In the event that no such module could be found, the find-prefixed functions return null whilst the get-prefixed functions throw an exception.

Process.enumerateModules()

enumerates modules loaded right now, returning an array of Module objects.

Process.attachModuleObserver(callbacks)

starts observing modules, calling the provided callbacks as modules are added and removed.

The callbacks argument is an object containing one or more of:

  • onAdded(module): callback function given the Module that was just added. Called with all existing modules right away, so the initial state vs. updates can be managed easily without worrying about race conditions. When called with a brand new module, the call happens synchronously right after that module has been loaded, but before the application has had a chance to use it. This means it’s a good time to apply your instrumentation, using e.g. Interceptor.
  • onRemoved(module): callback function given the Module that was just removed, i.e. unloaded.

Returns an observer object that you can call detach() on.

Process.findRangeByAddress(address), getRangeByAddress(address)

return an object with details about the range containing address. In the event that no such range could be found, findRangeByAddress() returns null whilst getRangeByAddress() throws an exception. See Process.enumerateRanges() for details about which fields are included.

Process.enumerateRanges(protection|specifier)

enumerates memory ranges satisfying protection given as a string of the form: rwx, where rw- means “must be at least readable and writable”. Alternatively you may provide a specifier object with a protection key whose value is as aforementioned, and a coalesce key set to true if you’d like neighboring ranges with the same protection to be coalesced (the default is false; i.e. keeping the ranges separate). Returns an array of objects containing the following properties:

  • base: base address as a NativePointer
  • size: size in bytes
  • protection: protection string (see above)
  • file: (when available) file mapping details as an object containing:
  • path: full filesystem path as a string
  • offset: offset in the mapped file on disk, in bytes
  • size: size in the mapped file on disk, in bytes

Process.enumerateMallocRanges()

just like enumerateRanges(), but for individual memory allocations known to the system heap.

Process.setExceptionHandler(callback)

install a process-wide exception handler callback that gets a chance to handle native exceptions before the hosting process itself does. Called with a single argument, details, that is an object containing:

  • type: string specifying one of:
  • abort
  • access-violation
  • guard-page
  • illegal-instruction
  • stack-overflow
  • arithmetic
  • breakpoint
  • single-step
  • system
  • address: address where the exception occurred, as a NativePointer
  • memory: if present, is an object containing:
  • operation: the kind of operation that triggered the exception, as a string specifying either read, write, or execute
  • address: address that was accessed when the exception occurred, as a NativePointer
  • context: object with the keys pc and sp, which are NativePointer objects specifying EIP/RIP/PC and ESP/RSP/SP, respectively, for ia32/x64/arm. Other processor-specific keys are also available, e.g. eax, rax, r0, x0, etc. You may also update register values by assigning to these keys.
  • nativeContext: address of the OS and architecture-specific CPU context struct, as a NativePointer. This is only exposed as a last resort for edge-cases where context isn’t providing enough details. We would however discourage using this and rather submit a pull-request to add the missing bits needed for your use-case.

It is up to your callback to decide what to do with the exception. It could log the issue, notify your application through a send() followed by a blocking recv() for acknowledgement of the sent data being received, or it can modify registers and memory to recover from the exception. You should return true if you did handle the exception, in which case Frida will resume the thread immediately. If you do not return true, Frida will forward the exception to the hosting process’ exception handler, if it has one, or let the OS terminate the process.

Thread

Objects returned by e.g. Process.enumerateThreads().

  • id: OS-specific id, as a number
  • name: string specifying the thread’s name, if available
  • state: snapshot of the thread’s state, as a string specifying either running, stopped, waiting, uninterruptible, or halted
  • context: snapshot of CPU registers, as an object with the keys pc and sp, which are NativePointer objects specifying EIP/RIP/PC and ESP/RSP/SP, respectively, for ia32/x64/arm. Other processor-specific keys are also available, e.g. eax, rax, r0, x0, etc.
  • entrypoint: where the thread started its execution, if applicable and available. When present, it’s an object containing:
  • parameter: parameter passed to routine, if available, as a NativePointer

setHardwareBreakpoint(id, address)

sets a hardware breakpoint, where id is a number specifying the breakpoint ID, and address is a NativePointer specifying the address of the breakpoint. Typically used in conjunction with Process.setExceptionHandler() to handle the raised exceptions.

unsetHardwareBreakpoint(id)

unsets a hardware breakpoint, where id is a number specifying the breakpoint ID previously set by calling setHardwareBreakpoint().

setHardwareWatchpoint(id, address, size, conditions)

sets a harware watchpoint, where id is a number specifying the watchpoint ID, address is a NativePointer specifying the address of the region to be watched, size is a number specifying the size of that region, and conditions is a string specifying either r, w, or rw. Here, r means to watch for reads, w means to watch for writes, and rw means to watch for both reads and writes. Typically used in conjunction with Process.setExceptionHandler() to handle the raised exceptions.

unsetHardwareWatchpoint(id)

unsets a hardware watchpoint, where id is a number specifying the watchpoint ID previously set by calling setHardwareWatchpoint().

Thread.backtrace([context, backtracer])

generate a backtrace for the current thread, returned as an array of NativePointer objects.

If you call this from Interceptor’s onEnter or onLeave callbacks you should provide this.context for the optional context argument, as it will give you a more accurate backtrace. Omitting context means the backtrace will be generated from the current stack location, which may not give you a very good backtrace due to the JavaScript VM’s stack frames. The optional backtracer argument specifies the kind of backtracer to use, and must be either Backtracer.FUZZY or Backtracer.ACCURATE, where the latter is the default if not specified. The accurate kind of backtracers rely on debugger-friendly binaries or presence of debug information to do a good job, whereas the fuzzy backtracers perform forensics on the stack in order to guess the return addresses, which means you will get false positives, but it will work on any binary. The generated backtrace is currently limited to 16 frames and is not adjustable without recompiling Frida.

1
2
3
4
5
6
7
8
9
const commonCrypto = Process.getModuleByName('libcommonCrypto.dylib');
const f = commonCrypto.getExportByName('CCCryptorCreate');
Interceptor.attach(f, {
  onEnter(args) {
    console.log('CCCryptorCreate called from:\n' +
        Thread.backtrace(this.context, Backtracer.ACCURATE)
        .map(DebugSymbol.fromAddress).join('\n') + '\n');
  }
});

Thread.sleep(delay)

suspend execution of the current thread for delay seconds specified as a number. For example 0.05 to sleep for 50 ms.

Module

Objects returned by e.g. Module.load() and Process.enumerateModules().

  • name: canonical module name as a string
  • size: size in bytes
  • path: full filesystem path as a string

ensureInitialized()

ensures that the module initializers have been run. This is important during early instrumentation, i.e. run early in the process lifetime, to be able to safely interact with APIs. One such use-case is interacting with ObjC classes provided by a given module.

enumerateImports()

enumerates imports of module, returning an array of objects containing the following properties:

  • type: string specifying either function or variable -name: import name as a string -module: module name as a string -address: absolute address as a NativePointer -slot: memory location where the import is stored, as a NativePointer

Only the name field is guaranteed to be present for all imports. The platform-specific backend will do its best to resolve the other fields even beyond what the native metadata provides, but there is no guarantee that it will succeed.

enumerateExports()

enumerates exports of module, returning an array of objects containing the following properties:

-type: string specifying either function or variable -name: export name as a string

enumerateSymbols()

enumerates symbols of module, returning an array of objects containing the following properties:

  • isGlobal: boolean specifying whether symbol is globally visible
  • type: string specifying one of:
    • unknown
    • section
    • undefined (Mach-O)
    • absolute (Mach-O)
    • prebound-undefined (Mach-O)
    • indirect (Mach-O)
    • object (ELF)
    • function (ELF)
    • file (ELF)
    • common (ELF)
    • tls (ELF)
  • section: if present, is an object containing: - id: string containing section index, segment name (if applicable) and section name – same format as r2’s section IDs - protection: protection like in Process.enumerateRanges()
  • name: symbol name as a string
  • size: if present, a number specifying the symbol’s size in bytes

enumerateSymbols()

is only available on i/macOS and Linux-based OSes

We would love to support this on the other platforms too, so if you find this useful and would like to help out, please get in touch. You may also find the DebugSymbol API adequate, depending on your use-case.

enumerateRanges(protection)

just like Process.enumerateRanges, except it’s scoped to the module.

enumerateSections()

enumerates sections of module, returning an array of objects containing the following properties:

  • id: string containing section index, segment name (if applicable) and section name – same format as r2’s section IDs
  • name: section name as a string
  • address: absolute address as a NativePointer
  • size: size in bytes

enumerateDependencies()

enumerates dependencies of module, returning an array of objects containing the following properties:

  • name: module name as a string
  • type: string specifying one of:
    • regular
    • weak
    • reexport
    • upward

findExportByName(name), getExportByName(name)

returns the absolute address of the export named name. In the event that no such export could be found, the find-prefixed function returns null whilst the get-prefixed function throws an exception.

findSymbolByName(name), getSymbolByName(name)

returns the absolute address of the symbol named name. In the event that no such symbol could be found, the find-prefixed function returns null whilst the get-prefixed function throws an exception.

Module.load(path)

loads the specified module from the filesystem path and returns a Module object. Throws an exception if the specified module cannot be loaded.

Module.findGlobalExportByName(name), Module.getGlobalExportByName(name)

returns the absolute address of the global export named name. This can be a costly search and should be avoided. In the event that no such export could be found, the find-prefixed function returns null whilst the get-prefixed function throws an exception.

ModuleMap

Memory

MemoryAccessMonitor

CModule

RustModule

ApiResolver

DebugSymbol

Kernel

Interceptor

Interceptor.attach(target, callbacks[, data])

intercept calls to function at target. This is a NativePointer specifying the address of the function you would like to intercept calls to. Note that on 32-bit ARM this address must have its least significant bit set to 0 for ARM functions, and 1 for Thumb functions. Frida takes care of this detail for you if you get the address from a Frida API (for example Module#getExportByName()).

  • 拦截对位于 target 处的函数的调用。这是一个native指针, 指定你想拦截调用的函数地址. 注意在32位ARM架构上, 对于ARM函数, 地址必须有它至少有效位设置为0, 对于Thumb函数则设置为1. 如果你从Frida的API(例如Module#getExportByName())获取到地址, Frida则会自动处理这个细节.

AI:

  • 拦截对位于 target 处的函数的调用。这是一个 NativePointer,用于指定你想要拦截调用的函数地址。注意:在 32 位 ARM 架构上,该地址的最低有效位(LSB)必须根据函数类型进行设置 ——对于 ARM 模式 的函数,最低位应为 0;对于 Thumb 模式 的函数,最低位应为 1。如果你通过 Frida 的 API(例如 Module.getExportByName())获取地址,Frida 会自动处理这一细节。
特性ARM 指令集Thumb 指令集
指令长度固定 32 位主要是 16 位(Thumb-2 支持 16/32 位混合)
性能更高(功能全)略低,但更省空间
内存占用较大减少约 30%~40%
适用场景高性能计算嵌入式设备、移动设备(如早期 Android)

这里我倒是想实践一下…要不就弄fopen这个函数吧…监测内存映射就是通过这个函数读取内核给的数据..

1
fopen("/proc/self/status", "r");
1
2
3
4
5
6
7
8
9
10
11
let nativePointer = Module.getGlobalExportByName("fopen");
console.log("nativePointer: " + nativePointer)
Interceptor.attach(nativePointer, {
    onEnter: function(args) {
        console.log("fopen called")
        console.log("filename: " + args[0].readUtf8String() + " mode: " + args[1].readUtf8String())
    },
    onLeave: function(retval) {
        console.log("fopen returned: " + retval)
    }
})

日志是

1
2
3
4
5
6
7
   . . . .   Connected to PKG110 (id=o.a:5555)
Compiled agent\t.ts (354 ms)                       
nativePointer: 0x7f6ff8b024
Spawned `com.leleketang.SchoolFantasy`. Resuming main thread!           
[PKG110::com.leleketang.SchoolFantasy ]-> fopen called
filename: /proc/self/cmdline mode: re
fopen returned: 0xb400007eea410018

调试线程确实是在某次读取map文件后退出的.

这次是status文件

我想篡改返回值嘞.. 因为status里面有tracepid, map里有匿名可执行内存, 这些都容易暴露该软件处于调试状态..所以接下来我要先拿到返回值..

原来fopen只是拿到了一个指针, 并不是直接的文件内容, 要修改文件内容, 应该拦截fread/fgetsread, 我测试的app源码如下:

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
extern "C" JNIEXPORT jstring JNICALL
Java_cn_medmi_antidebug_MainActivity_getTracerPid(JNIEnv *env, jclass clazz) {
    FILE* fd = fopen("/proc/self/status", "r");
    if (fd == nullptr) {
        return env->NewStringUTF("Error: Could not open file");
    }

    char line[256];  // 每行不会太长
    while (fgets(line, sizeof(line), fd)) {
        if (strncmp(line, "TracerPid:", 10) == 0) {
            // 找到了TracerPid行
            char* value_start = strchr(line, ':');
            if (value_start != nullptr) {
                value_start++; // 跳过冒号
                // 跳过可能的空白字符
                while (*value_start == ' ' || *value_start == '\t') {
                    value_start++;
                }
                // 去除行尾的换行符
                char* newline = strchr(value_start, '\n');
                if (newline != nullptr) {
                    *newline = '\0';
                }
                fclose(fd);
                __android_log_write(ANDROID_LOG_INFO, "TracerPid", value_start);
                return env->NewStringUTF(value_start);
            }
        }
    }
    fclose(fd);
    return env->NewStringUTF("0");  // 默认值,表示没有调试器
}

所以先拦截fgets吧, 分析函数签名如下,

1
char *fgets(char *str, int size, FILE *stream);
参数类型说明
strchar *目标缓冲区:指向你提供的字符数组(buffer),用于存储读取到的字符串
sizeint最大读取字符数:最多读取 size - 1 个字符(留一个位置给结尾的空字符 \0)。这是为了防止缓冲区溢出
streamFILE *输入流:要从中读取数据的文件指针,例如 stdin(标准输入)、fopen 返回的文件句柄等

该函数的返回值不包含读取到的结果, 而是函数执行完毕后, 会在输入的缓冲区产生结果, AI教我要在onLeave时读取或修改缓冲区的数据..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let fopen = Module.getGlobalExportByName("fopen");
Interceptor.attach(fopen, {
    onEnter: function(args) {
        console.log("open: " + args[0].readUtf8String() + " mode: " + args[1].readUtf8String())
    },
})
let fgets = Module.getGlobalExportByName("fgets");
Interceptor.attach(fgets, {
    onEnter: function(args) {
        this.buffer=args[0];
    },
    onLeave: function(retval) {
        console.log("buffer: " + this.buffer.readUtf8String())
    }
})

日志如下

1
2
3
4
5
6
7
8
9
10
open: /proc/self/maps mode: r
...
buffer: 7e3375b000-7e34e21000 r-xp 00000000 00:01 201650                         /memfd:frida-agent-64.so (deleted)
buffer: 7e34e21000-7e34e24000 ---p 00000000 00:00 0
buffer: 7e34e24000-7e34ef7000 r--p 016c5000 00:01 201650                         /memfd:frida-agent-64.so (deleted)
buffer: 7e34ef7000-7e34efb000 ---p 00000000 00:00 0
buffer: 7e34efb000-7e34f16000 rw-p 01798000 00:01 201650                         /memfd:frida-agent-64.so (deleted)
...
Process terminated
[PKG110::com.leleketang.SchoolFantasy ]->

读取到/proc/self/maps后, 调试线程被杀死了… 问一下AI这一堆日志里能看到什么

AI发现了frida-agent注入的痕迹.. 然后让AI教我生成修改返回值的代码.

1
2
3
4
5
6
7
8
9
10
11
12
let fgets = Module.getGlobalExportByName("fgets");  
Interceptor.attach(fgets, {  
    onEnter: function(args) {  
        this.buffer=args[0];  
    },  
    onLeave: function(retval) {  
        let src = this.buffer.readUtf8String();  
        let mod = src.replace(/frida/g,'fucky');  
        this.buffer.writeUtf8String(mod)  
        console.log("buffer: " + mod)  
    }  
})

但是还是闪退, 似乎还存在别的检测, 在这篇博客里面有所提及. 可以通过读取proc/self/task/%s/status检测特征字符gum-js-loop gmain. 先看看有没有..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
open: /proc/self/cmdline mode: re
open: /proc/self/cmdline mode: re
open: /proc/self/cmdline mode: re
open: /proc/self/cmdline mode: re
open: /proc/self/status mode: r
open: /proc/self/maps mode: r
open: /proc/self/maps mode: r
open: /proc/self/maps mode: r
open: /proc/self/maps mode: r
open: /proc/self/maps mode: r
open: /proc/self/maps mode: r
open: /proc/self/maps mode: r
open: /proc/self/maps mode: r
open: /proc/self/maps mode: r
open: /proc/22854/cmdline mode: r
open: /proc/self/maps mode: r
open: /apex/com.android.art/lib64/libart.so mode: rbe
open: /proc/22854/cmdline mode: r
open: /proc/22854/status mode: r
Process terminated

并没有… 那接着修改status里的返回值..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[PKG110::com.leleketang.SchoolFantasy ]-> open: /proc/self/cmdline mode: re
open: /proc/self/cmdline mode: re
open: /proc/self/cmdline mode: re
open: /proc/self/cmdline mode: re
open: /proc/self/status mode: r
buffer: TracerPid:      0
open: /proc/self/maps mode: r
open: /proc/self/maps mode: r
open: /proc/self/maps mode: r
open: /proc/self/maps mode: r
open: /proc/self/maps mode: r
open: /proc/self/maps mode: r
open: /proc/self/maps mode: r
open: /proc/self/maps mode: r
open: /proc/self/maps mode: r
open: /proc/26261/cmdline mode: r
open: /proc/self/maps mode: r
open: /apex/com.android.art/lib64/libart.so mode: rbe
open: /proc/26261/cmdline mode: r
open: /proc/26261/status mode: r
buffer: TracerPid:      0
Process terminated

额, 似乎status也没露出鸡脚, 看来监测点也不在这里… 那到底为什么调试线程会被终止呢? hook了一下android_dlopen_ext, 出现如下日志:

1
2
3
4
5
6
7
8
[PKG110::com.leleketang.SchoolFantasy ]-> android_dlopen_ext called: [libframework-connectivity-tiramisu-jni.so]
android_dlopen_ext called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/oat/arm64/base.odex]
android_dlopen_ext called: [/data/data/com.leleketang.SchoolFantasy/.jiagu/libjiagu_64.so]
android_dlopen_ext called: [libjgdtc.so]
android_dlopen_ext called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libjgdtc.so]
android_dlopen_ext called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so]
Process terminated
[PKG110::com.leleketang.SchoolFantasy ]->

这个libmsaoaidsec.so是老演员了啊, 好多博客都能看到这个frida检测模块, 我居然认为天天练没有… 这篇博客中, 爱奇艺也用到了这个检测模块,

我想偷懒直接hook do_dlopen函数, 但是不出意外地出错了, 可能这是内部实现, 不同版本的函数名,内容,参数不一样…

1
2
3
4
5
Interceptor.attach(Module.getGlobalExportByName("do_dlopen"), {  
    onEnter: function(args) {  
        console.log("android_dlopen_ext called: [" + args[0].readUtf8String()+"]")  
    },  
})
1
2
3
4
5
Spawned `com.leleketang.SchoolFantasy`. Resuming main thread!       
Error: unable to find global export 'do_dlopen'
    at value (/frida/runtime/core.js:218)                                          
    at <anonymous> (agent/t.ts:32)                                                 
[PKG110::com.leleketang.SchoolFantasy ]-> Process terminated

按照博客, 直接在二进制文件里面找吧…

1
readelf -sW /apex/com.android.runtime/bin/linker64 |grep do_dlopen
1
2
3
4
OP5D2BL1:/ # readelf -sW /apex/com.android.runtime/bin/linker64 |grep do_dlopen
  6857: 00000000000e6d7c    68 FUNC    LOCAL  DEFAULT   10 __dl__ZZ9do_dlopenPKciPK17android_dlextinfoPKvENK3$_1clEv.__uniq.250007671217850615957365636956552013758
  7096: 00000000000d94cc  3576 FUNC    LOCAL  HIDDEN    10 __dl__Z9do_dlopenPKciPK17android_dlextinfoPKv
OP5D2BL1:/ # 

我还是比较好奇这个函数具体所在的位置… 想用IDA看看实现..

所以使用ADB拉去到了本地

1
2
3
PS C:\Users\daily\MyFile\asset\0reverse\frida-agent-example> adb pull /apex/com.android.runtime/bin/linker64 ../elf/
/apex/com.android.runtime/bin/linker64: 1 file pulled, 0 skipped. 3.9 MB/s (2287376 bytes in 0.556s)
PS C:\Users\daily\MyFile\asset\0reverse\frida-agent-example> 

IDA打开后, 直接跳转到目标位置, 看到了目标函数

原来linker64的导出表就这么点东西?

但结果与命令读取的结果不太一样..

1
readelf -sW /apex/com.android.runtime/bin/linker64 |grep do_dlopen

直接在左侧函数表能搜到一样的函数.

1
const void *__fastcall _dl__Z9do_dlopenPKciPK17android_dlextinfoPKv(const char *a1, int a2, _QWORD *a3, __int64 a4)
1
2
3
__int64 __fastcall _dl__ZZ9do_dlopenPKciPK17android_dlextinfoPKvENK3__1clEv___uniq_250007671217850615957365636956552013758(
        __int64 result,
        __int64 a2)

AI给的答复

函数是否值得 Hook建议
do_dlopen(第一个)✅ 强烈推荐可以获取完整的 so 加载信息(路径、flag、caller)
长名字的 lambda(第二个)❌ 不推荐逻辑碎片化,参数无意义,调用上下文不明

那hook 这个linker的do_dlopen与android_dlopen_ext有何区别呢? 360加固不就有自实现linker么.. 难道是自实现linker也得由do_dlopen来最终加载嘛?

AI: 🔥 关键点:加固厂商的“自实现 linker”通常 完全绕过 android_dlopen_ext 和 do_dlopen!

那就是没啥本质区别咯…管他呢先试试hook后的结果..

1
2
3
4
5
Interceptor.attach(Module.getGlobalExportByName("__dl__Z9do_dlopenPKciPK17android_dlextinfoPKv"), {  
    onEnter: function(args) {  
        console.log("android_dlopen_ext called: [" + args[0].readUtf8String()+"]") 
    },  
})

日志如下:

1
2
3
4
5
6
Spawned `com.leleketang.SchoolFantasy`. Resuming main thread!       
Error: unable to find global export '__dl__Z9do_dlopenPKciPK17android_dlextinfoPKv'
    at value (/frida/runtime/core.js:218)                                          
    at <anonymous> (agent/t.ts:32)                                                 
[PKG110::com.leleketang.SchoolFantasy ]-> Process terminated
[PKG110::com.leleketang.SchoolFantasy ]->

新版本没有getBaseAddress()这个API啊..

AI指点后, 使用Module.load

1
2
3
4
5
Interceptor.attach(Module.load("linker64").base.add(0xd94cc), {
    onEnter: function(args) {
        console.log("do_dlopen called: [" + args[0].readUtf8String()+"]")
    },
})

看看结果:

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
Spawned `com.leleketang.SchoolFantasy`. Resuming main thread!           
[PKG110::com.leleketang.SchoolFantasy ]-> do_dlopen called: [libframework-connectivity-tiramisu-jni.so]
do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/oat/arm64/base.odex]
do_dlopen called: [/data/data/com.leleketang.SchoolFantasy/.jiagu/libjiagu_64.so]
do_dlopen called: [liblog.so]
do_dlopen called: [libz.so]
do_dlopen called: [libc.so]
do_dlopen called: [libm.so]
do_dlopen called: [libstdc++.so]
do_dlopen called: [libdl.so]
do_dlopen called: [libjiagu_64.so]
do_dlopen called: [libjiagu_64.so]
do_dlopen called: [libart.so]
do_dlopen called: [libjiagu_64.so]
do_dlopen called: [libjiagu_64.so]
do_dlopen called: [libjiagu_64.so]
do_dlopen called: [libjiagu_64.so]
do_dlopen called: [libjiagu_64.so]
do_dlopen called: [libjiagu_64.so]
do_dlopen called: [libjgdtc.so]
do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libjgdtc.so]
do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so]
do_dlopen called: [libc.so]
do_dlopen called: [libc.so]
Process terminated
[PKG110::com.leleketang.SchoolFantasy ]->

再对比看看android_dlopen_ext的结果

1
2
3
4
5
6
7
8
9
Spawned `com.leleketang.SchoolFantasy`. Resuming main thread!           
[PKG110::com.leleketang.SchoolFantasy ]-> android_dlopen_ext called: [libframework-connectivity-tiramisu-jni.so]
android_dlopen_ext called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/oat/arm64/base.odex]
android_dlopen_ext called: [/data/data/com.leleketang.SchoolFantasy/.jiagu/libjiagu_64.so]
android_dlopen_ext called: [libjgdtc.so]
android_dlopen_ext called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libjgdtc.so]
android_dlopen_ext called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so]
Process terminated
[PKG110::com.leleketang.SchoolFantasy ]->

嗯, 看来hook do_dlopen能拿到的信息确实多一些, 利于整理逆向思维..

根据博客的思路, 也就是大致定位导致崩溃的代码.. 在attach的回调方法 进入, 离开中分别打印日志.. 再联想so加载链接到JNI_Onload代码执行的过程判断反调试代码的位置..

那么我的问题来了, 我不直到so文件具体的加载链接过程啊, 所以切换学习的频道吧.. 补习去咯..

忍不了了, 看了个B站教程, 也是讲解libmsaoaidsec.so的检测.. 我想要hook pthread_create()函数…

1
2
3
4
5
6
7
8
#include <pthread.h>

int pthread_create(
    pthread_t *thread,                // 输出:新线程 ID
    const pthread_attr_t *attr,       // 输入:线程属性(可为 NULL)
    void *(*start_routine)(void *),   // 输入:线程入口函数
    void *arg                         // 输入:传给入口函数的参数
);
参数类型作用注意事项
threadpthread_t*输出参数,用于接收新线程的 ID必须指向有效内存
attrconst pthread_attr_t*线程属性(栈大小、调度策略等)NULL 表示使用默认属性
start_routinevoid*(*)(void*)线程主函数,相当于 main()必须是这种签名
argvoid*传递给 start_routine 的唯一参数可传结构体指针
1
2
3
4
5
6
7
8
Interceptor.attach(Module.getGlobalExportByName("pthread_create"),{  
    onEnter: function(args) {  
        console.log("pthread_create called: [" + args[0]+","+ args[1]+","+ args[2]+","+ args[3]+"]")  
    },  
    onLeave: function(retval) {  
        console.log("pthread_create retval: [" + retval.toInt32()+"]")  
    }  
})
1
2
3
4
5
6
7
8
9
10
11
do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so]
do_dlopen called: [libc.so]
pthread_create called: [0x7ff00cde20,0x0,0x7edfa5f544,0x7ee5c54f44]
pthread_create retval: [0]
pthread_create called: [0x7edfa8c650,0x0,0x7edfa5e8d4,0x0]
pthread_create retval: [0]
do_dlopen called: [libc.so]
pthread_create called: [0x7ff00cce40,0x0,0x7edfa69e5c,0x0]
pthread_create retval: [0]
Process terminated
[PKG110::com.leleketang.SchoolFantasy ]->

可以看到, 加载libmsaoaidsec.so后, 大概率是创建了2~3个线程, 然后Frida就被杀死了…博客作者直接说是调用某个系统调用导致frida调试进程死掉的, 咋就这么直接呢? 问了一下AI, 它举例如下:

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
// 枚举所有线程
DIR *dir = opendir("/proc/self/task/");
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
    if (isdigit(entry->d_name[0])) {
        char comm_path[64];
        snprintf(comm_path, sizeof(comm_path), "/proc/self/task/%s/comm", entry->d_name);
        
        // 读取线程名
        char thread_name[16];
        FILE *f = fopen(comm_path, "r");
        if (f) {
            fgets(thread_name, sizeof(thread_name), f);
            fclose(f);
            
            // 如果是 Frida 线程(如 "gmain", "frida")
            if (strstr(thread_name, "gmain") || strstr(thread_name, "frida")) {
                int tid = atoi(entry->d_name);
                // 只杀死这个线程!
                syscall(__NR_tkill, tid, SIGKILL); 
                // 注意:不是 kill(getpid(), ...),所以主进程不死
            }
        }
    }
}

那我何不如hook这个syscall?? 先试试..

1
2
3
4
5
6
7
8
Interceptor.attach(Module.getGlobalExportByName("syscall"),{
    onEnter: function(args) {
        console.log("syscall called: [" + args[0]+","+ args[1]+","+ args[2]+"]")
    },
    onLeave: function(retval) {
        console.log("syscall retval: [" + retval+"]")
    }
})
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
Spawned `com.leleketang.SchoolFantasy`. Resuming main thread!           
[PKG110::com.leleketang.SchoolFantasy ]-> syscall called: [0xac,0xa9688ff8,0x7ff00d0fa8]
syscall retval: [0x214c]
syscall called: [0xb2,0x7ff00d0fa8,0x2]
syscall retval: [0x214c]
do_dlopen called: [libframework-connectivity-tiramisu-jni.so]
pthread_create called: [0x7ff00d0d68,0x7ff00d0d70,0x7f80adfbb8,0xb400007e89715e00]
pthread_create retval: [0x0]
pthread_create called: [0x7e96e6cc28,0x7e96e6cc30,0x7f80adfbb8,0xb400007e89715e50]
pthread_create retval: [0x0]
syscall called: [0xc6,0x1,0x80802]
syscall retval: [0x5e]
syscall called: [0x39,0x5e,0x6]
syscall retval: [0x0]
syscall called: [0x62,0x7f99397160,0x81]
syscall retval: [0x0]
syscall called: [0xb2,0x1,0x7f6ebcee98]
syscall retval: [0x218e]
syscall called: [0xb2,0x1,0x7f6ebcee98]
syscall retval: [0x218e]
syscall called: [0xb2,0x1,0x7f6ebcee98]
syscall retval: [0x218e]
syscall called: [0xb2,0x1,0x7f6ebcee98]
syscall retval: [0x218e]
pthread_create called: [0x7e96e6cc28,0x7e96e6cc30,0x7f80adfbb8,0xb400007e87fc4320]
pthread_create retval: [0x0]
pthread_create called: [0x7e96e6bc40,0x7e96e6bc90,0x7ee5ee6f58,0xb400007e87e6e400]
pthread_create retval: [0x0]
syscall called: [0x62,0xb400007eea5002d4,0x80]
syscall retval: [0xffffffffffffffff]
pthread_create called: [0x7e96e6bda0,0x7e96e6bdf0,0x7ee5ee6f58,0xb400007e87e70000]
syscall called: [0x62,0xb400007eea5002d4,0x81]
syscall retval: [0x0]
pthread_create retval: [0x0]
pthread_create called: [0x7e96e6cc28,0x7e96e6cc30,0x7f80adfbb8,0xb400007e87fc43a0]
pthread_create retval: [0x0]
syscall called: [0x11b,0x8,0x0]
syscall retval: [0x0]
syscall called: [0xb2,0x1,0x7f6ebcee98]
syscall retval: [0x214c]
syscall called: [0xb2,0x1,0x7f6ebcee98]
syscall retval: [0x214c]
syscall called: [0xb2,0x1,0x7f6ebcee98]
syscall retval: [0x214c]
syscall called: [0xb2,0x1,0x7f6ebcee98]
syscall retval: [0x214c]
syscall called: [0xb2,0x1,0x7f6ebcee98]
syscall retval: [0x214c]
syscall called: [0xb2,0x1,0x7f6ebcee98]
syscall retval: [0x214c]
syscall called: [0xc6,0x1,0x80802]
syscall retval: [0x64]
syscall called: [0x39,0x64,0x6]
syscall retval: [0x0]
do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/oat/arm64/base.odex]
syscall called: [0x62,0xb400007eea4b081c,0x81]
syscall retval: [0x0]
pthread_create called: [0x7ee68182d8,0x0,0x7ee5ee0434,0xb400007e89626480]
pthread_create retval: [0x0]
syscall called: [0x62,0xb400007e89626514,0x81]
syscall retval: [0x0]
syscall called: [0x62,0xb400007e89626540,0x80]
syscall called: [0x62,0xb400007eea475fe0,0x83]
syscall retval: [0x1]
syscall called: [0x62,0xb400007eea475fb4,0x81]
syscall retval: [0x1]
syscall called: [0x11b,0x20,0x0]
syscall retval: [0x0]
syscall called: [0x62,0xb400007eea475fb4,0x81]
syscall retval: [0x0]
syscall called: [0x62,0xb400007eea475fe0,0x80]
syscall called: [0x62,0xb400007eea475fe0,0x83]
syscall retval: [0x1]
syscall called: [0x62,0xb400007eea475fb4,0x81]
syscall retval: [0x1]
syscall retval: [0x0]
syscall called: [0x11b,0x20,0x0]
syscall retval: [0x0]
syscall called: [0x62,0xb400007eea475fb4,0x81]
syscall retval: [0x0]
syscall called: [0x62,0xb400007eea475fe0,0x80]
do_dlopen called: [/data/data/com.leleketang.SchoolFantasy/.jiagu/libjiagu_64.so]
do_dlopen called: [liblog.so]
do_dlopen called: [libz.so]
do_dlopen called: [libc.so]
do_dlopen called: [libm.so]
do_dlopen called: [libstdc++.so]
do_dlopen called: [libdl.so]
do_dlopen called: [libjiagu_64.so]
syscall called: [0x11b,0x8,0x0]
syscall retval: [0x0]
do_dlopen called: [libjiagu_64.so]
do_dlopen called: [libart.so]
pthread_create called: [0x7ff00ce1c0,0x0,0x7e1e608520,0x7e1e6da080]
pthread_create retval: [0x0]
pthread_create called: [0x7ff00ce188,0x0,0x7e1e60b850,0x0]
pthread_create retval: [0x0]
pthread_create called: [0x7ff00ce180,0x0,0x7e1e60c524,0x0]
pthread_create retval: [0x0]
syscall called: [0x62,0x7edfcfd4d0,0x0]
syscall retval: [0x0]
pthread_create called: [0xb400007e87ec48a0,0x0,0x7e1e617328,0x0]
pthread_create retval: [0x0]
pthread_create called: [0xb400007e87ec48d0,0x0,0x7e1e6173a4,0x0]
pthread_create retval: [0x0]
pthread_create called: [0x7ff00cf200,0x0,0x7e1e57e214,0x0]
pthread_create retval: [0x0]
syscall called: [0xdc,0x1200011,0x0]
syscall called: [0x62,0x7eea601690,0x89]
syscall retval: [0x21a3]
syscall called: [0x62,0x7eea600c10,0x89]
syscall called: [0x62,0x7eea600c10,0x81]
syscall retval: [0x0]
syscall retval: [0xffffffffffffffff]
syscall called: [0x62,0x7eea601690,0x81]
syscall called: [0x62,0x7eea600c10,0x81]
syscall retval: [0x0]
syscall retval: [0x0]
syscall called: [0x62,0x7eea601690,0x81]
syscall retval: [0x0]
syscall retval: [0x1]
do_dlopen called: [libjiagu_64.so]
pthread_create called: [0x7ff00cf178,0x0,0x7e1e60240c,0xb400007e87fc2d40]
pthread_create retval: [0x0]
do_dlopen called: [libjiagu_64.so]
pthread_create called: [0xb400007e87fd02e0,0x0,0x7e1e5836d0,0xb400007e87fd15c0]
pthread_create retval: [0x0]
pthread_create called: [0xb400007e87fd02e8,0x0,0x7e1e5836d0,0xb400007e87fd15d0]
pthread_create retval: [0x0]
pthread_create called: [0xb400007e87fd02f0,0x0,0x7e1e5836d0,0xb400007e87fd15e0]
pthread_create retval: [0x0]
pthread_create called: [0xb400007e87fd02f8,0x0,0x7e1e5836d0,0xb400007e87fd15f0]
pthread_create retval: [0x0]
syscall called: [0x62,0x7edfb014d0,0x0]
syscall retval: [0x0]
syscall called: [0x62,0x7edfa034d0,0x0]
syscall retval: [0x0]
syscall called: [0x62,0x7e95d5b4d0,0x0]
syscall retval: [0x0]
do_dlopen called: [libjiagu_64.so]
do_dlopen called: [libjiagu_64.so]
pthread_create called: [0x7ff00cf250,0x0,0x7e1e597400,0xb400007eea49d9c0]
pthread_create retval: [0x0]
pthread_create called: [0x7ff00cebf8,0x0,0x7e1e658708,0xb400007e87fc0f00]
pthread_create retval: [0x0]
pthread_create called: [0x7ff00cec10,0x0,0x7e1e6597fc,0xb400007e81563000]
pthread_create retval: [0x0]
do_dlopen called: [libjiagu_64.so]
do_dlopen called: [libjiagu_64.so]
do_dlopen called: [libjgdtc.so]
do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libjgdtc.so]
pthread_create called: [0x7ff00cef50,0x7ff00cefa0,0xab00007ee5ee6f58,0xb400007e8159dc00]
pthread_create retval: [0x0]
syscall called: [0x62,0xb400007eea4af984,0x81]
syscall retval: [0x0]
syscall called: [0x62,0xb400007eea520750,0x80]
syscall called: [0x62,0xb400007eea520750,0x83]
syscall retval: [0x1]
syscall called: [0x62,0xb400007eea4af984,0x81]
syscall retval: [0x1]
syscall retval: [0x0]
pthread_create called: [0x7ff00cef50,0x7ff00cefa0,0xab00007ee5ee6f58,0xb400007e8159f800]
pthread_create retval: [0x0]
syscall called: [0x62,0xb400007eea4af984,0x81]
syscall retval: [0x0]
syscall called: [0x62,0xb400007eea520750,0x80]
syscall called: [0x62,0xb400007eea520750,0x83]
syscall retval: [0x1]
syscall called: [0x62,0xb400007eea4af984,0x81]
syscall retval: [0x1]
syscall retval: [0x0]
syscall called: [0x11b,0x8,0x0]
syscall retval: [0x0]
syscall called: [0x11b,0x8,0x0]
syscall retval: [0x0]
do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so]
do_dlopen called: [libc.so]
pthread_create called: [0x7ff00cde20,0x0,0x7edf961544,0x7ee5c54f44]
pthread_create retval: [0x0]
pthread_create called: [0x7edf98e650,0x0,0x7edf9608d4,0x0]
pthread_create retval: [0x0]
do_dlopen called: [libc.so]
pthread_create called: [0x7ff00cce40,0x0,0x7edf96be5c,0x0]
pthread_create retval: [0x0]
Process terminated
[PKG110::com.leleketang.SchoolFantasy ]->

额, 似乎并不能直接调用syscall导致进程终止..

1
2
syscall called: [0x11b,0x8,0x0]
这表示调用了系统调用号为 0x11b(十进制 283)的系统调用(__NR_tkill),参数为 0x8(tid线程ID) 和 0x0(探测)。

而且多次执行脚本后, 都是上述的情况

1
2
3
4
5
6
7
8
9
10
do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so]
do_dlopen called: [libc.so]
pthread_create called: [0x7ff00cde20,0x0,0x7edf961544,0x7ee5c54f44]
pthread_create retval: [0x0]
pthread_create called: [0x7edf98e650,0x0,0x7edf9608d4,0x0]
pthread_create retval: [0x0]
do_dlopen called: [libc.so]
pthread_create called: [0x7ff00cce40,0x0,0x7edf96be5c,0x0]
pthread_create retval: [0x0]
Process terminated

新线程ID, 父线程ID都是不变的..

1
2
pthread_create called: [0x7edf98e650,0x0,0x7edf9608d4,0x0]
位于0x7edf98e650的线程将开始执行位于0x7edf9608d4的代码...

感觉自己探索的这些方面没啥用, 走不远, 跟着博客走吧..

博客作者是利用frida脚本hook linker64的do_dlopen函数, 拦截该函数, 判断加载的so, 如果是libmsaoaidsec.so就延时10s再加载, 然后在这10s内, 手动在手机终端执行监控命令:

1
strace -e trace=process -i -f -p [pid]

等待10s后frida线程被杀死, strace便会打印结果..我的疑问就来了.. 他喵的strace又是什么鬼? 学个没完没了了..

1
2
3
OP5D2BL1:/ # strace -e trace=process,memory -i -f -p 25947
/system/bin/sh: strace: inaccessible or not found
127|OP5D2BL1:/ # 

似乎我的手机没有这个二进制命令… 细看博客, 似乎发现博主藏了一手, Log.log是博主自己封装的工具, 能直接打印当前代码上下文的线程id… 哦不, 还有一手, strace应该是装了什么模块才有的..

得补课了, strace的构建…好巧不巧, 刚好搜到了strace的magisk模块… 刷得飞起!

1
2
3
4
OP5D2BL1:/ # strace
strace: must have PROG [ARGS] or -p PID
Try 'strace -h' for more information.
1|OP5D2BL1:/ # 

这才对嘛, 请AI分析一下博主的命令..

1
strace -e trace=process -i -f -p 15534
参数含义详细说明
-e trace=process只跟踪与“进程/线程生命周期”相关的系统调用这是一个预定义的 syscall 分组(group),包含:
clone(创建线程/进程)
fork, vfork(创建子进程)
execve(执行新程序)
exit, _exit, exit_group(退出)
wait4, waitid(等待子进程)
在 Android 中,pthread_create 最终会调用 clone,所以这个选项能捕获所有新线程的创建!
-i打印系统调用发生时的指令指针(Instruction Pointer)显示形如 [00007abc12345678] 的地址,即当前执行代码在内存中的位置。
✅ 用途:可定位是哪个 so 库或函数触发了 clone/execve,辅助逆向分析(需结合 /proc/PID/maps)。
-f跟踪子进程和线程(follow forks)如果目标进程创建了新线程(clone)或子进程(fork),strace 会自动 attach 并继续跟踪它们。
⚠️ 不加 -f 的话,你只能看到主线程的行为,会漏掉反调试线程!
-p 15534attach 到 PID 为 15534 的运行中进程监控指定进程(通常是目标 App 的主进程)。

strace官网就说了, strace就是监控软件发出系统调用的工具.. 然后libmsaoaidsec.so刚好是通过系统调用杀掉frida调试进程的, 这下王八看绿豆, 对眼了.. 我必须学会使用strace..不过当前需要解决的问题是: 如何打印当前上下文的线程id? AI!!!!

1
2
3
4
5
6
7
const Log = {
  log(msg) {
    const pid = Process.id;
    const tid = Thread.id; // 或 syscall('gettid')
    console.log(`${pid}-${tid} ${msg}`);
  }
};

那我就改一改吧, 这沙比AI骗我… 根本没有这个API…

我就按自己的理解, 改一下logger模块里的代码, 应该是

1
2
3
export function log(message:any): void {  
    console.log(Process.id+"-"+Process.getCurrentThreadId()+" "+message);  
}

难绷, 才发现ts模块引用规范中, 原来模块名后缀ts, 但import语句中后缀是js..

1
2
3
4
5
6
7
8
9
10
11
12
import { log } from "./logger.js";  
  
Interceptor.attach(Module.load("linker64").base.add(0xd94cc), {  
    onEnter: function(args) {  
        this.name = args[0].readUtf8String();  
        log("do_dlopen called: [" + this.name+"]")  
        if (this.name!=null&&this.name.indexOf('libmsaoaidsec.so')>=0){  
            log("delay to load libmsaoaidsec.so")  
            Thread.sleep(10)  
        }  
    },  
})

结果符合预期.. 确实等待了10秒frida进程才死掉, 并且日志中最好不要有中文, 不然会常出现不符合预期的结果(英文就不会)..不好排查…

1
2
3
4
5
6
7
10645-10645 do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libjgdtc.so]
10645-10645 do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so]
10645-10645 delay to load libmsaoaidsec.so
10645-10645 do_dlopen called: [libc.so]
10645-10645 do_dlopen called: [libc.so]
Process terminated
[PKG110::com.leleketang.SchoolFantasy ]->

接下来开两个控制台, 一个执行frida, 另一个执行

1
strace -e trace=process -i -f -p 10645
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
Spawned `com.leleketang.SchoolFantasy`. Resuming main thread!           
[PKG110::com.leleketang.SchoolFantasy ]-> 12995-12995 do_dlopen called: [libframework-connectivity-tiramisu-jni.so]
12995-12995 do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/oat/arm64/base.odex]
12995-12995 do_dlopen called: [/data/data/com.leleketang.SchoolFantasy/.jiagu/libjiagu_64.so]
12995-12995 do_dlopen called: [liblog.so]
12995-12995 do_dlopen called: [libz.so]
12995-12995 do_dlopen called: [libc.so]
12995-12995 do_dlopen called: [libm.so]
12995-12995 do_dlopen called: [libstdc++.so]
12995-12995 do_dlopen called: [libdl.so]
12995-12995 do_dlopen called: [libjiagu_64.so]
12995-12995 do_dlopen called: [libjiagu_64.so]
12995-12995 do_dlopen called: [libart.so]
12995-12995 do_dlopen called: [libjiagu_64.so]
12995-12995 do_dlopen called: [libjiagu_64.so]
12995-12995 do_dlopen called: [libjiagu_64.so]
12995-12995 do_dlopen called: [libjiagu_64.so]
12995-12995 do_dlopen called: [libjiagu_64.so]
12995-12995 do_dlopen called: [libjiagu_64.so]
12995-12995 do_dlopen called: [libjgdtc.so]
12995-12995 do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libjgdtc.so]
12995-12995 do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so]
12995-12995 delay to load libmsaoaidsec.so
12995-12995 do_dlopen called: [libc.so]
Process terminated
[PKG110::com.leleketang.SchoolFantasy ]->
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
OP5D2BL1:/ # strace -e trace=process -i -f -p 12995
strace: Process 12995 attached with 23 threads
[pid 12995] [00000072fa919b10] clone(child_stack=0x724aafd4b0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTIDstrace: Process 13186 attached
, parent_tid=[13186], tls=0x724aafd840, child_tidptr=0x724aafd4d0) = 13186
[pid 12995] [00000072fa919b10] clone(child_stack=0x7201dfc4b0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTIDstrace: Process 13187 attached
, parent_tid=[13187], tls=0x7201dfc840, child_tidptr=0x7201dfc4d0) = 13187
[pid 13187] [0000007308979008] exit_group(0) = ?
[pid 13187] [????????????????] +++ exited with 0 +++
[pid 13186] [????????????????] +++ exited with 0 +++
[pid 13128] [????????????????] +++ exited with 0 +++
[pid 13125] [????????????????] +++ exited with 0 +++
[pid 13124] [????????????????] +++ exited with 0 +++
[pid 13101] [????????????????] +++ exited with 0 +++
[pid 13097] [????????????????] +++ exited with 0 +++
[pid 13096] [????????????????] +++ exited with 0 +++
[pid 13095] [????????????????] +++ exited with 0 +++
[pid 13091] [????????????????] +++ exited with 0 +++
[pid 13090] [????????????????] +++ exited with 0 +++
[pid 13073] [????????????????] +++ exited with 0 +++
[pid 13071] [????????????????] +++ exited with 0 +++
[pid 13070] [????????????????] +++ exited with 0 +++
[pid 13065] [????????????????] +++ exited with 0 +++
[pid 13008] [????????????????] +++ exited with 0 +++
[pid 13007] [????????????????] +++ exited with 0 +++
[pid 13005] [????????????????] +++ exited with 0 +++
[pid 13004] [????????????????] +++ exited with 0 +++
[pid 13003] [????????????????] +++ exited with 0 +++
[pid 13069] [????????????????] +++ exited with 0 +++
[pid 13009] [????????????????] +++ exited with 0 +++
[pid 13006] [????????????????] +++ exited with 0 +++
[pid 13094] [????????????????] +++ exited with 0 +++
[????????????????] +++ exited with 0 +++
OP5D2BL1:/ #
1
[pid 13187] [0000007308979008] exit_group(0) = ?

这一行显眼包啊, 和博客里面的一模一样…那么易得该代码在内存中是被动态释放的, 逆向也找不到, 应该追踪是哪里的代码处理过这一片的内存.. 按原来的操作再来一遍strace

给正在看的基础不牢的小伙伴: 就是在重新执行

1
frida -U -f package.name -l agent/index.ts

然后执行

1
strace -e trace=process,memory -i -f -p pid
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
OP5D2BL1:/ # strace -e trace=process,memory -i -f -p 17603
strace: Process 17603 attached with 21 threads
[pid 17603] [000000730a0906cc] mprotect(0x7308e3c000, 393216, PROT_READ|PROT_WRITE) = 0
[pid 17603] [000000730a0906cc] mprotect(0x7308f5a000, 393216, PROT_READ|PROT_WRITE) = 0
[pid 17603] [000000730a0906cc] mprotect(0x7308d43000, 393216, PROT_READ|PROT_WRITE) = 0
[pid 17603] [000000730a0906cc] mprotect(0x7308ce3000, 393216, PROT_READ|PROT_WRITE) = 0
[pid 17603] [000000730a09128c] mmap(NULL, 393216, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x72e6801000
[pid 17603] [000000730a09128c] mmap(NULL, 464, PROT_READ, MAP_PRIVATE, 103, 0) = 0x730898b000
[pid 17603] [000000730a09128c] mmap(NULL, 5904, PROT_READ, MAP_PRIVATE, 103, 0xa9000) = 0x7308989000
[pid 17603] [000000730a09128c] mmap(NULL, 3808, PROT_READ, MAP_PRIVATE, 103, 0x47000) = 0x7308988000
[pid 17603] [000000730a09128c] mmap(NULL, 40192, PROT_READ, MAP_PRIVATE, 103, 0x3000) = 0x730897e000
[pid 17603] [000000730a09128c] mmap(NULL, 1044480, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x724a709000
[pid 17603] [000000730a09064c] munmap(0x724a709000, 266240) = 0
[pid 17603] [000000730a09064c] munmap(0x724a7f4000, 81920) = 0
[pid 17603] [000000730a09128c] mmap(0x724a74a000, 225280, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED, 103, 0) = 0x724a74a000
[pid 17603] [000000730a09128c] mmap(0x724a790000, 408320, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 103, 0x46000) = 0x724a790000
[pid 17603] [000000730a0906cc] mprotect(0x724a790000, 8192, PROT_READ) = 0
[pid 17603] [000000730a09128c] mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x730897d000
[pid 17603] [000000730a0906cc] mprotect(0x725899a000, 4096, PROT_READ) = 0
[pid 17603] [000000730a0906cc] mprotect(0x730897d000, 4096, PROT_READ) = 0
[pid 17603] [000000730a0912cc] mremap(0x730897d000, 4096, 4096, MREMAP_MAYMOVE|MREMAP_FIXED, 0x725899a000) = 0x725899a000
[pid 17603] [000000730a09064c] munmap(0x730897e000, 40192) = 0
[pid 17603] [000000730a09064c] munmap(0x7308988000, 3808) = 0
[pid 17603] [000000730a09064c] munmap(0x7308989000, 5904) = 0
[pid 17603] [000000730a09064c] munmap(0x730898b000, 464) = 0
[pid 17603] [000000730a09064c] munmap(0x72e6801000, 393216) = 0
[pid 17603] [00000072fa95a88c] mmap(NULL, 21138016, PROT_READ, MAP_PRIVATE, 103, 0) = 0x70fe023000
[pid 17603] [00000072fa959c4c] munmap(0x70fe023000, 21138016) = 0
[pid 17603] [00000072fa95a88c] mmap(NULL, 1040384, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = 0x724aa04000
[pid 17603] [00000072fa959ccc] mprotect(0x724aa05000, 1032192, PROT_READ|PROT_WRITE) = 0
[pid 17603] [00000072fa919b10] clone(child_stack=0x724aafd4b0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tid=[17753], tls=0x724aafd840, child_tidptr=0x724aafd4d0) = 17753
[pid 17603] [00000072fa95a88c] mmap(NULL, 1040384, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = 0x72007bb000
[pid 17603] [00000072fa959ccc] mprotect(0x72007bc000, 1032192, PROT_READ|PROT_WRITE) = 0
[pid 17603] [00000072fa919b10] clone(child_stack=0x72008b44b0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTIDstrace: Process 17756 attached
, parent_tid=[17756], tls=0x72008b4840, child_tidptr=0x72008b44d0) = 17756
[pid 17603] [000000730a09128c] mmap(NULL, 393216, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x72e6801000
[pid 17603] [000000730a09064c] munmap(0x72e6801000, 393216) = 0
[pid 17603] [00000072fa95a88c] mmap(NULL, 1040384, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = 0x72006bd000
[pid 17603] [00000072fa959ccc] mprotect(0x72006be000, 1032192, PROT_READ|PROT_WRITE) = 0
[pid 17603] [00000072fa919b10] clone(child_stack=0x72007b64b0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tid=[17757], tls=0x72007b6840, child_tidptr=0x72007b64d0) = 17757
[pid 17756] [00000072fa95a88c] mmap(NULL, 36864, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0 <unfinished ...>
[pid 17603] [00000072fa959d0c] madvise(0x71ff617000, 69632, MADV_DONTNEED <unfinished ...>
[pid 17756] [00000072fa95a88c] <... mmap resumed>) = 0x7308983000
[pid 17603] [00000072fa959d0c] <... madvise resumed>) = 0
[pid 17756] [00000072fa959ccc] mprotect(0x7308983000, 4096, PROT_NONE <unfinished ...>
[pid 17603] [00000072fa95a88c] mmap(NULL, 1183744, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x71ff4df000
[pid 17603] [00000072fa959d0c] madvise(0x71ff780000, 4096, MADV_DONTNEED) = 0
[pid 17603] [00000072fa959d0c] madvise(0x71ff79a000, 12288, MADV_DONTNEED) = 0
[pid 17603] [00000072fa959d0c] madvise(0x71e8773000, 4096, MADV_DONTNEED) = 0
[pid 17603] [00000072fa959d0c] madvise(0x71e87cd000, 4096, MADV_DONTNEED) = 0
[pid 17603] [00000072fa959d0c] madvise(0x724adf8000, 4096, MADV_DONTNEED) = 0
[pid 17603] [00000072fa959d0c] madvise(0x71f5bbb000, 4096, MADV_DONTNEED) = 0
[pid 17603] [000000730a09128c] mmap(NULL, 393216, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x72e6801000
[pid 17603] [000000730a09064c] munmap(0x72e6801000, 393216) = 0
[pid 17756] [00000072fa959ccc] <... mprotect resumed>) = 0
[pid 17603] [000000730a09128c] mmap(NULL, 393216, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x72e6801000
[pid 17603] [000000730a09064c] munmap(0x72e6801000, 393216) = 0
[pid 17603] [000000730a09128c] mmap(NULL, 393216, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x72e6801000
[pid 17603] [000000730a09064c] munmap(0x72e6801000, 393216) = 0
[pid 17603] [000000730a09128c] mmap(NULL, 393216, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x72e6801000
[pid 17603] [000000730a09064c] munmap(0x72e6801000, 393216) = 0
[pid 17756] [00000072fa95a88c] mmap(NULL, 16777216, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x71fa200000
[pid 17756] [00000072fa959ccc] mprotect(0x71fb0f4000, 16384, PROT_READ|PROT_WRITE) = 0
[pid 17603] [000000730a09128c] mmap(NULL, 393216, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x72e6801000
[pid 17603] [000000730a09064c] munmap(0x72e6801000, 393216) = 0
[pid 17756] [00000072fa95a88c] mmap(NULL, 28, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7308982000
[pid 17756] [0000007308982008] exit_group(0) = ?
[pid 17756] [????????????????] +++ exited with 0 +++
[pid 17642] [????????????????] +++ exited with 0 +++
[pid 17611] [????????????????] +++ exited with 0 +++
strace: Process 17753 attached
strace: Process 17757 attached
[pid 17757] [????????????????] +++ exited with 0 +++
[pid 17753] [????????????????] +++ exited with 0 +++
[pid 17643] [????????????????] +++ exited with 0 +++
[pid 17626] [????????????????] +++ exited with 0 +++
[pid 17615] [????????????????] +++ exited with 0 +++
[pid 17607] [????????????????] +++ exited with 0 +++
[pid 17616] [????????????????] +++ exited with 0 +++
[pid 17619] [????????????????] +++ exited with 0 +++
[pid 17614] [????????????????] +++ exited with 0 +++
[pid 17610] [????????????????] +++ exited with 0 +++
[pid 17609] [????????????????] +++ exited with 0 +++
[pid 17623] [????????????????] +++ exited with 0 +++
[pid 17621] [????????????????] +++ exited with 0 +++
[pid 17618] [????????????????] +++ exited with 0 +++
[pid 17613] [????????????????] +++ exited with 0 +++
[pid 17646] [????????????????] +++ exited with 0 +++
[pid 17612] [????????????????] +++ exited with 0 +++
[pid 17608] [????????????????] +++ exited with 0 +++
[pid 17617] [????????????????] +++ exited with 0 +++
[pid 17620] [????????????????] +++ exited with 0 +++
[????????????????] +++ exited with 0 +++
OP5D2BL1:/ #

1
2
[pid 17756] [00000072fa95a88c] mmap(NULL, 28, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7308982000
[pid 17756] [0000007308982008] exit_group(0) = ?

上面这段日志更是显眼包, 也就是反调试模块利用mmap函数分配了起始于0x7308982000, 向后28字节长度的内存区域, 而且是可读, 可写, 可执行的私有匿名内存. 在这片内存的0x0000007308982008位置执行了退出代码…要看详细信息, 应该hook mmap函数…打印调用堆栈, 看看具体是哪里调用的…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { log } from "./logger.js";  
  
let loaded = false;  
  
Interceptor.attach(Module.getGlobalExportByName("mmap"),{  
    onEnter: function(args) {  
        let length = args[1].toString(16)  
        // if (parseInt(length, 16) == 28) {  
        if (loaded) {  
            log('backtrace:\n' + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n');  
        }  
    },  
})  
  
Interceptor.attach(Module.load("linker64").base.add(0xd94cc), {  
    onEnter: function(args) {  
        this.name = args[0].readUtf8String();  
        log("do_dlopen called: [" + this.name+"]")  
        if (this.name!=null&&this.name.indexOf('libmsaoaidsec.so')>=0){  
            // log("delay to load libmsaoaidsec.so")  
            // Thread.sleep(10)            loaded = true;  
        }  
    },  
})
1
2
3
4
5
6
7
8
9
10
11
12
27853-27853 do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so]
27853-27853 do_dlopen called: [libc.so]
27853-27853 backtrace:
0x724a85e7ec libmsaoaidsec.so!0x187ec
0x724a85e7ec libmsaoaidsec.so!0x187ec
0x724a85ede4 libmsaoaidsec.so!0x18de4
0x724a862ffc libmsaoaidsec.so!0x1cffc
0x724a861dcc libmsaoaidsec.so!0x1bdcc
0x724a861f7c libmsaoaidsec.so!0x1bf7c
0x724a85a624 libmsaoaidsec.so!_init+0x224
0x730a0cadf8 linker64!__dl__ZN6soinfo17call_constructorsEv+0x2ac
0x730a0cadf8 linker64!__dl__ZN6soinfo17call_constructorsEv+0x2ac

然后把该so文件拉取到电脑

1
adb pull /data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so ../elf/

然后用IDA查看libmsaoaidsec.so位于0x187ec处的代码..

各处调用mmap的代码转成伪C代码

  • 0x187ec
    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
    
    FILE *__fastcall sub_18774(_QWORD *a1, char *filename)
    {
    FILE *result; // x0
    unsigned int v4; // w20
    size_t v5; // x1
    void *v6; // x0
    _DWORD *v7; // x21
    _BYTE v8[48]; // [xsp+8h] [xbp-B8h] BYREF
    size_t len; // [xsp+38h] [xbp-88h]
    __int64 v10; // [xsp+88h] [xbp-38h]
    
    v10 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
    result = fopen(filename, (const char *)&dword_302BC);
    if ( result )
    {
      a1[2] = result;
      v4 = fileno(result);
      if ( fstat(v4, (struct stat *)v8) )
        return 0LL;
      v5 = len;
      a1[1] = len;
      v6 = mmap(0LL, v5, 1, 2, v4, 0LL);
      *a1 = v6;
      if ( v6 == (void *)-1LL )
      {
        return 0LL;
      }
      else
      {
        if ( _read_chk(v4, v6, a1[1], -1LL) == -1 )
        {
          v7 = (_DWORD *)_errno();
          while ( *v7 == 4 && _read_chk(v4, *a1, a1[1], -1LL) == -1 )
            ;
        }
        return (FILE *)(&dword_0 + 1);
      }
    }
    return result;
    }
    
  • 0x18de4
    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
    
    _QWORD *sub_18D54()
    {
    char **v0; // x20
    __int64 v1; // x0
    char *v2; // x1
    _QWORD *v3; // x19
    void *ptr[2]; // [xsp+0h] [xbp-30h] BYREF
    
    ptr[1] = *(void **)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
    sub_18B3C(ptr);
    v0 = (char **)ptr[0];
    if ( *(_QWORD *)ptr[0] )
    {
      if ( *((_QWORD *)ptr[0] + 1) < *((_QWORD *)ptr[0] + 2) )
      {
        v1 = operator new(0x158uLL);
        *(_BYTE *)(v1 + 200) = 0;
        *(_QWORD *)v1 = 0LL;
        *(_OWORD *)(v1 + 184) = 0u;
        *(_OWORD *)(v1 + 168) = 0u;
        *(_OWORD *)(v1 + 152) = 0u;
        *(_OWORD *)(v1 + 136) = 0u;
        *(_OWORD *)(v1 + 120) = 0u;
        *(_OWORD *)(v1 + 56) = 0u;
        *(_OWORD *)(v1 + 40) = 0u;
        *(_OWORD *)(v1 + 24) = 0u;
        *(_OWORD *)(v1 + 8) = 0u;
        *(_OWORD *)(v1 + 304) = 0u;
        *(_OWORD *)(v1 + 320) = 0u;
        *(_OWORD *)(v1 + 272) = 0u;
        *(_OWORD *)(v1 + 288) = 0u;
        *(_OWORD *)(v1 + 240) = 0u;
        *(_OWORD *)(v1 + 256) = 0u;
        *(_OWORD *)(v1 + 208) = 0u;
        *(_OWORD *)(v1 + 224) = 0u;
        v2 = *v0;
        v3 = (_QWORD *)v1;
        *(_QWORD *)(v1 + 336) = v0[1];
        if ( ((unsigned __int8)sub_18774((_QWORD *)v1, v2) & 1) != 0 && (sub_18240(v3 + 3, *v3) & 1) != 0 )
        {
    LABEL_7:
          v0 = (char **)ptr[0];
          if ( !ptr[0] )
            return v3;
          goto LABEL_8;
        }
        sub_18F0C(v3);
        operator delete(v3);
      }
      v3 = 0LL;
      goto LABEL_7;
    }
    v3 = 0LL;
    if ( !ptr[0] )
      return v3;
    LABEL_8:
    if ( *v0 )
      free(*v0);
    operator delete(v0);
    return v3;
    }
    
  • 0x1cffc
    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
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    
    __int64 __fastcall sub_1CEF8(__int64 (__fastcall *a1)(__int128 *, _QWORD, void (__fastcall __noreturn *)(), void *))
    {
    int v2; // w8
    void *v3; // x0
    void *v4; // x20
    unsigned int v5; // w0
    unsigned __int64 v6; // x8
    _QWORD *v7; // x0
    _QWORD *v8; // x20
    unsigned int v9; // w0
    unsigned __int64 v10; // x8
    _QWORD *v11; // x0
    unsigned int v12; // w0
    unsigned __int64 v13; // x8
    void *v14; // x21
    unsigned int v15; // w11
    unsigned int v16; // w12
    _BYTE *v17; // x13
    unsigned __int64 v18; // x16
    int v19; // w17
    unsigned __int64 v20; // x9
    _BYTE *v21; // x11
    int v22; // w13
    unsigned int v23; // w11
    unsigned int v24; // w12
    _BYTE *v25; // x13
    unsigned __int64 v26; // x16
    int v27; // w17
    unsigned __int64 v28; // x9
    _BYTE *v29; // x11
    int v30; // w13
    unsigned int v31; // w11
    unsigned int v32; // w12
    _BYTE *v33; // x13
    unsigned __int64 v34; // x16
    int v35; // w17
    unsigned __int64 v36; // x9
    _BYTE *v37; // x11
    int v38; // w13
    __int128 v40; // [xsp+0h] [xbp-70h] BYREF
    _BYTE v41[30]; // [xsp+10h] [xbp-60h]
    __int64 v42; // [xsp+38h] [xbp-38h]
    
    v42 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
    v2 = *off_47FB8;
    if ( (unsigned int)(*off_47FB8 - 21) <= 1 )
    {
      v3 = dlopen("libart.so", 0);
      if ( v3 )
      {
        v4 = v3;
        *(_OWORD *)&v41[14] = *(_OWORD *)((char *)&qword_30608[1] + 6);
        v40 = xmmword_305F8;
        *(_OWORD *)v41 = *(_OWORD *)qword_30608;
        v5 = _strlen_chk((const char *)&v40, 0x2EuLL);
        if ( (int)v5 >= 1 )
        {
          if ( v5 >= 2uLL )
          {
            v6 = v5 - (unsigned __int64)(v5 & 1);
            v15 = 0;
            v16 = 1;
            v17 = (char *)&v40 + 1;
            v18 = v6;
            do
            {
              v19 = *((_DWORD *)qword_30670 + v16 % 3);
              v18 -= 2LL;
              v16 += 2;
              LOBYTE(v19) = *v17 ^ v19;
              *(v17 - 1) ^= *((_BYTE *)qword_30670 + 4 * (v15 % 3));
              *v17 = v19;
              v17 += 2;
              v15 += 2;
            }
            while ( v18 );
            if ( (v5 & 1) == 0 )
              goto LABEL_22;
          }
          else
          {
            v6 = 0LL;
          }
          v20 = v5 - v6;
          v21 = &v41[v6 - 16];
          do
          {
            v22 = *((_DWORD *)qword_30670 + (unsigned int)v6 % 3);
            --v20;
            LODWORD(v6) = v6 + 1;
            *v21++ ^= v22;
          }
          while ( v20 );
        }
    LABEL_22:
        v14 = dlsym(v4, (const char *)&v40);
        dlclose(v4);
        goto LABEL_35;
      }
    LABEL_16:
      v14 = 0LL;
      goto LABEL_35;
    }
    if ( (unsigned int)(v2 - 23) > 2 )
    {
      if ( v2 < 26 )
        goto LABEL_16;
      v11 = sub_18D54();
      if ( !v11 )
        goto LABEL_16;
      v8 = v11;
      *(_WORD *)&v41[16] = 203;
      v40 = xmmword_3064D;
      *(_OWORD *)v41 = *(_OWORD *)byte_3065D;
      v12 = _strlen_chk((const char *)&v40, 0x22uLL);
      if ( (int)v12 >= 1 )
      {
        if ( v12 >= 2uLL )
        {
          v13 = v12 - (unsigned __int64)(v12 & 1);
          v31 = 0;
          v32 = 1;
          v33 = (char *)&v40 + 1;
          v34 = v13;
          do
          {
            v35 = *((_DWORD *)qword_30670 + v32 % 3);
            v34 -= 2LL;
            v32 += 2;
            LOBYTE(v35) = *v33 ^ v35;
            *(v33 - 1) ^= *((_BYTE *)qword_30670 + 4 * (v31 % 3));
            *v33 = v35;
            v33 += 2;
            v31 += 2;
          }
          while ( v34 );
          if ( (v12 & 1) == 0 )
            goto LABEL_34;
        }
        else
        {
          v13 = 0LL;
        }
        v36 = v12 - v13;
        v37 = &v41[v13 - 16];
        do
        {
          v38 = *((_DWORD *)qword_30670 + (unsigned int)v13 % 3);
          --v36;
          LODWORD(v13) = v13 + 1;
          *v37++ ^= v38;
        }
        while ( v36 );
      }
    }
    else
    {
      v7 = sub_18D54();
      if ( !v7 )
        goto LABEL_16;
      v8 = v7;
      *(_QWORD *)&v41[15] = 0xC5DCCDC8F1DDC2LL;
      v40 = xmmword_30626;
      *(_OWORD *)v41 = *(_OWORD *)&word_30636;
      v9 = _strlen_chk((const char *)&v40, 0x27uLL);
      if ( (int)v9 >= 1 )
      {
        if ( v9 >= 2uLL )
        {
          v10 = v9 - (unsigned __int64)(v9 & 1);
          v23 = 0;
          v24 = 1;
          v25 = (char *)&v40 + 1;
          v26 = v10;
          do
          {
            v27 = *((_DWORD *)qword_30670 + v24 % 3);
            v26 -= 2LL;
            v24 += 2;
            LOBYTE(v27) = *v25 ^ v27;
            *(v25 - 1) ^= *((_BYTE *)qword_30670 + 4 * (v23 % 3));
            *v25 = v27;
            v25 += 2;
            v23 += 2;
          }
          while ( v26 );
          if ( (v9 & 1) == 0 )
            goto LABEL_34;
        }
        else
        {
          v10 = 0LL;
        }
        v28 = v9 - v10;
        v29 = &v41[v10 - 16];
        do
        {
          v30 = *((_DWORD *)qword_30670 + (unsigned int)v10 % 3);
          --v28;
          LODWORD(v10) = v10 + 1;
          *v29++ ^= v30;
        }
        while ( v28 );
      }
    }
    LABEL_34:
    v14 = (void *)sub_18E5C(v8, &v40);
    sub_18EBC(v8);
    LABEL_35:
    if ( (sub_25B30(v14) & 1) != 0 )
      sub_234E0(0LL);
    return a1(&v40, 0LL, sub_1C544, v14);
    }
    
  • 0x1bdcc
    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
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    
    void sub_1B924()
    {
    int v0; // w9
    int v1; // w8
    char *v2; // x8
    int v3; // w0
    const char *v4; // x8
    unsigned int v5; // w9
    __int64 v6; // x10
    int v7; // w11
    int v8; // w0
    const char *v9; // x8
    unsigned int v10; // w9
    __int64 v11; // x10
    int v12; // w11
    __int64 v13; // [xsp+0h] [xbp-D0h] BYREF
    unsigned __int64 StatusReg; // [xsp+8h] [xbp-C8h]
    char *v15; // [xsp+10h] [xbp-C0h]
    char *v16; // [xsp+18h] [xbp-B8h]
    __int64 *v17; // [xsp+20h] [xbp-B0h]
    char *v18; // [xsp+28h] [xbp-A8h]
    const char *v19; // [xsp+30h] [xbp-A0h]
    char *v20; // [xsp+38h] [xbp-98h]
    const char *v21; // [xsp+40h] [xbp-90h]
    void *v22; // [xsp+48h] [xbp-88h]
    bool v23; // [xsp+53h] [xbp-7Dh]
    int v24; // [xsp+54h] [xbp-7Ch]
    __int64 (__fastcall *v25)(__int128 *, _QWORD, void (__fastcall __noreturn *)(), void *); // [xsp+58h] [xbp-78h]
    __int64 (__fastcall *v26)(__int128 *, _QWORD, void (__fastcall __noreturn *)(), void *); // [xsp+60h] [xbp-70h]
    __int64 v27; // [xsp+6Ch] [xbp-64h] BYREF
    int v28; // [xsp+74h] [xbp-5Ch]
    __int64 v29; // [xsp+78h] [xbp-58h]
    
    StatusReg = _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
    v0 = 113515708;
    v29 = *(_QWORD *)(StatusReg + 40);
    do
    {
      while ( 1 )
      {
        while ( 1 )
        {
          while ( 1 )
          {
            while ( 1 )
            {
              v1 = v0;
              if ( v0 > 113515707 )
                break;
              if ( v0 <= -1096741019 )
              {
                if ( v0 <= -1856103672 )
                {
                  if ( v0 == -2106510457 )
                  {
                    sub_1CEF8(v26);
                    v0 = 1749579912;
                  }
                  else
                  {
                    v25 = (__int64 (__fastcall *)(__int128 *, _QWORD, void (__fastcall __noreturn *)(), void *))dlsym(v22, v21);
                    v0 = 244441609;
                  }
                }
                else if ( v0 == -1856103671 )
                {
                  if ( v23 )
                    v0 = -1508321617;
                  else
                    v0 = 1991731874;
                }
                else if ( v0 == -1508321617 )
                {
                  v26((__int128 *)&unk_49650, 0LL, (void (__fastcall __noreturn *)())sub_1B8D4, 0LL);
                  v0 = -1036248338;
                }
                else
                {
                  v20 = v15;
                  v3 = _strlen_chk(v15, 0xFFFFFFFFFFFFFFFFLL);
                  v4 = v20;
                  if ( v3 >= 1 )
                  {
                    v5 = 0;
                    v6 = (unsigned int)v3;
                    do
                    {
                      v7 = *((_DWORD *)&v27 + v5 % 3);
                      --v6;
                      ++v5;
                      *v4++ ^= v7;
                    }
                    while ( v6 );
                  }
                  v21 = v16;
                  v8 = _strlen_chk(v16, 0xFFFFFFFFFFFFFFFFLL);
                  v9 = v21;
                  v0 = -669730265;
                  if ( v8 >= 1 )
                  {
                    v10 = 0;
                    v11 = (unsigned int)v8;
                    do
                    {
                      v12 = *((_DWORD *)&v27 + v10 % 3);
                      --v11;
                      ++v10;
                      *v9++ ^= v12;
                    }
                    while ( v11 );
                    v0 = -669730265;
                  }
                }
              }
              else if ( v0 > -814891729 )
              {
                if ( v0 == -814891728 )
                {
                  dlclose(v22);
                  goto LABEL_2;
                }
                if ( v0 == -669730265 )
                {
                  v0 = -1096741018;
                }
                else if ( (sub_12D9C(0LL) & 1) != 0 )
                {
                  v0 = 1749579912;
                }
                else
                {
                  v0 = -2106510457;
                }
              }
              else if ( v0 == -1096741018 )
              {
                v22 = dlopen(v20, 2);
                if ( v22 )
                  v0 = -1901407442;
                else
                  v0 = 264983448;
              }
              else
              {
                v0 = 703713188;
                if ( v1 != -1036248338 )
                {
                  v16 = (char *)(&v13 - 2);
                  v17 = &v27;
                  v28 = 236;
                  v27 = 0xA700000099LL;
                  v18 = v15;
                  *(_QWORD *)v15 = 0xF69F89FA8ECEF5LL;
                  v19 = v16;
                  v2 = v16;
                  *(_QWORD *)(v16 + 7) = 0xC2ED8DC2EB8FF8LL;
                  *(_QWORD *)v2 = 0xF8FD8DC2EB84D3E9LL;
                  v0 = -1449217009;
                }
              }
            }
            if ( v0 <= 1154361410 )
              break;
            if ( v0 <= 1749579911 )
            {
              if ( v0 == 1154361411 )
              {
                v26((__int128 *)&unk_49658, 0LL, sub_19E0C, 0LL);
                v0 = 974408896;
              }
              else
              {
                v0 = 719856854;
              }
            }
            else if ( v0 == 1749579912 )
            {
              v23 = (unsigned int)sub_CAE8() == 249;
              v0 = -1856103671;
            }
            else if ( v0 == 1801253217 )
            {
              if ( v24 == 167 )
                v0 = 1154361411;
              else
                v0 = -814891728;
            }
            else
            {
              sub_1B380(v22, v26);
              v0 = 703713188;
            }
          }
          if ( v0 > 703713187 )
            break;
          if ( v0 == 113515708 )
          {
            v15 = (char *)(&v13 - 2);
            v0 = -1022300220;
          }
          else if ( v0 == 244441609 )
          {
            v26 = v25;
            if ( (unsigned int)sub_CAA8() == 248 )
              v0 = -601035478;
            else
              v0 = 1749579912;
          }
          else
          {
    LABEL_2:
            v0 = 1235481701;
          }
        }
        if ( v0 != 703713188 )
          break;
        v24 = sub_CA28();
        v0 = 1801253217;
      }
      v0 = -814891728;
    }
    while ( v1 == 974408896 );
    }
    

妈蛋混淆成一坨大便了…谁看得懂??AI!!!!!!!

  • 0x1bf7c
    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
    
    __int64 sub_1BEC4()
    {
    int i; // w8
    __pid_t v2; // [xsp+4h] [xbp-4Ch]
    
    _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
    for ( i = 1469504930; ; i = 901232910 )
    {
      while ( i > 901232909 )
      {
        if ( i == 901232910 )
        {
          dword_49014 = v2;
          i = -118926584;
        }
        else
        {
          v2 = getpid();
          i = -553339161;
        }
      }
      if ( i != -553339161 )
        break;
    }
    sub_1B924();
    return 0LL;
    }
    

反正也看不懂, 就假定函数void sub_1B924()就是检测函数吧, 按博客思路, 该函数是在构造函数中被执行的, 所以应该hook linker64中调用构造函数的函数, 然后替换掉这个无参数, 无返回值的函数… 那我为什么不直接利用Module.load加基址然后用拦截器替换呢??

1
2
3
Interceptor.replace(Module.load("libmsaoaidsec.so").base.add(0x1b924),new NativeCallback(function () {
   log(`hook_sub_1b924 >>>>>>>>>>>>>>>>> replace`)
}, 'void', []))

报错, 找不到模块, 缓存手机上的路径后执行脚本

看来确实时机很重要…有趣的是: 目前这个模块中我找到的目标函数与博客中的一样, 看来这个反调试库已经2年没有更新了.. 那我就直接套用博主的脚本了, 但理解才是最重要的..先hook linker64中的call_constructors函数以确定hook时机..

1
readelf -sW /apex/com.android.runtime/bin/linker64 |grep call_constructors
1
2
3
OP5D2BL1:/ # readelf -sW /apex/com.android.runtime/bin/linker64 |grep call_constructors
  7741: 00000000000fcb4c  1044 FUNC    LOCAL  HIDDEN    10 __dl__ZN6soinfo17call_constructorsEv
OP5D2BL1:/ # 

拿到偏移: 0xfcb4c

书写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
import {log} from "./logger.js";

function hook_linker64_call_constructors() {
    let invocationListener = Interceptor.attach(Module.load('linker64').base.add(0xfcb4c), {
        onEnter: function (args) {
            log("call_constructors called: [" + args[0].readUtf8String() + "]")
            let findModuleByName = Process.findModuleByName("libmsaoaidsec.so");
            if (findModuleByName != null) {
                Interceptor.replace(findModuleByName.base.add(0x1b924), new NativeCallback(function () {
                    log(`hook_sub_1b924 >>>>>>>>>>>>>>>>> replace`)
                }, 'void', []))
                invocationListener.detach()
            }
        },
    });
}

Interceptor.attach(Module.load("linker64").base.add(0xd94cc), {
    onEnter: function (args) {
        this.name = args[0].readUtf8String();
        log("do_dlopen called: [" + this.name + "]")
        if (this.name != null && this.name.indexOf('libmsaoaidsec.so') >= 0) {
            log("load libmsaoaidsec.so")
            hook_linker64_call_constructors();
        }
    },
})

又报错??!!

1
2
3
4
5
6
7
8
9
10
11
12
20419-20419 do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so]
20419-20419 load libmsaoaidsec.so
20419-20419 call_constructors called: [@tJr]
20419-20419 hook_sub_1b924 >>>>>>>>>>>>>>>>> replace
20419-20419 do_dlopen called: [libc.so]
20419-20419 do_dlopen called: [liblog.so]
20419-20419 do_dlopen called: [libc.so]
20419-20419 do_dlopen called: [libm.so]
20419-20419 do_dlopen called: [libstdc++.so]
20419-20419 do_dlopen called: [libdl.so]
20419-20419 do_dlopen called: [null]
Process crashed: Bad access due to invalid address

算了换成博客里的脚本试试..(适配新版fridaAPI)

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
import {log} from './logger.js'
function hookDlopen() {
    let linker64_base_addr = Module.load('linker64').base;
    let offset = 0xd94cc // __dl__Z9do_dlopenPKciPK17android_dlextinfoPKv
    let android_dlopen_ext = linker64_base_addr.add(offset)
    if (android_dlopen_ext != null) {
        Interceptor.attach(android_dlopen_ext, {
            onEnter: function(args){
                this.name = args[0].readCString()
                if (this.name != null && this.name.indexOf('libmsaoaidsec.so') >= 0) {
                    hook_linker_call_constructors()
                }
            }, onLeave: function(retval){
                log(`dlopen onLeave name: ${this.name}`)
                if (this.name != null && this.name.indexOf('libmsaoaidsec.so') >= 0) {
                    let JNI_OnLoad = Module.load(this.name).getExportByName('JNI_OnLoad')
                    log(`dlopen onLeave JNI_OnLoad: ${JNI_OnLoad}`)
                }
            }
        })
    }
}

function hook_linker_call_constructors() {
    let linker64_base_addr = Module.load('linker64').base
    let offset = 0xfcb4c // __dl__ZN6soinfo17call_constructorsEv
    let call_constructors = linker64_base_addr.add(offset)
    let listener = Interceptor.attach(call_constructors, {
        onEnter: function (args) {
            log('hook_linker_call_constructors onEnter')
            let secmodule = Process.findModuleByName("libmsaoaidsec.so")
            if (secmodule != null) {
                hook_sub_1b924(secmodule)
                listener.detach()
            }
        }
    })
}

function hook_sub_1b924(secmodule:any) {
    Interceptor.replace(secmodule.base.add(0x1b924), new NativeCallback(function () {
        log(`hook_sub_1b924 >>>>>>>>>>>>>>>>> replace`)
    }, 'void', []));
}

报错了

1
Failed to load script: malformed package

还得是我写的代码才能正常运行, 应该是函数void sub_1B924()里面做了升级, 改了某些标识, 让不执行这个函数的流程走向异常… 但如果执行这个函数的话, 又得分析它具体是如何检测的…

算了时间不早了, 我要睡觉了…

直接看看libmsaoaidsec.so里面的字符表…希望能找到检测相关的字符…

确实有…

看交叉引用呗…

原来是这个函数

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

AI!!!

参数含义
name目标缓冲区指针。格式化后的字符串将写入这里。
0LLflags = 0标志位。通常为 0。某些实现中,若为 -1 表示“未知长度”,但此处为 0,表示正常检查。
1024LLdest_len_from_compiler = 1024编译器推断的缓冲区大小。这是关键!编译器在编译时根据 name 的声明(如 char name[1024])自动传入这个值,用于运行时边界检查。
"/proc/%d/task"format string格式化模板字符串。
v0要填入 %d 的整数值,通常是当前进程的 PID 或某个线程 ID(TID)。

hook看一下..

1
2
3
4
5
6
7
Interceptor.attach(Module.getGlobalExportByName('_sprintf_chk'), {  
    onEnter: function (args) {  
        log('[+] _sprintf_chk called: [' + args[0]+ ',' + args[1]+ ',' + args[2]+ ',' + args[3]+ ']');  
        this.buffer = args[0]  
        this.filePath = args[2]  
    }  
})
1
2
3
4
5
6
Spawned `com.leleketang.SchoolFantasy`. Resuming main thread!       
Error: unable to find global export '_sprintf_chk'
    at value (/frida/runtime/core.js:218)                                         
    at <anonymous> (agent/index.ts:3)                                             
[PKG110::com.leleketang.SchoolFantasy ]-> Process terminated
[PKG110::com.leleketang.SchoolFantasy ]->

找不到函数.. IDA定位一下.. 发现位于打入表..

哪个模块导入的呢?? 盲猜c标准库.. 还真是..

1
2
3
4
5
Interceptor.attach(Module.load('libc.so').getExportByName('_sprintf_chk'), {
    onEnter: function (args) {
        log('[+] _sprintf_chk called: [' + args[0]+ ',' + args[1]+ ',' + args[2]+ ',' + args[3]+ ']');
    }
})
1
2
3
4
5
6
Spawned `com.leleketang.SchoolFantasy`. Resuming main thread!           
Error: /apex/com.android.runtime/lib64/bionic/libc.so: unable to find export '_sprintf_chk'
    at value (/frida/runtime/core.js:232)
    at <anonymous> (agent/index.ts:3)
[PKG110::com.leleketang.SchoolFantasy ]-> Process terminated
[PKG110::com.leleketang.SchoolFantasy ]->

问题答案
Library 列显示 .dynsym 是什么意思?IDA 未成功关联到具体 so 名,.dynsym 只是符号表节区名,不是库名。
__sprintf_chk 实际来自哪个 so?libc.so(Android)或 libc.so.6(Linux)
如何确认?1. 查看 .dynamic 段的 DT_NEEDED
2. 用 readelf -Ws 查看符号绑定
3. 根据常识:只有 libc 提供此函数
Frida 中应 hook 哪个模块?Module.getExportByName('libc.so', '_sprintf_chk')

看来IDA不能解析该函数具体来自哪儿, 那就得运用昨天学到的命令readelf, 直接获取got表中__sprintf_chk的槽位, 然后hook即可..

实际上libmsaoaidsec.so并没有导出这个函数…

1
2
OP5D2BL1:/ # readelf -sW /data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so |grep _sprintf_chk          
1|OP5D2BL1:/ # 

不过既然从IDA中拿到了plt表的偏移, 那就尝试hook看

1
2
3
4
5
Interceptor.attach(Module.load('/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so').base.add(0x8180), {
    onEnter: function (args) {
        log('[+] _sprintf_chk called: [' + args[0]+ ',' + args[1]+ ',' + args[2]+ ',' + args[3]+ ']');
    }
})

果然报错..

1
Failed to load script: the connection is closed   

难道又是hook时机不对的问题??? 至此我不得不思考直接hook plt函数表..由此引出深度思考: 为什么外部函数的调用总是->plt->got->实际函数地址..感觉got表完全可以取代plt…AI:

1
2
3
4
5
6
7
8
9
10
11
12
ARM64 为什么必须用 PLT?
在 ARM64 中,要实现“通过 GOT 调用外部函数”,至少需要两条指令:

ADRP X16, #got_page        ; 计算 GOT 所在页基址(PC 相关)
LDR  X17, [X16, #got_off]  ; 从 GOT 加载函数地址
BR   X17                   ; 间接跳转

这三行 就是 PLT stub 的标准模板!
如果没有 PLT,每个调用点都要重复这三行 → 代码膨胀;
更糟的是:ADRP 的范围有限(±4GB),如果 GOT 太远会失败;
而 PLT 把这段逻辑 集中管理,所有调用者只需 BL plt_stub(一条指令)。
✅ PLT 的作用:把“复杂的位置无关间接跳转”封装成一个可复用的跳板。

也就是说, 通过plt表, 不仅可以让代码段可以实现位置无关的共享使用, 还能节省空间??

对于位置无关间接跳转的理解, 相比于直接调用GOT表, 代码中对GOT表内函数的调用代码取决于GOT表的位置, 而so加载过程中, 有可能got.plt的位置每次都不一样, 毕竟需要调用系统函数mmap来分配具体的内存. 地址是随机的… 如果代码中写死了GOT表的基址, 运行时必定会导致内存的非法访问… 而出现plt表后, 可以临时让调用链停靠在plt表内, 具体要调用哪一个got表的函数, 就要统一用函数来查找了吧.. 这里是一个逻辑模糊的地方…

对于节省空间的说法. 这得实际看看GOT表中的函数是如何被调用的..

这是PLT表里, _sprintf_chk函数的槽位0x8180-0x8190 , 在HEX里就刚好这么一行..也就是16个字节.

用010Editor看看它具体在哪一个段..

SegmentTypeStartSizeFlagsComment
[0]Loadable40h38hR X.text?
[1]Loadable78h38hR W.data?
[2]Dynamic80h38hR W关键!含 .got, .plt 相关数据
[3]GCC eh_frameE8h38hRexception handling
[4]GNU Stack120h38hR Wstack exec flag
[5]GNU RELRO158h38hRread-only after relocation

AI对程序头解析的理解如上. 而我的目标位置0x8180是否位于Dynamic段, 就要看看展开后的结果

额, 解析展开后的结果是: Dynamic段位于0x47650-0x47890, 这差别有点大啊.

IDA中该位置被解析为

确实是动态段的结构体..并且在下一行看到了GOT全称_GLOBAL_OFFSET_TABLE_

并且在0x47d60找到了目标函数__imp___sprintf_chk的字样

got表的调用肯定是来源于plt表.. 查看交叉引用..怎么有2个??

就看哪个是真的plt表, 先看第一个

很明显, 第一个就是真正的plt表, 可是它的位置太靠前了, 我现在不清楚它和位于0x47650-0x47890的Dynamic段有何关联??

再看看另一个引用

哦, 原来x查看的交叉引用包括了本身.. 这里就是got表里的函数嘞, 看看HEX的位置长啥样..

这位置真就只够放一个函数指针, 8个字节. 看看现在放的是什么

1
0x00000000000A9080

它具体有什么含义?

联系调用出的PLT表的槽位:

1
2
3
4
LOAD:0000000000008180                 ADRP            X16, #off_47D60@PAGE
LOAD:0000000000008184                 LDR             X17, [X16,#off_47D60@PAGEOFF]
LOAD:0000000000008188                 ADD             X16, X16, #off_47D60@PAGEOFF
LOAD:000000000000818C                 BR              X17 ; __imp___sprintf_chk

看AI的回答

1
2
3
4
5
6
7
8
9
10
11
AI
具体来说:
当程序第一次调用 __sprintf_chk 时,GOT[47D60] 中的值(比如 0xA9080)会被加载到 X17;
然后 BR X17 跳转到 0xA9080;
xA9080 处的代码会:
将函数索引(relocation index)压栈或传参;
调用动态链接器(如 _dl_runtime_resolve);
解析出 __sprintf_chk 的真实地址(来自 libc);
将真实地址写回 off_47D60(即 GOT 槽位);
跳转到真实函数执行。
之后再次调用时,GOT 槽位已更新为真实地址,不再经过解析桩。

照AI这么说, GOT指向的函数还挺多样化..

每一个导入函数的GOT槽初始值都不一样…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0000000000047D40  98 9D 0A 00 00 00 00 00  B0 79 02 00 00 00 00 00  .........y......
0000000000047D50  A0 9D 0A 00 00 00 00 00  A8 9D 0A 00 00 00 00 00  ................
0000000000047D60  B0 9D 0A 00 00 00 00 00  78 BA 02 00 00 00 00 00  ........x.......
0000000000047D70  C4 99 02 00 00 00 00 00  30 D3 01 00 00 00 00 00  ę ......0.......
0000000000047D80  B8 9D 0A 00 00 00 00 00  F8 9B 02 00 00 00 00 00  ................
0000000000047D90  10 8A 02 00 00 00 00 00  F4 8C 02 00 00 00 00 00  ................
0000000000047DA0  C0 9D 0A 00 00 00 00 00  C8 9D 0A 00 00 00 00 00  ........ȝ ......
0000000000047DB0  D0 9D 0A 00 00 00 00 00  D8 9D 0A 00 00 00 00 00  Н ......؝ ......
0000000000047DC0  E0 9D 0A 00 00 00 00 00  E8 9D 0A 00 00 00 00 00  ................
0000000000047DD0  F0 9D 0A 00 00 00 00 00  34 9C 02 00 00 00 00 00  ........4.......
0000000000047DE0  F8 9D 0A 00 00 00 00 00  00 9E 0A 00 00 00 00 00  ................
0000000000047DF0  08 9E 0A 00 00 00 00 00  10 9E 0A 00 00 00 00 00  ................
0000000000047E00  18 9E 0A 00 00 00 00 00  20 9E 0A 00 00 00 00 00  ........ .......
0000000000047E10  E8 9B 02 00 00 00 00 00  28 9E 0A 00 00 00 00 00  ........(.......
0000000000047E20  48 85 02 00 00 00 00 00  C8 97 02 00 00 00 00 00  H.......ȗ ......

跳过去看一下..

啥也没有…欸, 到这里又不懂了, AI说这里在连接时会由链接器动态填充.. 算了假装懂了吧..

继续解决之前的疑问: PLT与Dynamic段到底有何关系…

1
2
3
4
5
6
7
8
9
10
11
12
AI
PLT 是代码,放在代码段;GOT 是数据,放在数据段;Dynamic 段是指南针,告诉动态链接器去哪里找重定位信息来填充 GOT。
第一个 LOAD 段 (0x0 - 0x37000, R . X . L)
这是一个 可读、可执行 的段。
它必然包含了所有需要执行的代码节,即 .text 和 .plt。
结论:PLT 表(那些 imp_xxx 函数的代码体)就定义在这个段里。 你在反汇编窗口看到的 __sprintf_chk 的 ADRP/LDR/BR 代码就在这里。
第二个 LOAD 段 (0x46000 - 0xA9B00, R W . . L)
这是一个 可读、可写 的段。
它包含了所有可写的数据节,例如 .data, .bss, .got, .got.plt, 以及 .dynamic。
结论:
GOT 表(包括 0xA9080 这个槽位)就在这里。
Dynamic 段(0x47650-0x47890)也在这里。

…..

就是说,

  • 第一个加载段就是0x0h -37000h,
  • 第二个加载段就是0x46000-0xa9b00,

现在我才发现, IDA应该是模拟加载了第一段和第二段, 因为HEX中并没有0x37000-0x46000. 看看文件中这一部分长什么样子..

结果全是这样的00填充…那so文件生成时为什么不直接省略掉呢? 不管了..无法深究..

  • 第三个加载段就是0x47650-0x47890,

那这第三个段真是Dynamic段..

  • 第四个加载段就是0x311E8-0x3212c,

  • 第五个加载段就是0x0 -0x0,
  • 第六个加载段就是0x36F80-0x38000, 全是00字节..

并且第三段是完全重合在第二段中的…第四段完全重合在第一段里..乱的一批….

接着分析Dynamic段的结构体

1
2
3
4
5
struct Elf64_Dyn 
 {                                      
	unsigned __int64 d_tag;
	unsigned __int64 d_un;
 };

AI细说

1
2
3
4
5
6
7
typedef struct {
    Elf64_Sxword d_tag; // 条目类型 (Signed, 因为有 DT_NULL)
    union {
        Elf64_Xword d_val; // 当 d_tag 需要一个整数值(如大小、数量、标志)
        Elf64_Addr  d_ptr; // 当 d_tag 需要一个虚拟地址(指向某个表或字符串)
    } d_un;
} Elf64_Dyn;

1. 依赖库声明 (DT_NEEDED)
1
2
3
1LOAD:0000000000047650 stru_47650      Elf64_Dyn <1, 0x1E3F>   ; DT_NEEDED liblog.so
2...
3LOAD:00000000000476A0                 Elf64_Dyn <1, 0x1E6A>   ; DT_NEEDED libstdc++.so
  • d_tag = 1: 这是 DT_NEEDED
  • d_un = 0x1E3F, 0x1E49...: 这些是 d_val,代表偏移量
  • 作用: 动态链接器会拿着这些偏移量(如 0x1E3F),去 .dynstr (动态字符串表) 中查找对应的字符串。例如,在 .dynstr 表的 0x1E3F 偏移处,存放着 "liblog.so" 这个字符串。这告诉链接器:“我需要加载 liblog.so 这个库”。

2. 库自身名称 (DT_SONAME)
1
1LOAD:00000000000476B0                 Elf64_Dyn <0xE, 0x1EAC> ; DT_SONAME libmsaoaidsec.so
  • d_tag = 0xE (14): 这是 DT_SONAME
  • d_un = 0x1EAC: 这是一个 d_val (偏移量)。
  • 作用: 指向 .dynstr 表中的一个字符串,该字符串是此共享库自身的“逻辑名”(libmsaoaidsec.so)。当其他程序链接到这个库时,它们记录的是这个名字,而不是文件在磁盘上的完整路径。

3. 初始化/终止函数 (DT_INITDT_INIT_ARRAYDT_FINI_ARRAY)
1
2
3
4
5
6
1LOAD:00000000000476C0                 Elf64_Dyn <0xC, 0x14400> ; DT_INIT
2LOAD:00000000000476D0                 Elf64_Dyn <0x19, 0x46F80> ; DT_INIT_ARRAY
3LOAD:00000000000476E0                 Elf64_Dyn <0x1B, 0x30>  ; DT_INIT_ARRAYSZ
4...
5LOAD:00000000000476F0                 Elf64_Dyn <0x1A, 0x46FB0> ; DT_FINI_ARRAY
6LOAD:0000000000047700                 Elf64_Dyn <0x1C, 0x10>  ; DT_FINI_ARRAYSZ
  • DT_INIT (0xC)d_un = 0x14400 是一个 d_ptr,指向一个初始化函数的地址。在 main 函数执行前会被调用。
  • DT_INIT_ARRAY (0x19)d_un = 0x46F80 是一个 d_ptr,指向一个函数指针数组的起始地址。数组中的每个函数都会在 main 之前被依次调用。
  • DT_INIT_ARRAYSZ (0x1B)d_un = 0x30 是一个 d_val,表示 INIT_ARRAY 数组的总大小(字节)。0x30 / 8 = 6 个函数指针。
  • DT_FINI_ARRAY 和 DT_FINI_ARRAYSZ: 作用与 INIT 相反,在程序正常退出时被调用。

4. 符号与字符串表信息 (DT_HASHDT_STRTABDT_SYMTABDT_STRSZDT_SYMENT)
1
2
3
4
5
6
1LOAD:0000000000047710                 Elf64_Dyn <4, 0x190>    ; DT_HASH
2LOAD:0000000000047720                 Elf64_Dyn <5, 0x3050>   ; DT_STRTAB
3...
4LOAD:0000000000047740                 Elf64_Dyn <6, 0x1208>   ; DT_SYMTAB
5LOAD:0000000000047750                 Elf64_Dyn <0xA, 0x9C60> ; DT_STRSZ
6LOAD:0000000000047760                 Elf64_Dyn <0xB, 0x18>   ; DT_SYMENT
  • DT_HASH (4)d_un = 0x190 是 d_ptr,指向符号哈希表的地址,用于加速符号查找。
  • DT_STRTAB (5)d_un = 0x3050 是 d_ptr,指向 .dynstr (动态字符串表) 的虚拟地址。
  • DT_SYMTAB (6)d_un = 0x1208 是 d_ptr,指向 .dynsym (动态符号表) 的虚拟地址。
  • DT_STRSZ (0xA)d_un = 0x9C60 是 d_val,表示 .dynstr 表的总大小。
  • DT_SYMENT (0xB)d_un = 0x18 是 d_val,表示 .dynsym 表中每个符号项 (Elf64_Sym) 的大小(24字节)。

位于0x1208DT_SYMTAB长这个样子..

24字节..

虽然叫符号表, 但是Elf64_Sym(动态符号表项)本身并不直接包含符号名称,而是通过一个索引(偏移)引用 DT_STRTAB(动态字符串表)中的实际字符串。

AI解析结果如下

字段偏移字节(小端序)十六进制值十进制值含义
st_name0–3E9 01 00 000x000001E9489符号名在 .dynstr 中的偏移
st_info4120x1218绑定 + 类型
st_other5000x000保留/可见性
st_shndx6–700 000x00000 (SHN_UNDEF)未定义符号(导入)
st_value8–1500 00 00 00 00 00 00 000x00000000000000000运行时地址(尚未重定位)
st_size16–2300 00 00 00 00 00 00 000x00000000000000000符号大小(如函数长度)

.synstr的基址是0x3050, 那么该符号表项对应的字符的地址是0x3239

正是__sprintf_chk, 对于导入函数来说, st_value无意义. 因为现在处于Dynamic段解析时间, 是链接器来读取信息. 链接器没有任何理由要求你提供你本就没有的东西..所以, st_value对于导出函数来说, 有意义, 因为其他模块可能依赖了本模块的某个函数, 所以需要链接器连查找函数的实现地址..

为了证明, 我应该找找这个so中的导出函数(JNI_Onload)..那就看看导出表..

地址是0x013a4c, 符号大小是0x11c

字段字节(小端)值(十六进制)值(十进制)含义
st_name1D 00 00 000x0000001D29符号名在 .dynstr 中的偏移
st_info120x1218STB_GLOBAL \| STT_FUNC(全局函数)
st_other000x000默认可见性
st_shndx0A 000x000A10节区索引 = 10(通常是 .text)→ 本模块定义的函数 
st_value4C 3A 01 00 00 00 00 000x0000000000013A4C80,460函数在节区内的偏移地址 
st_size1C 01 00 00 00 00 00 000x000000000000011C284✅ 符号大小 = 284 字节“符号大小”就是 JNI_OnLoad 函数在二进制中占用的字节数,用于调试、分析和工具支持,但不影响动态链接过程。

看看从0x0000000000013A4C0x0000000000013CD0到底是不是刚好圈定了JNI_Onload函数.

看来并没有…但AI也说, 这个并非100%精确. 不重要…

💡 注意: 你看到多个 DT_STRTAB 条目(0x477200x477300x47800…)是正常的。链接器或某些工具可能会生成冗余条目,但动态链接器通常只使用第一个有效的。


5. 重定位信息 (DT_PLTGOTDT_JMPRELDT_PLTRELSZDT_RELADT_RELASZ)
1
2
3
4
5
6
1LOAD:0000000000047770                 Elf64_Dyn <3, 0x47890>  ; DT_PLTGOT
2LOAD:0000000000047780                 Elf64_Dyn <2, 0x11D0>   ; DT_PLTRELSZ
3LOAD:0000000000047790                 Elf64_Dyn <0x14, 7>     ; DT_PLTREL
4LOAD:00000000000477A0                 Elf64_Dyn <0x17, 0x6620> ; DT_JMPREL
5LOAD:00000000000477B0                 Elf64_Dyn <7, 0x51C8>   ; DT_RELA
6LOAD:00000000000477C0                 Elf64_Dyn <8, 0x1458>   ; DT_RELASZ
  • DT_PLTGOT (3)d_un = 0x47890 是 d_ptr,指向 GOT (Global Offset Table) 表的起始地址。PLT 代码通过 GOT 实现间接跳转。

.got.plt 导入函数槽

1
.got.plt 中的每个条目(从第4个开始)最终会存放外部库函数(导入函数)的真实地址,但在程序刚加载、尚未调用该函数时,它并不直接存真实地址,而是存一个“桩地址”(指向 PLT 的跳转指令)。

这里.got.plt表第4个条目存储的是0xa9b00

IDA解析后是对应这个函数..

我想知道现在.got.plt槽位具体指向什么东西…

啊? 搞不明白为什么.got.plt表本就是存放函数地址的地方, 现在又指向另一个存放函数地址的地方?? 反正目前.got.plt表中存储的不是真实函数地址, 而是指向了文件尾部的完全空的槽位..

想看看最后一个.got.plt函数指向哪里..大概是这个

指向了0x47550

见鬼了, 居然不是文件末尾,,,我再看最后一个粉色的函数

指向了0xa9e80

还真是最后一个!!! 那这些蓝色的函数到底为什么会出现在.got.plt表中呢?

随便找了一个看看

难道是导出函数? 去导出表看看…搜索后发现地址确实符合该函数

总结一下, .got.plt表中存储了导入函数和导出函数, 并且导入函数统一指向了so文件的末尾各自的槽位.. 然后导入函数通知指向了so文件内的导出函数的地址..但要注意, 目前该so文件并没有被加载到内存中.. 情况可能不一样.. 我需要在二进制文件中手动定位.got.plt表.查找非0a____开头的值. 看它具体指向哪儿..所以先使用010editor定位到.got.plt表的位置..根据已有知识, .got.plt表的起始位置在Dynamic段中有所记录.. 先找到Dynamic段基址..

跳到0x0000000000047770

查看第一个项目

1
03 00 00 00 00 00 00 00 90 78 04 00 00 00 00 00

前8个字节03 00 00 00 00 00 00 00是标志, 决定如何解析后面的8个字节.03表示后面的8字节解析为.got.plt表的地址, 按小端序读取也就是0x0000000000047890, 跳到这个位置看看..

确实是.got.plt表的特点, 前3项为空, 第4项开始才存放函数..有趣的事情来了.. 文件里面的情况和IDA不一样…IDA指向了不同的地方, 要么文件末尾, 要么文件中间部分.. 而当前文件中, 确实统一指向了0x77f0, 跳过去看看..

感觉是个函数, 换IDA看看..

感觉这才是真正的桩函数吧…分析看看..

1
2
3
4
__int64 sub_77F0()
{
  return qword_478A0();
}

没有传入任何参数, 并且内部又调用了拎一个函数, 那桩函数是如何定位当前需要调用的外部函数呢? 先不管, 继续分析qword_478A0()

我擦? 兜兜转转又跳回了got表?? 不能看IDA, 应该是IDA给我解析好了.. 回到010Editor看0x478A0的具体情况..

啥也没有啊啊…完了问题越来越多了..继续看0x77f0的函数, 确保没那么简单..

1
2
F0 7B BF A9 10 02 00 90 11 52 44 F9 10 82 22 91 
20 02 1F D6 1F 20 03 D5 1F 20 03 D5 1F 20 03 D5
偏移原始字节(小端)重组后(大端表示,用于查 opcode)功能用途描述
0x00F0 7B BF A9A9 BF 7B F0stp x16, x30, [sp, #-16]!功能:将 x16(IP0)和 x30(链接寄存器 LR)压入栈,并将栈指针减 16。
目的:保存返回地址(LR)和临时寄存器(x16),为后续调用做准备。
0x0410 02 00 9090 00 02 10adrp x16, #page功能:将当前 PC 的页基址 + 相对偏移加载到 x16。
典型用途:计算 .got.plt 所在页的基地址
0x0811 52 44 F9F9 44 52 11ldr x17, [x16, #offset]功能:从 x16 + 0x888 处加载一个 8 字节值到 x17。
关键点:这个地址就是 .got.plt 中对应本函数的条目地址!
即:x16 + 0x888 = &(.got.plt[n])
初始时,该内存中存的是下一条指令的地址(即 PLT[n] + 12),用于首次跳转到解析器。
0x0C10 82 22 9191 22 82 10add x16, x16, #offset功能:计算完整的 .got.plt[n] 地址(虽然此处未使用,可能是冗余或用于调试?)
实际上,在标准 PLT 中,这一步常用于传递重定位索引,但此处更可能是为了对齐或兼容。
0x1020 02 1F D6D6 1F 02 20br x17功能:无条件跳转到 x17 指向的地址。
行为:
首次调用:x17 = PLT[n] + 12 → 跳转到 PLT 表头,触发 _dl_runtime_resolve
后续调用:x17 = 真实函数地址 → 直接跳转到目标函数
0x141F 20 03 D5nop16字节对齐
0x181F 20 03 D5nop16字节对齐
0x1C1F 20 03 D5nop16字节对齐

原来就是IDA里面的桩函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
LOAD:00000000000077F0
LOAD:00000000000077F0 ; =============== S U B R O U T I N E =======================================
LOAD:00000000000077F0
LOAD:00000000000077F0
LOAD:00000000000077F0 ; __int64 sub_77F0()
LOAD:00000000000077F0 sub_77F0
LOAD:00000000000077F0
LOAD:00000000000077F0 var_10          = -0x10
LOAD:00000000000077F0 var_8           = -8
LOAD:00000000000077F0
LOAD:00000000000077F0                 STP             X16, X30, [SP,#var_10]!
LOAD:00000000000077F4                 ADRP            X16, #qword_478A0@PAGE
LOAD:00000000000077F8                 LDR             X17, [X16,#qword_478A0@PAGEOFF]
LOAD:00000000000077FC                 ADD             X16, X16, #qword_478A0@PAGEOFF
LOAD:0000000000007800                 BR              X17
LOAD:0000000000007800 ; End of function sub_77F0
LOAD:0000000000007800
LOAD:0000000000007800 ; ---------------------------------------------------------------------------

F5查看的反汇编伪C代码却看不出端倪.. 因为这段汇编确实能定位到具体的.got.plt槽位..

总结了一下, 业务代码里调用的外部函数实际都指向plt表, 而plt表里的函数将会读取对应.got.plt槽位里的地址, 并且跳转到该地址的代码.. 如果是第一次调用plt表里的函数, 则会由plt里的函数主导, 跳转到0x77f0桩函数, 桩函数会根据plt表函数压入的信息跳转到指定函数去查找对应的真实函数, 保存到got表中.

我懂了, got表只是plt表的一个挂件/扩展..可是plt表有在哪儿有记录呢? 接着往下看

rela.plt重定位表

  • DT_JMPREL (0x17)d_un = 0x6620 是 d_ptr,指向 .rela.plt 表的地址。这个表包含了所有需要通过 PLT 进行延迟绑定的函数调用的重定位信息。
  • DT_PLTRELSZ (2)d_un = 0x11D0 是 d_val,表示 .rela.plt 表的大小。

由上面的信息可以推断出: .rela.plt的地址区间是 0x6620-0x77f0, 这个0x77f0好眼熟, 不就是桩函数的起始位置嘛, 也就是说, .rela.plt表与.plt表完全拼接在一起的.. 但.got.plt位于0x47770起始于0x47890, 离得老远了… Dynamic段中并没有明确标注.plt表的起始位置, 但是很明显, .got.plt存储的初始值就是指向了.plt表的第一个函数, 桩函数.. 回答我自己的问题:可是plt表有在哪儿有记录呢?,不重要, .got.plt表中自会找到桩函数, 或者通过.rela.plt也可以定位桩函数的起始位置..

接着深入理解.rela.plt表的作用. 按这个名命方式, .rela.plt表应该也属于.plt表的挂件..

Elf64_Rela结构体

1
2
3
4
5
typedef struct {
    Elf64_Addr   r_offset;   // 8 bytes: 需要重定位的地址(在 GOT 中)
    Elf64_Xword  r_info;     // 8 bytes: 符号索引 + 重定位类型
    Elf64_Sxword r_addend;   // 8 bytes: 加数(addend)
} Elf64_Rela;
1
2
3
A8 78 04 00  00 00 00 00
02 04 00 00  0A 00 00 00
00 00 00 00  00 00 00 00
字段值(十六进制)含义
r_offset0x00000000000478A8.got.plt 中某函数槽的地址
r_info0x0000000A00000401(应为)符号索引 = 10,类型 = R_AARCH64_JUMP_SLOT (1025)
r_addend0x0无额外偏移

我问AI:不是说.got.plt函数槽是懒加载的嘛.而且.plt表中已有函数能直接读取.got.plt的值. 为什么还要重定位表来执行.plt表中的函数的任务?

AI的话不能随便信,

不同的外部函数/导入函数, 都有对应的独一无二的plt函数..

如图, 除箭头处的桩函数外, 剩下的每一行全都是plt函数.. 都不一样.. 但这方面没有任何文章/博客体到, 或者大部分文章所描述的都是错的, 才导致AI的答复也是错的.. 在世界这个巨大的草台班子上面, 也有一群不懂装懂的草包…

指令是否变化原因
1. adrp x16, page通常不变所有 GOT 条目在同一内存页
2. ldr x17, [x16, #off]变化每个函数的 GOT 条目偏移不同
3. add x16, x16, #off变化传递正确的 GOT 地址给解析器
4. br x17不变统一跳转

AI又在吹牛逼了…服了.. so文件里.plt处的机器码, 它都能说成是根据.rela.plt表决定的..然而并不是在链接时生成的, 而是直接存在于so文件里面的…..

总结性理解: .got.plt 负责在存储外部函数的真实地址(或初始解析桩),
而 .rela.plt 负责在懒加载函数阶段提供 .got.plt 中每个槽位的元信息——包括该槽位的内存位置(r_offset)和对应的函数符号(通过 r_info 指向 .dynsym)。

组件存什么?谁用?何时用?
.plt可执行的跳转代码(通用模板)CPU每次调用外部函数时
.got.plt函数指针数组
• 初始值 = 解析桩地址
• 首次调用后 = 真实函数地址
PLT 代码(ldr x17, [x16, #off]运行时读写
.rela.plt重定位项数组
r_offset = .got.plt[n] 的地址
r_info = (符号索引, JUMP_SLOT
动态链接器(_dl_runtime_resolve首次调用函数时
.dynsym动态符号表(含符号名在 .dynstr 中的偏移、类型等)动态链接器解析时查符号
.dynstr动态符号字符串表(如 "printf\0malloc\0"动态链接器获取函数名

.rela.plt只在.plt中的函数被调用时才起作用, 接下来以位于0x8180__sprintf_chkplt函数为例, 尝试深入研究.plt表是如何利用.rela.plt表定位.got.plt表的具体槽位..

1
F0 01 00 F0 11 B2 46 F9  10 82 35 91 20 02 1F D6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
F0 01 00 F0​ : ADRP X16, #offset

计算目标函数地址所在GOT页的基址,存入X16寄存器。

11 B2 46 F9​ : LDR X17, [X16, #0x8D0]

从GOT中加载目标函数的绝对地址到X17寄存器。这里的立即数偏移为 0x8D0。

10 82 35 91​ : ADD X16, X16, #0x8D0

将X16寄存器的值增加 0x8D0。这个调整后的值通常会被下一条PLT条目复用,以优化指令编码。

20 02 1F D6​ : BR X17

跳转到X17寄存器中保存的目标函数地址执行。

确实没有看到任何调用.rela.plt表的痕迹..并且似乎汇编语言并不能直接直接反编译出函数具体的名称, 看来这是IDA处理的.. 这段代码包括.got.plt表都是会被直接加载到进程空间的. 读取文件内地址无用. 记录目标函数到.got.plt表起点的偏移才是有用的. 那么0x8D0就是函数__sprintf_chk.got.plt中的偏移. 验证一下, 实际上并没有…看看.rela.plt中的偏移呢.欸, 这个偏移到底代表什么??

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
LOAD:0000000000047650 stru_47650      Elf64_Dyn <1, 0x1E3F>   ; DATA XREF: LOAD:off_C0↑o
LOAD:0000000000047650                                         ; sub_11A60+210↑o ...
LOAD:0000000000047650                                         ; DT_NEEDED liblog.so
LOAD:0000000000047660                 Elf64_Dyn <1, 0x1E49>   ; DT_NEEDED libdl.so
LOAD:0000000000047670                 Elf64_Dyn <1, 0x1E52>   ; DT_NEEDED libz.so
LOAD:0000000000047680                 Elf64_Dyn <1, 0x1E5A>   ; DT_NEEDED libc.so
LOAD:0000000000047690                 Elf64_Dyn <1, 0x1E62>   ; DT_NEEDED libm.so
LOAD:00000000000476A0                 Elf64_Dyn <1, 0x1E6A>   ; DT_NEEDED libstdc++.so
LOAD:00000000000476B0                 Elf64_Dyn <0xE, 0x1EAC> ; DT_SONAME libmsaoaidsec.so
LOAD:00000000000476C0                 Elf64_Dyn <0xC, 0x14400> ; DT_INIT
LOAD:00000000000476D0                 Elf64_Dyn <0x19, 0x46F80> ; DT_INIT_ARRAY
LOAD:00000000000476E0                 Elf64_Dyn <0x1B, 0x30>  ; DT_INIT_ARRAYSZ
LOAD:00000000000476F0                 Elf64_Dyn <0x1A, 0x46FB0> ; DT_FINI_ARRAY
LOAD:0000000000047700                 Elf64_Dyn <0x1C, 0x10>  ; DT_FINI_ARRAYSZ
LOAD:0000000000047710                 Elf64_Dyn <4, 0x190>    ; DT_HASH
LOAD:0000000000047720                 Elf64_Dyn <5, 0x3050>   ; DT_STRTAB
LOAD:0000000000047730                 Elf64_Dyn <5, 0x3050>   ; DT_STRTAB
LOAD:0000000000047740                 Elf64_Dyn <6, 0x1208>   ; DT_SYMTAB
LOAD:0000000000047750                 Elf64_Dyn <0xA, 0x9C60> ; DT_STRSZ
LOAD:0000000000047760                 Elf64_Dyn <0xB, 0x18>   ; DT_SYMENT
LOAD:0000000000047770                 Elf64_Dyn <3, 0x47890>  ; DT_PLTGOT
LOAD:0000000000047780                 Elf64_Dyn <2, 0x11D0>   ; DT_PLTRELSZ
LOAD:0000000000047790                 Elf64_Dyn <0x14, 7>     ; DT_PLTREL
LOAD:00000000000477A0                 Elf64_Dyn <0x17, 0x6620> ; DT_JMPREL
LOAD:00000000000477B0                 Elf64_Dyn <7, 0x51C8>   ; DT_RELA
LOAD:00000000000477C0                 Elf64_Dyn <8, 0x1458>   ; DT_RELASZ
LOAD:00000000000477D0                 Elf64_Dyn <9, 0x18>     ; DT_RELAENT
LOAD:00000000000477E0                 Elf64_Dyn <0x1E, 8>     ; DT_FLAGS
LOAD:00000000000477F0                 Elf64_Dyn <0x6FFFFFFB, 1> ; DT_FLAGS_1
LOAD:0000000000047800                 Elf64_Dyn <5, 0x3050>   ; DT_STRTAB
LOAD:0000000000047810                 DCQ 5                   ; d_tag ; DT_STRTAB
LOAD:0000000000047818                 DCQ 0x3050              ; d_un
LOAD:0000000000047820                 Elf64_Dyn <5, 0x3050>   ; DT_STRTAB
LOAD:0000000000047830                 Elf64_Dyn <0x6FFFFFF9, 0x50> ; DT_RELACOUNT
LOAD:0000000000047840                 Elf64_Dyn <0>           ; DT_NULL

再看看AI描述IDA解析so做的关于__sprintf_chk事情

1
2
3
4
5
6
IDA(或其他高级反汇编器如 Ghidra)会:
解析 ELF 的重定位表(.rela.plt 或 .rela.dyn)。
找到地址 0x8180 处的 ADRP 指令对应的重定位记录。
例如:R_AARCH64_ADR_GOT_PAGE 或 R_AARCH64_LD64_GOT_LO12_NC
该重定位记录会指向一个 动态符号(如 _sprintf_chk)。
IDA 将 .got 中对应条目的地址(比如 0x47D60)命名为 off_47D60,并关联到 _sprintf_chk。

确实, rela.plt表中存在__sprintf_chk相关信息..

1
2
3
LOAD:0000000000007448 60 7D 04 00 00 00 00 00                             DCQ 0x47D60             ; r_offset ; R_AARCH64_JUMP_SLOT __sprintf_chk
LOAD:0000000000007450 02 04 00 00 62 00 00 00                             DCQ 0x6200000402        ; r_info
LOAD:0000000000007458 00 00 00 00 00 00 00 00                             DCQ 0                   ; r_addend

1
2
3
4
5
typedef struct {
    Elf64_Addr   r_offset;   // 8 bytes: 需要重定位的地址(在目标文件中的偏移)
    Elf64_Xword  r_info;     // 8 bytes: 符号索引 + 重定位类型
    Elf64_Sxword r_addend;   // 8 bytes: 加数(显式加数,用于 RELA 类型)
} Elf64_Rela;
字段含义
r_offset0x47D60重定位目标地址(通常是 .got.got.plt 中的一项)
r_info0x0000006200000402 
– 符号索引98.dynsym 中第 98 个符号(从 0 开始)
– 重定位类型1026 (R_AARCH64_JUMP_SLOT)动态链接时填入函数真实地址
r_addend0无额外偏移

但这里并没有看出.rela.plt表于.plt表的直接联系. 迷茫了…先让AI持续分析这个机器码, 看看是不是AI的问题. 也就是__sprintf_chk对应的plt函数

1
2
F0 01 00 F0  11 B2 46 F9
10 82 35 91  20 02 1F D6

IDA解析的

1
2
3
4
ADRP            X16, #off_47D60@PAGE
LDR             X17, [X16,#off_47D60@PAGEOFF]
ADD             X16, X16, #off_47D60@PAGEOFF
BR              X17 ; __imp___sprintf_chk

AI的第一次解析, 初见端倪..

1
2
3
4
ADRP    X16, #0x1000          ; 计算页地址到X16
LDR     X17, [X16, #0xD58]    ; 从[X16+0xD58]加载数据到X17
ADD     X16, X16, #0xD60      ; X16 = X16 + 0xD60
RET     X17                   ; 从X17返回

和第一次居然不一样, 看来AI不可信…

1
2
3
4
ADRP  X0, #0x1000          ; 计算页基址
LDR   X17, [X18, #0x11AC]  ; 从内存加载数据
ADD   X16, X2, #0xD60      ; 地址计算
RET                        ; 返回调用者

我选择相信IDA的反汇编能力, 应该是能直接定位到.got表的基址, 然后计算正确的偏移, 定位到.got.plt槽. 至于.rela.plt的调用, 应该是.got.plt表未初始化, 默认指向桩函数时才会让链接器读取.rela.plt信息, 才能再次定位到.got.plt槽..

AI又来拍马屁了….艹…

  • DT_PLTREL (0x14)d_un = 7 是 d_val,表示 .rela.plt 表使用的重定位类型(7 代表 R_X86_64_JUMP_SLOT 或 ARM64 上的类似类型)。
  • DT_RELA (7)d_un = 0x51C8 是 d_ptr,指向 .rela.dyn 表的地址。这个表包含了所有非函数类型的重定位信息(如全局变量)。
  • DT_RELASZ (8)d_un = 0x1458 是 d_val,表示 .rela.dyn 表的大小。

非懒加载的/非函数的全局变量表的重定位表


6. 终止符 (DT_NULL)
1
1LOAD:0000000000047840                 Elf64_Dyn <0>           ; DT_NULL
  • d_tag = 0: 这是 DT_NULL
  • 作用: 动态链接器看到这个条目就知道 .dynamic 数组到此结束,停止解析。

深度补习so相关部分知识后, 我要尝试使用frida hook libmsaoaidsec.so.got.plt里面的__sprintf_chk函数地址..

妈蛋给AI问无语了…已读不回…看来这个方法行不通. 只能按依赖库一个一个试了..

1
2
3
4
5
6
LOAD:0000000000047650                                         ; DT_NEEDED liblog.so
LOAD:0000000000047660                 Elf64_Dyn <1, 0x1E49>   ; DT_NEEDED libdl.so
LOAD:0000000000047670                 Elf64_Dyn <1, 0x1E52>   ; DT_NEEDED libz.so
LOAD:0000000000047680                 Elf64_Dyn <1, 0x1E5A>   ; DT_NEEDED libc.so
LOAD:0000000000047690                 Elf64_Dyn <1, 0x1E62>   ; DT_NEEDED libm.so
LOAD:00000000000476A0                 Elf64_Dyn <1, 0x1E6A>   ; DT_NEEDED libstdc++.so
1
2
3
4
5
6
7
8
import {hook_do_dlopen, log} from "./logger.js";  
  
hook_do_dlopen();  
Interceptor.attach(Module.getGlobalExportByName("__sprintf_chk"), {  
    onEnter: function(args) {  
        log("__sprintf_chk called: [" + args[1].readUtf8String()+"]")  
    }  
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
13672-13672 do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so]
13672-13672 __sprintf_chk called: [null]
13672-13672 do_dlopen called: [libc.so]
13672-13672 __sprintf_chk called: [null]
13672-13672 do_dlopen called: [libc.so]
13672-13752 __sprintf_chk called: [null]
13672-13752 __sprintf_chk called: [null]
13672-13752 __sprintf_chk called: [null]
13672-13752 __sprintf_chk called: [null]
13672-13752 __sprintf_chk called: [null]
13672-13752 __sprintf_chk called: [null]
13672-13752 __sprintf_chk called: [null]
13672-13752 __sprintf_chk called: [null]
13672-13752 __sprintf_chk called: [null]
13672-13752 __sprintf_chk called: [null]
13672-13752 __sprintf_chk called: [null]
Process terminated
[PKG110::com.leleketang.SchoolFantasy ]->

啊, 今天怎么就能找到了呢??不对啊, 不管了, 尝试直接查找proc字符, 逆向找到可疑函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void __noreturn sub_1B8D4()
{
  unsigned int v0; // w0
  __useconds_t v1; // w19
  int v2; // w0

  v0 = sub_CB28();
  if ( v0 >= 0x64 )
    v1 = v0;
  else
    v1 = 2000000;
  while ( 1 )
  {
    v2 = sub_1AE48();
    if ( v2 == -1 || v2 && (sub_1AB54() & 1) == 0 || (unsigned int)sub_1B730() == 777 )
      sub_11FA4();
    usleep(v1);
  }
}

直接hook字符比较函数..

1
2
3
4
5
6
7
8
25334-25386 strstr called: [Name:       gmain,gum-js-loop]
25334-25386 strstr called: [Name:       gmain,gmain]
12326-12409 strstr called: [/system/framework/boot-framework-adservices.vdex , XposedBridge.jar]
12326-12409 strstr called: [/system/framework/boot-framework-adservices.vdex , eddexmaker.jar]
12326-12409 strstr called: [/system/framework/boot-framework-adservices.vdex , eddalvikdx.jar]
12326-12409 strstr called: [/system/framework/boot-framework-adservices.vdex , edxp.jar]
12326-12409 strstr called: [/system/framework/boot-framework-adservices.vdex , io.va.exposed]
12326-12409 strstr called: [/system/framework/boot-framework-adservices.vdex , me.weishu.exp]

本想看看为什么还能读取原来的字符, 结果

吓哭了.. 接着hook看结果吧. 似乎结果好一点了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
21904-21904 do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so]
21904-21904 do_dlopen called: [libc.so]
21904-21904 do_dlopen called: [libc.so]
Error: access violation accessing 0x7083eb4000
    at onLeave (agent/logger.ts:139)                                               
[BYPASS] strstr(..., "gum-js-loop") → NULL
---
[BYPASS] strstr(..., "gmain") → NULL
21904-21904 do_dlopen called: [liblog.so]
21904-21904 do_dlopen called: [libc.so]
21904-21904 do_dlopen called: [libm.so]
21904-21904 do_dlopen called: [libstdc++.so]
21904-21904 do_dlopen called: [libdl.so]
[BYPASS] strstr(..., "gum-js-loop") → NULL
[BYPASS] strstr(..., "gmain") → NULL
21904-21904 do_dlopen called: [null]
[BYPASS] strstr(..., "gum-js-loop") → NULL
[BYPASS] strstr(..., "gmain") → NULL
[BYPASS] strstr(..., "gum-js-loop") → NULL
[BYPASS] strstr(..., "gmain") → NULL
[BYPASS] strstr(..., "gum-js-loop") → NULL
[BYPASS] strstr(..., "gmain") → NULL

相比于没有修改比较返回值的情况:

1
2
3
4
5
6
7
8
9
10
25905-25905 do_dlopen called: [/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so]
25905-25905 do_dlopen called: [libc.so]
25905-25905 do_dlopen called: [libc.so]
[BYPASS] strstr(..., "gum-js-loop") → NULL
[BYPASS] strstr(..., "gmain") → NULL
---
[BYPASS] strstr(..., "gum-js-loop") → NULL
Process terminated
[PKG110::com.leleketang.SchoolFantasy ]->

也就是说, 修改返回值后, 成功过掉了初始化函数里面的检测, 成功加载了一下模块

1
2
3
4
5
26874-26874 do_dlopen called: [liblog.so]
26874-26874 do_dlopen called: [libc.so]
26874-26874 do_dlopen called: [libm.so]
26874-26874 do_dlopen called: [libstdc++.so]
26874-26874 do_dlopen called: [libdl.so]

重新整理了思路, 就是替换启动线程的目标函数. 和替换崩溃函数

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
  
export function log(message: any): void {  
    console.log(Process.id + "-" + Process.getCurrentThreadId() + " " + message);  
}  
  
function getLinker() {  
    //用Find可能找不到, 麻烦, 直接用get  
    if (Process.pointerSize === 4) {  
        return Process.getModuleByName("linker");  
    }  
    return Process.getModuleByName("linker64")  
}  
  
function getFuncAddr(linker: Module, fun: string): NativePointerValue {  
    for (const enumerateSymbol of linker.enumerateSymbols()) {  
        if (enumerateSymbol.name.includes(fun)  
            && !enumerateSymbol.name.includes("__uniq")) {//排除__uniq函数, hook了不生效  
            log("found function: " + enumerateSymbol.name + " in [" + linker.name + "]")  
            return enumerateSymbol.address;  
        }  
    }  
    ;  
    console.log("cannot find function: " + fun + " in [" + linker.name + "]")  
    return NULL;  
}  
  
  
  
let map = new Map<string, Map<number,null>>();  
function replaced(module_name: string, address: number) {  
    if (map.has(module_name)) {//先看有没有存过  
        //有  
        log(`发现已保存的数据, 返回`);  
        let has = map.get(module_name)?.has( address);  
        return has ;  
    }else {  
        //没有  
        log(`没保存过关于${module_name}的数据..现在存, 肯定是返回false啊`)  
        let mou = new Map<number,null>();  
        mou.set(address, null);  
        map.set(module_name, mou);  
        return false;  
    }  
}  
  
  
  
export function on_linker64_dlopen(so_name: string) {  
    let linker = getLinker();  
    let linker_do_dlopen_addr = getFuncAddr(linker, "do_dlopen");  
    Interceptor.attach(linker_do_dlopen_addr, {  
        onEnter: function (args) {  
            let module_name = args[0].readUtf8String();  
            if (module_name?.includes(so_name)) {//加载了目标so, 可以直接获取模块  
                log(`[linker_do_dlopen] ${module_name}`)  
                Interceptor.attach(Module.getGlobalExportByName("pthread_create"), {  
                    onEnter: function (args) {  
                        var module = Process.findModuleByAddress(args[2]);  
                        //在创建线程时, 如果创建的是目标so的线程, 那就替换线程执行的函数, 而目标so可以通过args[2]找到..  
                        if (module != null&&module.name.includes(so_name)){  
                            log(`[pthread_create] ${module.name}->${args[2].sub(module.base)}`);  
                            Interceptor.replace(args[2], new NativeCallback(function () {  
                                log('check is do nothing... ')  
                            }, "void", []));  
                            //检测模块此时已经被正常加载进来了..顺便hook崩溃函数  
                            if (!replaced(module_name,0x14824)){  
                                Interceptor.replace(Process.getModuleByName(module_name).base.add(0x14824),new NativeCallback(function () {  
                                    log('0x14824 do nothing... ')  
                                }, "void", []));  
                            }  
                        }  
                    },  
                });  
            }  
        },  
    })  
}

运行效果还行, 最起码能跑了

1
2
3
4
5
6
7
8
9
10
11
12
[PKG110::com.leleketang.SchoolFantasy ]-> 16014-16014 [linker_do_dlopen] /data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so
16014-16014 [pthread_create] libmsaoaidsec.so->0x1c544
16014-16014 没保存过关于/data/app/~~4a3zxVex62QXGM8tBHyF7A==/com.leleketang.SchoolFantasy-rGwZk0keiP3AoL0pAxrBmA==/lib/arm64/libmsaoaidsec.so的数据..现在存, 肯定是返回false啊
16014-16014 [pthread_create] libmsaoaidsec.so->0x1b8d4
16014-16014 发现已保存的数据, 返回
16014-16078 check is do nothing... 
16014-16014 [pthread_create] libmsaoaidsec.so->0x26e5c
16014-16014 发现已保存的数据, 返回
16014-16079 check is do nothing... 
16014-16014 0x14824 do nothing...
Failed to load script: timeout was reached
[PKG110::com.leleketang.SchoolFantasy ]-> 

脚本其实还不够完善, 应该在调用构造函数的时候, 查找内存中的模块…hook dlopen确实还是她早了, 模块都还没被加载, 应该hook call_constructors的

看来构造函数是不能被hook的…但检测就在构造里面啊.. 前面的拦截逻辑就在放行构造函数, 但拦截构造函数内创建检测线程的行为, 期间可以通过findModuleByName或者getModuleByAddress()随意获取目标模块的对象.. 但无论如何, 检测模块都必须调用系统函数才能进行检测, 所以盯紧调用的系统函数!!

好了, 函数已经封装好了, 在模块加载后尽早给机会hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {
    log,
    onEarlyModuleLoaded
} from "./logger.js";
onEarlyModuleLoaded('libmsaoaidsec',(module:Module)=>{
    Interceptor.replace(module.base.add(0x14824),new NativeCallback(function () {
        log('[bypass] func at 0x14824... ')
    }, "void", []));
    Interceptor.replace(module.base.add(0x1c544),new NativeCallback(function () {
        log('[bypass] func at 0x1c544... ')
    }, "void", []));
    Interceptor.replace(module.base.add(0x1b8d4),new NativeCallback(function () {
        log('[bypass] func at 0x1b8d4... ')
    }, "void", []));
    Interceptor.replace(module.base.add(0x26e5c),new NativeCallback(function () {
        log('[bypass] func at 0x26e5c... ')
    }, "void", []));
});

接下来搞定软件内证书信任问题…从今天起, 正式进入native层逆向!! #2026年4月26日

与网络请求有关的函数

1
2
3
LDataRequestTask *__fastcall LDataRequest::get(__int64 a1, unsigned __int8 *a2, __int64 a3, __int64 a4, __int64 a5)
void __fastcall LDataRequestTask::LDataRequestTask(LDataRequestTask *this)
void __fastcall LDataRequestTask::onRequest

已测试, 仅 能在mumu上安装root..屁用没有.. 还是调试不了.. 还卡

我现在一直像尝试使用ida动态调试so文件.. 总结一下踩过的坑

1
2
3
4
5
1.高版本android系统注定失败,无论是否修改xml中的debug属性
2.就算修改了, 也没用, 高版本安卓对ro.debuggable属性不怎么看重..
3.想要成功调试, 似乎必须有ddms的参与, 哪怕只是拿它看一下对应的进程调试端口...挺玄学的,迟早有一天弄清楚它的机制, 然后摆脱ddms

ida动态调试调试是有机会能成的. 我的搭配是ida9.0+android10,无线调试, 由于是低版本的缘故, 并不需要修改apk debug属性. 安装模块就能全局调试了..

我现在想写一个系统框架的lsp模块, 实现全局调试..

1
2
//关键解析器
frameworks/base/core/java/android/content/pm/PackageParser.java

追踪解析器的结果存放在哪儿

1
2
//应该是新版Android解析包的实际代码
frameworks/base/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java

学不动了, 就算追踪到代码也没心思改了… 先看老师给的论文吧 #2026年4月28日

接着看天天练的代码

1
LDataRequestTask::onRequest

这个方法中调用的sub_1126a98是设置证书验证的关键函数

这个千问AI总是导致IDEA自动全选整行, 关掉就行了

继续处理IDA中解析json相关的函数

1
LDataRequestTask::post(char const*,std::list<RequestParamData> const&,std::function<void ()(LDataRequestTask*)>)

第三个参数正是处理回调的函数. 但是没找到, 接着找post的调用函数

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
void __fastcall easyui::Package::startCheckUpdate(easyui::Package *this)
{
  __int64 v2; // x20
  __int64 v3; // x9
  __int64 v4; // x20
  LDataRequest *v5; // x0
  __int64 v6; // x9
  __int64 Instance; // x0
  void (*v8)(void); // x8
  __int64 v9; // x8
  __int64 *v10; // x19
  __int64 v11; // x10
  bool v12; // zf
  __int64 *v13; // x22
  __int64 v14; // [xsp+0h] [xbp-110h] BYREF
  __int64 *v15; // [xsp+8h] [xbp-108h]
  __int64 v16; // [xsp+10h] [xbp-100h]
  __int64 v17[3]; // [xsp+18h] [xbp-F8h] BYREF
  _QWORD v18[4]; // [xsp+30h] [xbp-E0h] BYREF
  _QWORD *v19; // [xsp+50h] [xbp-C0h]
  _BYTE v20[16]; // [xsp+68h] [xbp-A8h] BYREF
  void *ptr; // [xsp+78h] [xbp-98h]
  _BYTE v22[16]; // [xsp+80h] [xbp-90h] BYREF
  void *v23; // [xsp+90h] [xbp-80h]
  int v24; // [xsp+98h] [xbp-78h]
  _BYTE v25[16]; // [xsp+A0h] [xbp-70h] BYREF
  void *v26; // [xsp+B0h] [xbp-60h]
  _BYTE v27[16]; // [xsp+B8h] [xbp-58h] BYREF
  void *v28; // [xsp+C8h] [xbp-48h]
  int v29; // [xsp+D0h] [xbp-40h]
  __int64 v30; // [xsp+D8h] [xbp-38h]

  v30 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  if ( *((_BYTE *)this + 368) )
    return;
  *((_BYTE *)this + 368) = 1;
  std::string::assign((_DWORD)this + 376, &byte_15D0C01, 0LL);
  cocos2d::StringUtils::format(v17, (cocos2d::StringUtils *)"%s/a/package.php", "https://app.leleketang.com/cr/service");
  std::string::basic_string<decltype(nullptr)>((int)v20, "pkgid");
  std::string::basic_string(v22, (char *)this + 24);
  v24 = 1;
  std::string::basic_string<decltype(nullptr)>((int)v25, "version");
  std::string::basic_string(v27, (char *)this + 96);
  v29 = 1;
  v14 = (__int64)&v14;
  v15 = &v14;
  v16 = 0LL;
  v2 = operator new(0x48uLL);
  *(_QWORD *)v2 = 0LL;
  std::string::basic_string(v2 + 16, v20);
  std::string::basic_string(v2 + 40, v22);
  v3 = v14;
  *(_DWORD *)(v2 + 64) = v24;
  *(_QWORD *)v2 = v3;
  *(_QWORD *)(v2 + 8) = &v14;
  *(_QWORD *)(v3 + 8) = v2;
  v14 = v2;
  ++v16;
  v4 = operator new(0x48uLL);
  *(_QWORD *)v4 = 0LL;
  std::string::basic_string(v4 + 16, v25);
  v5 = (LDataRequest *)std::string::basic_string(v4 + 40, v27);
  v6 = v14;
  *(_DWORD *)(v4 + 64) = v29;
  *(_QWORD *)v4 = v6;
  *(_QWORD *)(v4 + 8) = &v14;
  *(_QWORD *)(v6 + 8) = v4;
  v14 = v4;
  ++v16;
  if ( (v27[0] & 1) != 0 )
  {
    operator delete(v28);
    if ( (v25[0] & 1) == 0 )
    {
LABEL_4:
      if ( (v22[0] & 1) == 0 )
        goto LABEL_5;
      goto LABEL_12;
    }
  }
  else if ( (v25[0] & 1) == 0 )
  {
    goto LABEL_4;
  }
  operator delete(v26);
  if ( (v22[0] & 1) == 0 )
  {
LABEL_5:
    if ( (v20[0] & 1) == 0 )
      goto LABEL_7;
    goto LABEL_6;
  }
LABEL_12:
  operator delete(v23);
  if ( (v20[0] & 1) != 0 )
LABEL_6:
    operator delete(ptr);
LABEL_7:
  Instance = LDataRequest::getInstance(v5);
  v18[0] = &off_1935F58;
  v18[1] = this;
  v19 = v18;
  LDataRequest::post(Instance, (unsigned __int8 *)v17, (__int64)&v14, (__int64)v18, (__int64)this);
  if ( v18 == v19 )
  {
    v8 = *(void (**)(void))(*v19 + 32LL);
  }
  else
  {
    if ( !v19 )
      goto LABEL_16;
    v8 = *(void (**)(void))(*v19 + 40LL);
  }
  v8();
LABEL_16:
  if ( v16 )
  {
    v9 = v14;
    v10 = v15;
    v11 = *v15;
    v12 = v15 == &v14;
    *(_QWORD *)(v11 + 8) = *(_QWORD *)(v14 + 8);
    **(_QWORD **)(v9 + 8) = v11;
    v16 = 0LL;
    if ( !v12 )
    {
      do
      {
        v13 = (__int64 *)v10[1];
        if ( (v10[5] & 1) != 0 )
          operator delete((void *)v10[7]);
        if ( (v10[2] & 1) != 0 )
          operator delete((void *)v10[4]);
        operator delete(v10);
        v10 = v13;
      }
      while ( v13 != &v14 );
    }
  }
  if ( (v17[0] & 1) != 0 )
    operator delete((void *)v17[2]);
}

off_1935F58就是json处理函数, 过去看看

真找到了…呜呜呜, 找了3天…

既然定位到了关键函数, 现在要处理签名问题了, 360加固, 我又来了… 直接签名看日志咯

从日志来看,这是一个典型的 Android SO 加固(壳)加载与初始化流程,而且明显是 国内常见的一代/二代加固(如娜迦、爱加密、梆梆等企业版壳)。下面按时间线给你拆解它的基本行为逻辑


一、整体加固流程概览(结论先行)

流程本质

应用启动 → 系统加载壳 SO → 壳在 native 层完成 反调试 + 环境检测 + 内存解密 + 原 DEX/SO 还原 → 转交控制权给真实应用

你这份日志,正好卡在“壳刚启动、正在铺基础设施”的阶段


二、逐阶段日志分析

✅ 1️⃣ 壳 SO 首次加载

1
2
linker_do_dlopen onEnter: libjiagu_64.so
linker_do_dlopen onLeave: libjiagu_64.so

📌 说明:

  • 这是 加固 SO 被 dlopen 进进程

  • libjiagu_64.so64 位 ARM 加固壳

  • 发生在 JVM / ART 之前或早期


✅ 2️⃣ JNI 入口探测

1
2
3
[dlsym] JNI_OnLoad
[dlsym] JNI_OnLoad
[dlsym] JNI_OnLoad

📌 说明:

  • 加固壳一定会实现 JNI_OnLoad

  • 多次 dlsym可能是:

    • 多 SO 校验

    • 防止 JNI_OnLoad 被 hook

    • 自己查找自己的入口(壳套壳)


✅ 3️⃣ 批量解析基础系统库(打地基)

1
2
3
4
5
6
linker_do_dlopen: liblog.so
linker_do_dlopen: libz.so
linker_do_dlopen: libc.so
linker_do_dlopen: libm.so
linker_do_dlopen: libstdc++.so
linker_do_dlopen: libdl.so

📌 目的:

  • 明确 不使用直接链接

  • 为后续 手动调用系统函数​ 做准备

  • 规避 linker namespace 限制


✅ 4️⃣ 大规模 dlsym(壳的典型特征)

这是最关键的识别点👇

🔹 反调试 / 反注入

1
2
3
4
5
6
ptrace
fork
kill
raise
sigaction
prctl

📌 行为含义:

函数用途
ptrace防调试(PTRACE_TRACEME / ATTACH)
fork子进程监控
kill自杀 / 杀调试器
prctl设置 PTRACE 禁止

典型壳行为:一旦检测到 debugger,立即退出


🔹 文件系统 & 内存操作

1
2
3
4
5
open / close / read / write
mmap / munmap
fopen / fclose
lstat / readlink
inotify_init / add_watch / rm_watch

📌 用途:

  • 扫描 /proc/self/maps

  • 检查自身是否被 dump

  • 监控 DEX / SO 文件变化

  • 准备 内存加载原 DEX


🔹 压缩 & 解密相关

1
2
3
inflate / inflateInit2_
deflate / deflateInit_
adler32

📌 说明:

  • 原 APK / DEX 被压缩或加密存储

  • 壳会在内存中解压

  • 不会落盘


🔹 多线程 & 同步

1
2
3
pthread_create
pthread_mutex_*
pthread_cond_*

📌 用途:

  • 后台监控线程

  • 定时反调试

  • 心跳检测


🔹 系统属性读取

1
__system_property_get

📌 用途:

  • 获取:

    • ro.build.version

    • ro.debuggable

    • ro.secure

  • 判断是否在模拟器 / Root / Debug 环境


✅ 5️⃣ 再次加载壳 SO(壳套壳)

1
2
linker_do_dlopen onEnter: libjiagu_64.so
linker_do_dlopen onLeave: libjiagu_64.so

📌 说明:

  • 二次加载

  • 常见原因:

    • 第一段壳负责 loader

    • 第二段壳才是真正的解密执行体

  • 也叫 loader + payload 结构


✅ 6️⃣ 接触 ART 运行时

1
linker_do_dlopen onEnter: libart.so

📌 非常关键:

  • 壳开始 hook ART

  • 常见手段:

    • Hook OpenDexFileNative

    • Hook LoadMethod

    • 内存中替换 DEX

  • 这是 DEX 脱壳点


✅ 7️⃣ 最终状态

  • 日志结束于 仍在初始化阶段

  • 尚未看到:

    • dex2oat

    • ClassLinker

    • 真实 App 的 Java 方法执行

👉 说明你抓的是“壳刚醒”的时刻


三、加固类型判定(经验判断)

特征结果
libjiagu_xx.so✅ 国产商业壳
大量 dlsym✅ 强混淆 + 反分析
ptrace + fork✅ 强反调试
inflate + adler32✅ 内存解包
多次 dlopen✅ 壳套壳
触达 libart.so✅ DEX 内存加载

📌 综合判断

👉 二代加固(内存加载 DEX,无落地)

👉 大概率 企业版加固

以下是日志中出现的所有 dlsym解析过的函数(已去重 & 分类整理),共 约 150+ 个函数,覆盖 系统调用 / libc / pthread / IO / 压缩 / 网络 / 反调试​ 等。


一、🔹 基础 C 库(libc)

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
abort
access
atol
atoll
atoi
calloc
clock
clock_gettime
close
creat
dup2
execl
execve
exit
fabs
fclose
fcntl
feof
ferror
fflush
fgetc
fgets
fileno
flock
fopen
fprintf
fputc
fread
free
freopen
fsync
ftruncate
fwrite
getcwd
getenv
getpagesize
getpid
getppid
gettid
gettimeofday
isdigit
isspace
isupper
isxdigit
kill
localtime_r
lseek
lseek64
lstat
malloc
memchr
memcmp
memcpy
memmove
memset
mkdir
mktime
mmap
munmap
nanosleep
open
opendir
pause
perror
pipe
poll
pread
pwrite
raise
rand
read
readdir
readlink
realloc
remove
rename
rewind
rmdir
sbrk
scanf
select
setenv
setpgid
setsockopt
sigaction
sigfillset
signal
sleep
snprintf
sprintf
sscanf
stat
strcasecmp
strcat
strchr
strcmp
strcpy
strdup
strerror
strftime
strlen
strncmp
strncpy
strptime
strrchr
strstr
strtod
strtok
strtol
strtoll
strtoul
strtoull
syscall
system
time
times
toascii
tolower
toupper
umask
unlink
vfprintf
vprintf
vsnprintf
vsyslog
waitpid
wcstod
wcstol
wcstoll
wcstold
wcstoul
wcstoull
wctomb
wmemcpy
wmemset
write

二、🔹 数学库(libm)

1
2
3
4
5
6
7
8
9
10
ceil
cos
exp
fabs
floor
fmod
fmodf
pow
sin
sqrt

三、🔹 线程 & 同步(pthread)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pthread_attr_destroy
pthread_attr_init
pthread_attr_setdetachstate
pthread_cond_broadcast
pthread_cond_destroy
pthread_cond_init
pthread_cond_signal
pthread_cond_timedwait
pthread_cond_wait
pthread_create
pthread_detach
pthread_equal
pthread_exit
pthread_getspecific
pthread_join
pthread_key_create
pthread_key_delete
pthread_mutex_destroy
pthread_mutex_init
pthread_mutex_lock
pthread_mutex_unlock
pthread_once
pthread_self
pthread_setspecific

四、🔹 动态链接(dl)

1
2
3
4
dlclose
dlerror
dlopen
dlsym

五、🔹 压缩 / 解压(zlib)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
adler32
compress
compress2
crc32
deflate
deflateBound
deflateEnd
deflateInit
deflateInit2
inflate
inflateEnd
inflateInit
inflateInit2
uncompress
zlibVersion

六、🔹 文件 & 目录监控(inotify)

1
2
3
inotify_add_watch
inotify_init
inotify_rm_watch

七、🔹 网络 & Socket

1
2
3
4
5
6
7
8
9
10
11
12
13
accept
bind
connect
gethostbyname
getsockname
getsockopt
inet_addr
listen
recv
send
setsockopt
shutdown
socket

八、🔹 反调试 / 进程控制(加固核心)

1
2
3
4
5
6
7
8
fork
ptrace
prctl
raise
kill
sigaction
sigfillset
syscall

九、🔹 Android 特有

1
2
3
4
5
6
7
8
9
10
__android_log_print
__android_log_vprint
__assert2
__cxa_atexit
__cxa_finalize
__errno
__sF
__stack_chk_fail
__stack_chk_guard
__system_property_get

十、🔹 JNI / ART 相关

1
JNI_OnLoad

✅ 统计汇总

类别数量
libc 基础函数~90
pthread~20
zlib~12
网络~10
反调试~8
Android 特有~10
动态链接~4
数学库~8
总计~160+

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
onEarlyModuleLoaded('libjiagu_64', (module: Module) => {
    // 拦截 strncmp(带长度限制的字符串比较)
    Interceptor.attach(Module.getGlobalExportByName('strncmp'), {
        onEnter: function (args) {
            var s1 = args[0].readCString() || "(null)";
            var s2 = args[1].readCString() || "(null)";
            var n = args[2].toInt32();
            log('[strncmp] s1: "' + s1 + '", s2: "' + s2 + '", n: ' + n);
        }
    });

// 拦截 strcasecmp(不区分大小写的字符串比较)
    Interceptor.attach(Module.getGlobalExportByName('strcasecmp'), {
        onEnter: function (args) {
            var s1 = args[0].readCString() || "(null)";
            var s2 = args[1].readCString() || "(null)";
            log('[strcasecmp] s1: "' + s1 + '", s2: "' + s2 + '"');
        }
    });

// 拦截 memcmp(内存区域比较,常用于字符串)
    Interceptor.attach(Module.getGlobalExportByName('memcmp'), {
        onEnter: function (args) {
            var s1 = args[0].readCString() || "(null)";
            var s2 = args[1].readCString() || "(null)";
            var n = args[2].toInt32();
            log('[memcmp] ptr1: "' + s1 + '", ptr2: "' + s2 + '", n: ' + n);
        }
    });

// 拦截 strstr(子串查找,涉及字符串对比)
    Interceptor.attach(Module.getGlobalExportByName('strstr'), {
        onEnter: function (args) {
            var haystack = args[0].readCString() || "(null)";
            var needle = args[1].readCString() || "(null)";
            log('[strstr] haystack: "' + haystack + '", needle: "' + needle + '"');
        }
    });

// 可选:拦截 strcmp(日志中未出现,但常见于字符串对比)
// Interceptor.attach(Module.getGlobalExportByName('strcmp'), {
//     onEnter: function (args) {
//         try {
//             var s1 = args[0].readCString() || "(null)";
//             var s2 = args[1].readCString() || "(null)";
//             log('[strcmp] s1: "' + s1 + '", s2: "' + s2 + '"');
//         } catch (e) {
//             log('[strcmp] 读取失败: ' + e.message);
//         }
//     }
// });
})

根据日志分析,该加固(疑似360加固保或类似方案)的验签流程如下:


一、整体流程概览

加固在应用启动时通过 Native层(SO)​ 完成签名校验,核心逻辑包括:

  1. 环境初始化:加载加固SO,获取系统函数。

  2. 包信息获取:通过Java反射调用获取应用包名、签名等信息。

  3. 签名哈希比对:计算当前APK签名哈希,与预置的白名单哈希逐条比较。

  4. 辅助校验:检查文件完整性、系统属性、运行环境等。

  5. 反调试/防篡改:检测调试器、修改痕迹。


二、详细步骤分解

1. 加固SO加载与初始化

  • 动态加载加固核心库:
    1
    2
    
      [linker_do_dlopen] onEnter: libjiagu_64.so
      [linker_do_dlopen] onLeave: libjiagu_64.so
    
  • 获取系统函数地址(用于后续调用):
    1
    
      [dlsym] __system_property_get
    

2. 获取应用上下文与包管理

通过 memcmp检测反射调用的字符串,逐步获取应用信息:

1
2
3
4
5
[memcmp] ptr1: "Lcom/stub/StubApp;"          // 加固入口类
[memcmp] ptr1: "getAppContext"               // 获取Context
[memcmp] ptr1: "getPackageManager"           // 获取PackageManager
[memcmp] ptr1: "getPackageInfo"              // 获取PackageInfo
[memcmp] ptr1: "Landroid/content/pm/Signature;" // 签名对象

3. 提取并计算签名哈希

  • PackageInfo中提取 Signature数组。

  • 对签名数据计算 MD5 哈希(32位十六进制字符串)。

  • 日志中频繁出现32字符哈希比较,例如:

    1
    2
    
      [memcmp] ptr1: "515fe56e838c9e98c7dd1ffad524a783", ptr2: "515fe56e838c9e98c7dd1ffad524a783", n: 32
      [memcmp] ptr1: "924c16677d3128cb33d1ecbe0cf77de0", ptr2: "924c16677d3128cb33d1ecbe0cf77de0", n: 32
    

    这些是预置的合法签名哈希白名单

4. 哈希白名单匹配

加固会循环比较当前签名哈希与内置白名单:

  • 日志中出现 数十个​ 不同的32位哈希字符串(如 34089686bab4f6a80d558fb01af3b05a等)。

  • 只要有一个匹配,即认为签名合法。

  • 若全部不匹配,则判定为篡改或重打包。

5. 辅助校验项目

加固同时进行多项辅助检查:

校验项日志证据
包名校验[memcmp] ptr1: "com.leleketang.SchoolFantasy"
APK完整性[memcmp] ptr1: "apk-md5"
文件头校验[memcmp] ptr1: "ELF"(检测SO是否被修改)
系统属性[strncmp] s1: "persist.sys.assert.panic"(检测调试状态)
路径校验[strncmp] s1: "assets/", s2: "lib/"(防止资源替换)
时间校验[memcmp] ptr1: "protect-time"(防回滚攻击)

6. 环境检测与反调试

  • 检测ART运行时、Binder、内存映射等:

    c

    c

    1
    2
    
      [strncmp] s1: "/apex/com.android.art/lib64/libart.so", needle: "fake-libs"
      [memcmp] ptr1: "dalvik-local ref table"  // 检测虚拟机环境
    
  • 检查调试端口、进程状态等。

7. 最终结果处理

  • 若所有校验通过 → 正常启动应用。

  • 若校验失败 → 可能直接 exit()、抛异常或进入“假死”状态。


三、关键特征总结

  1. 多哈希白名单:内置多个合法签名哈希,支持多渠道/多版本。

  2. 反射调用隐藏:通过字符串比较间接调用Java方法,增加逆向难度。

  3. Native层校验:核心逻辑在SO中实现,难以绕过。

  4. 综合防护:结合签名、文件、环境、时间等多维度校验。


四、典型绕过思路(仅用于安全研究)

  1. Hook memcmp/strncmp:强制返回匹配结果。

  2. Patch SO:修改哈希比较逻辑(如跳过校验分支)。

  3. 动态调试:在哈希比较处下断点,修改返回值。

  4. 重打包并重算哈希:将新签名哈希加入白名单(需破解加密)。

The callbacks argument is an object containing one or more of:

  • onEnter(args): callback function given one argument args that can be used to read or write arguments as an array of NativePointer objects. {: #interceptor-onenter}

  • onLeave(retval): callback function given one argument retval that is a NativePointer-derived object containing the raw return value. You may call retval.replace(1337) to replace the return value with the integer 1337, or retval.replace(ptr("0x1234")) to replace with a pointer. Note that this object is recycled across onLeave calls, so do not store and use it outside your callback. Make a deep copy if you need to store the contained value, e.g.: ptr(retval.toString()).

    In case the hooked function is very hot, onEnter and onLeave may be NativePointer values pointing at native C functions compiled using CModule. Their signatures are:

    • void onEnter (GumInvocationContext * ic)
  • void onLeave (GumInvocationContext * ic)

    In such cases, the third optional argument data may be a NativePointer accessible through gum_invocation_context_get_listener_function_data().

    You may also intercept arbitrary instructions by passing a function instead of the callbacks object. This function has the same signature as onEnter, but the args argument passed to it will only give you sensible values if the intercepted instruction is at the beginning of a function or at a point where registers/stack have not yet deviated from that point.

    Just like above, this function may also be implemented in C by specifying a NativePointer instead of a function.

    Returns a listener object that you can call detach() on.

    Note that these functions will be invoked with this bound to a per-invocation (thread-local) object where you can store arbitrary data, which is useful if you want to read an argument in onEnter and act on it in onLeave.

    For example:

1
2
3
4
5
6
7
8
9
10
11
const libc = Process.getModuleByName('libc.so');
Interceptor.attach(libc.getExportByName('read'), {
  onEnter(args) {
    this.fileDescriptor = args[0].toInt32();
  },
  onLeave(retval) {
    if (retval.toInt32() > 0) {
      /* do something with this.fileDescriptor */
    }
  }
});
  • Additionally, the object contains some useful properties:

    • returnAddress: return address as a NativePointer

    • context: object with the keys pc and sp, which are NativePointer objects specifying EIP/RIP/PC and ESP/RSP/SP, respectively, for ia32/x64/arm. Other processor-specific keys are also available, e.g. eax, rax, r0, x0, etc. You may also update register values by assigning to these keys.

    • errno: (UNIX) current errno value (you may replace it)

    • lastError: (Windows) current OS error value (you may replace it)

    • threadId: OS thread ID

    • depth: call depth of relative to other invocations

    For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Interceptor.attach(Module.getGlobalExportByName('read'), {
  onEnter(args) {
    console.log('Context information:');
    console.log('Context  : ' + JSON.stringify(this.context));
    console.log('Return   : ' + this.returnAddress);
    console.log('ThreadId : ' + this.threadId);
    console.log('Depth    : ' + this.depth);
    console.log('Errornr  : ' + this.err);

    // Save arguments for processing in onLeave.
    this.fd = args[0].toInt32();
    this.buf = args[1];
    this.count = args[2].toInt32();
  },
  onLeave(result) {
    console.log('----------')
    // Show argument 1 (buf), saved during onEnter.
    const numBytes = result.toInt32();
    if (numBytes > 0) {
      console.log(hexdump(this.buf, { length: numBytes, ansi: true }));
    }
    console.log('Result   : ' + numBytes);
  }
})

Performance considerations

The callbacks provided have a significant impact on performance. If you only need to inspect arguments but do not care about the return value, or the other way around, make sure you omit the callback that you don’t need; i.e. avoid putting your logic in onEnter and leaving onLeave in there as an empty callback.

On an iPhone 5S the base overhead when providing just onEnter might be something like 6 microseconds, and 11 microseconds with both onEnter and onLeave provided.

Also be careful about intercepting calls to functions that are called a bazillion times per second; while send() is asynchronous, the total overhead of sending a single message is not optimized for high frequencies, so that means Frida leaves it up to you to batch multiple values into a single send()-call, based on whether low delay or high throughput is desired.

However when hooking hot functions you may use Interceptor in conjunction with CModule to implement the callbacks in C.

Interceptor.detachAll()

detach all previously attached callbacks.

Interceptor.replace(target, replacement[, data])

replace function at target with implementation at replacement. This is typically used if you want to fully or partially replace an existing function’s implementation.

1
2
3
4
5
6
7
8
9
Use [`NativeCallback`](https://frida.re/docs/javascript-api/#nativecallback) to implement a `replacement` in JavaScript.

In case the replaced function is very hot, you may implement `replacement` in C using **[CModule](https://frida.re/docs/javascript-api/#cmodule)**. You may then also specify the third optional argument `data`, which is a [`NativePointer`](https://frida.re/docs/javascript-api/#nativepointer) accessible through `gum_invocation_context_get_listener_function_data()`. Use `gum_interceptor_get_current_invocation()` to get hold of the `GumInvocationContext *`.

Note that `replacement` will be kept alive until [`Interceptor#revert`](https://frida.re/docs/javascript-api/#interceptor-revert) is called.

If you want to chain to the original implementation you can synchronously call `target` through a [`NativeFunction`](https://frida.re/docs/javascript-api/#nativefunction) inside your implementation, which will bypass and go directly to the original implementation.

Here’s an example:
1
2
3
4
5
6
7
8
9
10
const libc = Process.getModuleByName('libc.so');
const openPtr = libc.getExportByName('open');
const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
Interceptor.replace(openPtr, new NativeCallback((pathPtr, flags) => {
  const path = pathPtr.readUtf8String();
  log('Opening "' + path + '"');
  const fd = open(pathPtr, flags);
  log('Got fd: ' + fd);
  return fd;
}, 'int', ['pointer', 'int']));

Interceptor.replaceFast(target, replacement)

like replace() except target is modified to vector directly to your replacement, which means there is less overhead compared to replace(). This also means that you need to use the returned pointer if you want to call the original implementation.

Interceptor.revert(target)

revert function at target to the previous implementation.

Interceptor.flush()

ensure any pending changes have been committed to memory. This is should only be done in the few cases where this is necessary, e.g. if you just attach()ed to or replace()d a function that you are about to call using NativeFunction. Pending changes are flushed automatically whenever the current thread is about to leave the JavaScript runtime or calls send(). This includes any API built on top of send(), like when returning from an RPC method, and calling any method on the console API.

Interceptor.breakpointKind

a string specifying the kind of breakpoints to use for non-inline hooks. Only available in the Barebone backend.

1
Defaults to ‘soft’, i.e. software breakpoints. Set it to ‘hard’ to use hardware breakpoints.

Stalker

Stalker.exclude(range)

marks the specified memory range as excluded, which is an object with base and size properties – like the properties in an object returned by e.g. Process.getModuleByName().

1
2
3
This means Stalker will not follow execution when encountering a call to an instruction in such a range. You will thus be able to observe/modify the arguments going in, and the return value coming back, but won’t see the instructions that happened between.

Useful to improve performance and reduce noise.

Stalker.follow([threadId, options])

start stalking threadId (or the current thread if omitted), optionally with options for enabling events.

1
For example:
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
const mainThread = Process.enumerateThreads()[0];

Stalker.follow(mainThread.id, {
  events: {
    call: true, // CALL instructions: yes please

    // Other events:
    ret: false, // RET instructions
    exec: false, // all instructions: not recommended as it's
                 //                   a lot of data
    block: false, // block executed: coarse execution trace
    compile: false // block compiled: useful for coverage
  },

  //
  // Only specify one of the two following callbacks.
  // (See note below.)
  //

  //
  // onReceive: Called with `events` containing a binary blob
  //            comprised of one or more GumEvent structs.
  //            See `gumevent.h` for details about the
  //            format. Use `Stalker.parse()` to examine the
  //            data.
  //
  //onReceive(events) {
  //},
  //

  //
  // onCallSummary: Called with `summary` being a key-value
  //                mapping of call target to number of
  //                calls, in the current time window. You
  //                would typically implement this instead of
  //                `onReceive()` for efficiency, i.e. when
  //                you only want to know which targets were
  //                called and how many times, but don't care
  //                about the order that the calls happened
  //                in.
  //
  onCallSummary(summary) {
  },

  //
  // Advanced users: This is how you can plug in your own
  //                 StalkerTransformer, where the provided
  //                 function is called synchronously
  //                 whenever Stalker wants to recompile
  //                 a basic block of the code that's about
  //                 to be executed by the stalked thread.
  //
  //transform(iterator) {
  //  let instruction = iterator.next();
  //
  //  const startAddress = instruction.address;
  //  const isAppCode = startAddress.compare(appStart) >= 0 &&
  //      startAddress.compare(appEnd) === -1;
  //
  //  /*
  //   * Need to be careful on ARM/ARM64 as we may disturb instruction sequences
  //   * that deal with exclusive stores.
  //   */
  //  const canEmitNoisyCode = iterator.memoryAccess === 'open';
  //
  //  do {
  //    if (isAppCode && canEmitNoisyCode && instruction.mnemonic === 'ret') {
  //      iterator.putCmpRegI32('eax', 60);
  //      iterator.putJccShortLabel('jb', 'nope', 'no-hint');
  //
  //      iterator.putCmpRegI32('eax', 90);
  //      iterator.putJccShortLabel('ja', 'nope', 'no-hint');
  //
  //      iterator.putCallout(onMatch);
  //
  //      iterator.putLabel('nope');
  //
  //      /* You may also use putChainingReturn() to insert an early return. */
  //    }
  //
  //    iterator.keep();
  //  } while ((instruction = iterator.next()) !== null);
  //},
  //
  // The default implementation is just:
  //
  //   while (iterator.next() !== null)
  //     iterator.keep();
  //
  // The example above shows how you can insert your own code
  // just before every `ret` instruction across any code
  // executed by the stalked thread inside the app's own
  // memory range. It inserts code that checks if the `eax`
  // register contains a value between 60 and 90, and inserts
  // a synchronous callout back into JavaScript whenever that
  // is the case. The callback receives a single argument
  // that gives it access to the CPU registers, and it is
  // also able to modify them.
  //
  // function onMatch (context) {
  //   console.log('Match! pc=' + context.pc +
  //       ' rax=' + context.rax.toInt32());
  // }
  //
  // Note that not calling keep() will result in the
  // instruction getting dropped, which makes it possible
  // for your transform to fully replace certain instructions
  // when this is desirable.
  //

  //
  // Want better performance? Write the callbacks in C:
  //
  // /*
  //  * const cm = new CModule(\`
  //  *
  //  * #include <gum/gumstalker.h>
  //  *
  //  * static void on_ret (GumCpuContext * cpu_context,
  //  *     gpointer user_data);
  //  *
  //  * void
  //  * transform (GumStalkerIterator * iterator,
  //  *            GumStalkerOutput * output,
  //  *            gpointer user_data)
  //  * {
  //  *   cs_insn * insn;
  //  *
  //  *   while (gum_stalker_iterator_next (iterator, &insn))
  //  *   {
  //  *     if (insn->id == X86_INS_RET)
  //  *     {
  //  *       gum_x86_writer_put_nop (output->writer.x86);
  //  *       gum_stalker_iterator_put_callout (iterator,
  //  *           on_ret, NULL, NULL);
  //  *     }
  //  *
  //  *     gum_stalker_iterator_keep (iterator);
  //  *   }
  //  * }
  //  *
  //  * static void
  //  * on_ret (GumCpuContext * cpu_context,
  //  *         gpointer user_data)
  //  * {
  //  *   printf ("on_ret!\n");
  //  * }
  //  *
  //  * void
  //  * process (const GumEvent * event,
  //  *          GumCpuContext * cpu_context,
  //  *          gpointer user_data)
  //  * {
  //  *   switch (event->type)
  //  *   {
  //  *     case GUM_CALL:
  //  *       break;
  //  *     case GUM_RET:
  //  *       break;
  //  *     case GUM_EXEC:
  //  *       break;
  //  *     case GUM_BLOCK:
  //  *       break;
  //  *     case GUM_COMPILE:
  //  *       break;
  //  *     default:
  //  *       break;
  //  *   }
  //  * }
  //  * `);
  //  */
  //
  //transform: cm.transform,
  //onEvent: cm.process,
  //data: ptr(1337) /* user_data */
  //
  // You may also use a hybrid approach and only write
  // some of the callouts in C.
  //
});

Performance considerations

The callbacks provided have a significant impact on performance. If you only need periodic call summaries but do not care about the raw events, or the other way around, make sure you omit the callback that you don’t need; i.e. avoid putting your logic in onCallSummary and leaving onReceive in there as an empty callback.

Also note that Stalker may be used in conjunction with CModule, which means the callbacks may be implemented in C.

Stalker.unfollow([threadId])

stop stalking threadId (or the current thread if omitted).

Stalker.parse(events[, options])

parse GumEvent binary blob, optionally with options for customizing the output.

1
For example:
1
2
3
4
5
6
7
8
9
  onReceive(events) {
    console.log(Stalker.parse(events, {
      annotate: true, // to display the type of event
      stringify: true
        // to format pointer values as strings instead of `NativePointer`
        // values, i.e. less overhead if you're just going to `send()` the
        // thing not actually parse the data agent-side
    }));
  },

Stalker.flush()

flush out any buffered events. Useful when you don’t want to wait until the next Stalker.queueDrainInterval tick.

Stalker.garbageCollect()

free accumulated memory at a safe point after Stalker#unfollow. This is needed to avoid race-conditions where the thread just unfollowed is executing its last instructions.

Stalker.invalidate(address)

invalidates the current thread’s translated code for a given basic block. Useful when providing a transform callback and wanting to dynamically adapt the instrumentation for a given basic block. This is much more efficient than unfollowing and re-following the thread, which would discard all cached translations and require all encountered basic blocks to be compiled from scratch.

Stalker.invalidate(threadId, address)

invalidates a specific thread’s translated code for a given basic block. Useful when providing a transform callback and wanting to dynamically adapt the instrumentation for a given basic block. This is much more efficient than unfollowing and re-following the thread, which would discard all cached translations and require all encountered basic blocks to be compiled from scratch.

Stalker.addCallProbe(address, callback[, data])

call callback (see Interceptor#attach#onEnter for signature) synchronously when a call is made to address. Returns an id that can be passed to Stalker#removeCallProbe later.

1
2
3
4
5
It is also possible to implement `callback` in C using **[CModule](https://frida.re/docs/javascript-api/#cmodule)**, by specifying a [`NativePointer`](https://frida.re/docs/javascript-api/#nativepointer) instead of a function. Signature:

- `void onCall (GumCallSite * site, gpointer user_data)`

In such cases, the third optional argument `data` may be a [`NativePointer`](https://frida.re/docs/javascript-api/#nativepointer) whose value is passed to the callback as `user_data`.
  • Stalker.removeCallProbe: remove a call probe added by Stalker#addCallProbe.

  • Stalker.trustThreshold: an integer specifying how many times a piece of code needs to be executed before it is assumed it can be trusted to not mutate. Specify -1 for no trust (slow), 0 to trust code from the get-go, and N to trust code after it has been executed N times. Defaults to 1.

  • Stalker.queueCapacity: an integer specifying the capacity of the event queue in number of events. Defaults to 16384 events.

  • Stalker.queueDrainInterval: an integer specifying the time in milliseconds between each time the event queue is drained. Defaults to 250 ms, which means that the event queue is drained four times per second. You may also set this property to zero to disable periodic draining, and instead call Stalker.flush() when you would like the queue to be drained.

本文由作者按照 CC BY 4.0 进行授权