【Android安全-基于VM的全新Trace框架发布!功能强大,一分钟1.5g,提高你的逆向体验~】此文章归类为:Android安全。
因为现有的作品均有大大小小的问题,想自己开发一个自己用着比较舒服的框架,提供分析效率。
因为想收集大家出现的反馈,继续发展壮大,我会持续维护一阵子,来提升自己的开发实力。
答案是不开源,会给出so文件,以及详细的使用方法,以及入门的例子,但是不开源不代表不维护
文章结尾会放上微信技术交流群,来讨论trace相关问题,以及逆向相关问题
这一篇算是对我git上的注入模块的部分的一个填坑,也是某搜题软件的第二篇的填坑,我最终目的是开发出无痕注入 、hook 、trace 、debug。
欢迎大家贴数据对比速度,当然后期有更多的优化空间,目前先不考虑改善(够用了)
已经手动trace过某团、某音、某Q,均无崩溃完成trace完成
但是在我测试中也有无法trace无法使用的情况,比如某宝,但是根据trace
进行了针对性patch后也可以继续运行完成
所以我希望大家发掘更多样本进行开发修复
文本例子(可以结尾下载trace文件,自己搜索查看)
1 2 3 4 | Call addr: 0x7f3d15b9a0 [libc.so! memmove ] [libc.so! memmove ](0xb400007d5ace2da0, 0xb400007db18f7c14, 32) Call addr: 0x7f3d1582c0 [libc.so! memcmp ] memcmp (0xb400007d5a9dc760, 0xb400007d5ace2da0, 32) |
并且支持通过得到的Jmethod id 反查是哪个类型
*为正在读取的内存内容
以及一些方便算法还原的打印
1 | memory read at 0x7d1e843ba8, instruction address = 0x7df45647cc, data size = 8, data value = 2a00000000000000 |
这个目前不全,后面会加上所有调用参数等的打印
有人会问有啥用
append一些函数就能逮到了
后面会和上面的功能一起维护好,打印所有入参,相当于给所有函数都inlinehook了一遍
此次的案例以我上一篇trace的案例作为教学,所有相关文件都会放在git仓库里
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 | function getHandle(object) { var handle = null; try { handle = object.$handle; } catch (e) { } if (handle == null) { try { handle = object.$h; } catch (e) { } } if (handle == null) { try { handle = object.handle; } catch (e) { } } return handle; } Java.perform(function () { let ReadableNativeMap = Java.use( "com.fenbi.android.leo.utils.e" ); console. log (getHandle(ReadableNativeMap[ "zcvsd1wr2t" ])) }); |
运行后得到0x9deeed68
继续运行
ptr(0x9deeed68).add(16).readPointer();
得到
1 2 3 4 5 6 7 8 9 10 | DebugSymbol.fromAddress(ptr(0x7de5c78bf4)) { "address" : "0x7de5c78bf4" , "column" : 0, "fileName" : "" , "lineNumber" : 0, "moduleName" : "libRequestEncoder.so" , "name" : "0x61bf4" } |
由于读写内存的权限问题,需要刷入指定面具模块
可以在github仓库中找到
推动git上的so到/data/local/tmp/test.so
关闭setlinux
1 2 3 | adb shell su setenforse 0 |
刷入面具模块(在仓库里)
在过掉msao后(见上一篇帖子开头)
attach输入
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 | function prepareArgs(args) { if (args === undefined || !Array.isArray(args)) { args = []; } var argNum = args.length; var argSize = Process.pointerSize * argNum; var argsPtr = Memory.alloc(argSize); for (var i = 0; i < argNum; i++) { var arg = args[i]; var argPtr; if (!arg){ arg=0 } if (arg instanceof NativePointer) { // 如果是 NativePointer,直接使用 argPtr = arg; } else if (typeof arg === 'number' ) { // 如果是数字,直接转换为指针 argPtr = ptr(arg); } else if (typeof arg === 'string' ) { // 如果是字符串,分配内存并获取指针 argPtr = Memory.allocUtf8String(arg); } else if (typeof arg === 'object' && arg.hasOwnProperty( 'handle' )) { // 如果是带有 handle 属性的对象(如 JNIEnv) argPtr = arg.handle; } else if (typeof arg === 'object' && arg instanceof ArrayBuffer) { // 如果是二进制数据,分配内存并写入数据 var dataPtr = Memory.alloc(arg.byteLength); Memory.writeByteArray(dataPtr, arg); argPtr = dataPtr; } else { console.error( 'Unsupported argument type at index ' + i + ':' , typeof arg); throw new TypeError( 'Unsupported argument type at index ' + i + ': ' + typeof arg); } // 将参数指针写入参数数组 Memory.writePointer(argsPtr.add(i * Process.pointerSize), argPtr); } return { argsPtr: argsPtr, argNum: argNum }; } var dlopenPtr = Module.findExportByName(null, 'dlopen' ); var dlopen = new NativeFunction(dlopenPtr, 'pointer' , [ 'pointer' , 'int' ]); var soPath = "/data/local/tmp/test.so" ; // 示例路径 var soPathPtr = Memory.allocUtf8String(soPath); var handle = dlopen(soPathPtr, 2); var traceaddr = Module.findExportByName( "test.so" , 'start_trace' ); var trace = new NativeFunction(traceaddr, 'pointer' , [ 'pointer' , 'pointer' , 'uint32' , 'pointer' , 'uint32' ]); var aimbase =Module.findBaseAddress( "libRequestEncoder.so" ); var targetFuncAddr = aimbase.add(0x61bf4); console. log (handle); Interceptor.replace(targetFuncAddr, new NativeCallback(function (arg0,arg1,arg2,arg3,arg4,arg5) { console. log ( "memory_function called with pointer: " + ptr); var args =[arg0,arg1,arg2,arg3,arg4,arg5]; var {argsPtr, argNum} = prepareArgs(args); var argPtr1 = Memory.allocUtf8String( "/data/user/0/com.fenbi.android.leo/log.txt" ); var res =trace(targetFuncAddr, argsPtr,argNum,argPtr1,6); return ptr(res); }, 'pointer' , [ 'pointer' , 'pointer' , 'pointer' , 'pointer' , 'uint32' ])); function call(){ Java.perform(function() { console. log ( "gan_sign script loaded successfully" ); // 使用要 hook 的 Java 类 var e = Java.use( "com.fenbi.android.leo.utils.e" ); // 定义输入参数 var str = "/leo-gateway/android/auth/password" ; // 要访问的链接 var str2 = "wdi4n2t8edr" ; // 固定参数 var intParam = -28673; // 获取 int 参数 // 调用目标方法并获取返回值 var result = e.zcvsd1wr2t(str, str2, intParam); // 输出输入参数 console. log ( "input: " , str, str2, intParam); // 发送返回值到 Frida 客户端 send(result); // 输出返回结果 console. log ( "output: " , result); }); } |
之后主动调用拿到结果:
1 2 3 4 5 6 7 8 | call() gan_sign script loaded successfully memory_function called with pointer: function value() { [native code] } input: /leo-gateway/android/auth/password wdi4n2t8edr -28673 output: d9b6b12a7352587971a9b3b007be672d message: { 'type' : 'send' , 'payload' : 'd9b6b12a7352587971a9b3b007be672d' } data: None |
去手机里拉到/data/user/0/com.fenbi.android.leo/log.txt
trace文件
防止读取到无效内存的地址,缓存maps相关的内存区段,防止进行非法读写
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 | void AddrResolver::cacheModules() { modules.clear(); for ( const auto &map: QBDI::getCurrentProcessMaps( true )) { // 过滤匿名内存映射 (例如 "[anon:...]") if (map.name.empty() || map.name.find( "[anon]" ) != std::string::npos) { continue ; } // 过滤无效路径的映射 (不含 "/" 的无效路径) if (map.name.find( "/" ) == std::string::npos) { continue ; } // 过滤掉不可读和不可执行的映射 (只保留可读或可执行的映射) if ((map.permission & QBDI::PF_READ) == 0) { continue ; } // 检查模块是否已存在,存在则追加,否则新增 auto r = std::find_if(std::begin(modules), std::end(modules), [&]( const Module &m) { return m.path == map.name; }); if (r != std::end(modules)) { r->append(map); } else { modules.emplace_back(map); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | void AddrResolver::loadModules( const std::vector< const Module *> &modules) { for ( const auto &m: modules) { if (loaded_path.find(m->path) != loaded_path.end()) { continue ; } std::unique_ptr externlib = ELFTOOLS::ELF::Parser::parse(m->path); if (not externlib) { continue ; } for ( const auto &s: externlib->symbols()) { QBDI::rword addr = s.value() + m->range.start(); resolv_cache[addr].emplace(s.demangled_name()); } loaded_path.emplace(m->path); } } |
在跳转指令执行的时候(拦截B,BL的指令)
首先先dladdr找到调用的模块的落地文件,使用elf解析工具,把所有符号+偏移地址都缓存到自己的内存里,
如果匹配就可以替换
JNI打印就是在此基础上进行优化的打印
方案1 速度很慢
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 | // 使用 process_vm_readv 读取自身进程内存并搜索可见字符串 inline void searchVisibleStringsInMemoryProcessVM(rword addr, size_t size, LogManager *logManager) { pid_t pid = getpid(); // 获取当前进程ID // 本地缓冲区 std::vector<uint8_t> buffer(size); // 设置本地 iovec struct iovec local_iov; local_iov.iov_base = buffer.data(); local_iov.iov_len = size; // 设置远程 iovec struct iovec remote_iov; remote_iov.iov_base = reinterpret_cast < void *>(addr); remote_iov.iov_len = size; // 调用 process_vm_readv ssize_t nread = process_vm_readv(pid, &local_iov, 1, &remote_iov, 1, 0); if (nread < 0) { std::stringstream ss; ss << "process_vm_readv 失败: " << strerror ( errno ) << "\n" ; logManager->logPrint(ss.str().c_str()); return ; } logManager->logPrint( "内存读取成功。\n" ); // 搜索可见字符串 std::string visibleString; for ( size_t i = 0; i < static_cast < size_t >(nread); ++i) { if (std::isprint(buffer[i])) { visibleString += static_cast < char >(buffer[i]); } else { if (visibleString.length() >= minStringLength) { rword stringAddr = addr + i - visibleString.length(); std::stringstream ss; ss << "地址: 0x" << std::hex << stringAddr << ", 字符串: " << visibleString << "\n" ; logManager->logPrint(ss.str().c_str()); } visibleString.clear(); } } // 检查数据结尾是否有未处理的字符串 if (visibleString.length() >= minStringLength) { rword stringAddr = addr + nread - visibleString.length(); std::stringstream ss; ss << "地址: 0x" << std::hex << stringAddr << ", 字符串: " << visibleString << "\n" ; logManager->logPrint(ss.str().c_str()); } } |
方案2 自己实现了一些搜索的策略 贪婪搜索策略 不再是窗口策略
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 | void scanMemoryWithNullCharHandling(uint8_t *data, rword addr, size_t size, LogManager *logManager, size_t windowSize, size_t minStringLength, size_t maxNullSkip) { int myi = 0; std::string visibleString; size_t nullCharCount = 0; // 记录跳过的 \0 字符数 // 向后扫描,限制范围为 windowSize while (myi < size && myi < windowSize) { if (isMemoryAccessible(addr + myi, 1)) { if (data[myi] == '\0' ) { nullCharCount++; if (nullCharCount > maxNullSkip) { break ; // 如果跳过的 \0 数量超过限制,停止扫描 } // 如果是 '\0' 且还未超过限制,则跳过并继续扫描 myi += 1; continue ; } nullCharCount = 0; // 重置 \0 字符计数 if (std::isprint(data[myi])) { visibleString += static_cast < char >(data[myi]); } else { if (visibleString.length() >= minStringLength) { rword stringAddr = addr + myi - visibleString.length(); std::stringstream ss; ss << "Address: 0x" << std::hex << stringAddr << ", String: " << visibleString << ", Length: " << visibleString.length() << "\n" ; logManager->logPrint(ss.str().c_str()); } visibleString.clear(); break ; // 遇到不可打印字符时结束向后扫描 } } myi += 1; } // 如果在结束时还有未处理的可打印字符,进行最后的日志记录 if (!visibleString.empty() && visibleString.length() >= minStringLength) { rword stringAddr = addr + myi - visibleString.length(); std::stringstream ss; ss << "Address: 0x" << std::hex << stringAddr << ", String: " << visibleString << ", Length: " << visibleString.length() << "\n" ; logManager->logPrint(ss.str().c_str()); } // 向前扫描,限制范围为 windowSize visibleString.clear(); myi = -1; // 重置索引,向前扫描 nullCharCount = 0; // 重置 \0 计数 while (myi >= -windowSize) { if (isMemoryAccessible(addr + myi, 1)) { if (data[myi] == '\0' ) { nullCharCount++; if (nullCharCount > maxNullSkip) { break ; // 如果跳过的 \0 数量超过限制,停止扫描 } // 如果是 '\0' 且还未超过限制,则跳过并继续扫描 myi -= 1; continue ; } nullCharCount = 0; // 重置 \0 字符计数 if (std::isprint(data[myi])) { visibleString = static_cast < char >(data[myi]) + visibleString; // 向前拼接字符串 } else { if (visibleString.length() >= minStringLength) { rword stringAddr = addr + myi + 1; // 调整地址,确保正确记录字符串开始位置 std::stringstream ss; ss << "Address: 0x" << std::hex << stringAddr << ", String: " << visibleString << ", Length: " << visibleString.length() << "\n" ; logManager->logPrint(ss.str().c_str()); } visibleString.clear(); break ; // 遇到不可打印字符时结束向前扫描 } } myi -= 1; } // 如果向前扫描时发现了未处理的字符串,也进行日志记录 if (!visibleString.empty() && visibleString.length() >= minStringLength) { rword stringAddr = addr + myi + 1; std::stringstream ss; ss << "Address: 0x" << std::hex << stringAddr << ", String: " << visibleString << ", Length: " << visibleString.length() << "\n" ; logManager->logPrint(ss.str().c_str()); } } |
方案3 管道
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 | inline void searchVisibleStringsInMemoryPipe(rword addr, size_t size, LogManager *logManager) { int pipefd[2]; if (pipe(pipefd) == -1) { logManager->logPrint( "创建管道失败\n" ); return ; } pid_t pid = fork(); if (pid == -1) { logManager->logPrint( "创建子进程失败\n" ); close(pipefd[0]); close(pipefd[1]); return ; } if (pid == 0) { // 子进程:读取内存并写入管道 close(pipefd[0]); // 关闭读取端 uint8_t *data = reinterpret_cast <uint8_t *>(addr); ssize_t bytesToWrite = size; ssize_t written = 0; while (written < bytesToWrite) { ssize_t result = write(pipefd[1], data + written, bytesToWrite - written); if (result == -1) { logManager->logPrint( "子进程写入管道失败\n" ); break ; } written += result; } logManager->logPrint( "全部写入完毕\n" ); close(pipefd[1]); // 关闭写入端 _exit(0); } else { // 父进程:从管道读取数据并处理 close(pipefd[1]); // 关闭写入端 std::vector<uint8_t> buffer(size); ssize_t totalRead = 0; while (totalRead < static_cast <ssize_t>(size)) { ssize_t bytesRead = read(pipefd[0], buffer.data() + totalRead, size - totalRead); if (bytesRead == -1) { logManager->logPrint( "父进程读取管道失败\n" ); break ; } if (bytesRead == 0) { // 管道关闭 break ; } totalRead += bytesRead; } // 处理读取的数据 std::string visibleString; for (ssize_t i = 0; i < totalRead; ++i) { if (std::isprint(buffer[i])) { visibleString += static_cast < char >(buffer[i]); } else { if (visibleString.length() >= minStringLength) { rword stringAddr = addr + i - visibleString.length(); std::stringstream ss; ss << "地址: 0x" << std::hex << stringAddr << ", 字符串: " << visibleString << "\n" ; logManager->logPrint(ss.str().c_str()); } visibleString.clear(); } } // 检查数据结尾是否有未处理的字符串 if (visibleString.length() >= minStringLength) { rword stringAddr = addr + totalRead - visibleString.length(); std::stringstream ss; ss << "地址: 0x" << std::hex << stringAddr << ", 字符串: " << visibleString << "\n" ; logManager->logPrint(ss.str().c_str()); } close(pipefd[0]); // 关闭读取端 waitpid(pid, nullptr, 0); // 等待子进程结束 } |
方案4 错误捕获
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 | void searchVisibleStrings(uint8_t *baseAddr, size_t totalSize, LogManager *logManager) { std::string visibleString; struct sigaction sa, old_sa; sa.sa_handler = signalHandler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_NODEFER; sigaction(SIGSEGV, &sa, &old_sa); for ( size_t i = 0; i < totalSize; ++i) { if (sigsetjmp(jumpBuffer, 1) == 0) { uint8_t byte = baseAddr[i]; if (std::isprint(byte)) { visibleString += static_cast < char >(byte); } else { if (visibleString.length() >= minStringLength) { uintptr_t stringAddr = reinterpret_cast < uintptr_t >(baseAddr + i - visibleString.length()); std::stringstream ss; ss << "Address: 0x" << std::hex << stringAddr << ", String: " << visibleString << "\n" ; // std::cout << ss.str(); logManager->logPrint(ss.str().c_str()); } visibleString.clear(); } } else { // 发生段错误,跳过当前字节 visibleString.clear(); continue ; } } // 检查数据结尾是否有未处理的字符串 if (visibleString.length() >= minStringLength) { uintptr_t stringAddr = reinterpret_cast < uintptr_t >(baseAddr + totalSize - visibleString.length()); std::stringstream ss; ss << "Address: 0x" << std::hex << stringAddr << ", String: " << visibleString << "\n" ; logManager->logPrint(ss.str().c_str()); } sigaction(SIGSEGV, &old_sa, NULL); } |
还有4种方案
其实就是遇到SVC指令的时候 读取他的X8拿到系统调用号,拿到调用名称并且打印
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 | // 获取系统调用号 rword syscall_number = gprState->x8; // syscall number in x8 for ARM64 const char *syscall_name = get_syscall_name(syscall_number); // 获取系统调用的参数 rword arg0 = gprState->x0; rword arg1 = gprState->x1; rword arg2 = gprState->x2; rword arg3 = gprState->x3; rword arg4 = gprState->x4; rword arg5 = gprState->x5; if ( strstr (syscall_name, "read" ) != nullptr) { logManager->logPrint( "syscall read(fd=0x%" PRIRWORD ", buf=%s, count=0x%" PRIRWORD ")\n" , arg0, arg1, arg2); return QBDI::CONTINUE; } else if ( strstr (syscall_name, "openat" ) != nullptr) { // 对 openat 系统调用进行专门处理 logManager->logPrint( "syscall openat(dirfd=0x%" PRIRWORD ", pathname=%s, flags=0x%" PRIRWORD ", mode=0x%" PRIRWORD ")\n" , arg0, arg1, arg2, arg3); } else { // 打印系统调用及其参数和返回值 logManager->logPrint( "syscall %s(0x%" PRIRWORD ", 0x%" PRIRWORD ", 0x%" PRIRWORD ", 0x%" PRIRWORD ", 0x%" PRIRWORD ", 0x%" PRIRWORD ")\n" , syscall_name, arg0, arg1, arg2, arg3, arg4, arg5); } |
后面如果想打印更多返回值 就可以像// 对 openat 系统调用进行专门处理 这里一样增加更多case
但是目前只有openat被处理了,后面更新也会更新这里
仓库地址:
f8aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6B7K9i4q4A6N6e0t1H3x3U0u0Q4x3V1k6$3L8g2)9J5k6s2c8J5j5h3y4W2i4K6u0V1M7X3g2D9k6h3q4K6k6b7`.`.
欢迎使用,求个star~ 有问题可以提issue,有问题我会补充到文档里
交流群:(纯技术交流,无广告,欢迎大佬们来交朋友)
预告:
接下来闲下来会完善hook相关的事情,以及做一些Linux内核模块开发的分享(隐藏调试信息,改机等)
更多【Android安全-基于VM的全新Trace框架发布!功能强大,一分钟1.5g,提高你的逆向体验~】相关视频教程:www.yxfzedu.com