+
+版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/
+
+
什么是系统调用 (syscall)
+
系统调用是操作系统提供给应用程序的一组接口,允许用户空间程序与内核进行交互。
+
在 Android(基于 Linux 内核)中,系统调用由 软中断 实现,通常通过 svc 指令(在 ARM 架构中)触发。系统调用会将 CPU 从用户模式切换到内核模式,使得程序可以执行更高权限的操作。
+
Android 使用的 C 库是 Bionic,它是为移动设备优化的轻量级 C 库。对应的模块为 libc.so。
+
Bionic 提供了对系统调用的封装。大多数标准库函数(如 printf、malloc、pthread_create)都通过 Bionic 实现,底层调用了相应的系统调用。
+
在 NDK 目录中可以找到相关的系统调用号定义头文件。例如
+
<NDK_PATH>\27.1.12297006\toolchains\llvm\prebuilt\windows-x86_64\sysroot\usr\include\asm-generic\unistd.h
+
+
搜索 bionic 模块 可以找到不同CPU架构下的 syscall 实现
+
+
在 http://androidxref.com/9.0.0_r3/xref/bionic/libc/arch-arm/syscalls/ 可以找到 Android 中所有系统调用的汇编代码文件
+
+
syscall 在 Android 上的应用场景
+
系统工具和调试:如 strace、lsof 等工具,通过 syscall 获取系统状态。
+
安全与反调试:某些安全检测和反调试技术会直接使用 syscall 绕过标准的 libc 函数,以防止被 hook。
+
嵌入式开发:在一些嵌入式系统中,开发者需要直接控制硬件,这时通常会使用 syscall。
+
如何在 Android 中使用 syscall
+
假设我们希望通过 syscall 直接读取文件内容,编写 native 方法代码如下
+
// 引入必要的头文件
+#include <jni.h>
+#include <string>
+#include <fcntl.h> // 文件控制定义(如 O_RDONLY)
+#include <unistd.h> // 系统调用号(如 __NR_openat)
+#include <sys/syscall.h> // 系统调用函数
+#include <android/log.h>
+
+#define LOG_TAG "syscall-lib.cpp"
+
+// 定义 Android 日志宏,用于输出信息级别日志
+#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
+
+// 使用 extern "C" 告诉编译器按照 C 语言的方式来编译和链接这个函数
+extern "C"
+JNIEXPORT jstring JNICALL
+Java_com_cyrus_example_syscall_SyscallActivity_readFileWithSyscall(JNIEnv *env, jobject,
+ jstring path) {
+ // 将 Java 字符串 (jstring) 转换为 C 字符串 (const char *)
+ const char *filePath = env->GetStringUTFChars(path, nullptr);
+
+ // 使用 syscall 系统调用打开文件
+ // __NR_openat 是 openat() 系统调用的调用号
+ // AT_FDCWD 表示使用当前工作目录
+ // O_RDONLY 表示以只读方式打开文件
+ int fd = syscall(__NR_openat, AT_FDCWD, filePath, O_RDONLY);
+
+ // 如果文件打开失败,返回错误信息
+ if (fd < 0) {
+ // 释放通过 GetStringUTFChars 分配的资源
+ env->ReleaseStringUTFChars(path, filePath);
+ return env->NewStringUTF("Failed to open file");
+ }
+
+ // 定义一个缓冲区,用于存储文件内容
+ char buffer[1024];
+
+ // 使用 syscall 系统调用读取文件内容
+ // __NR_read 是 read() 系统调用的调用号
+ // 读取的内容存储到 buffer 中,最多读取 sizeof(buffer) - 1 字节
+ ssize_t bytesRead = syscall(__NR_read, fd, buffer, sizeof(buffer) - 1);
+
+ // 如果读取失败,返回错误信息
+ if (bytesRead < 0) {
+ // 关闭文件描述符
+ syscall(__NR_close, fd);
+
+ // 释放通过 GetStringUTFChars 分配的资源
+ env->ReleaseStringUTFChars(path, filePath);
+ return env->NewStringUTF("Failed to read file");
+ }
+
+ // 使用 syscall 系统调用关闭文件
+ syscall(__NR_close, fd);
+
+ // 释放通过 GetStringUTFChars 分配的资源
+ env->ReleaseStringUTFChars(path, filePath);
+
+ // 确保缓冲区以 '\0' 结尾(C 字符串需要以 '\0' 作为结束符)
+ buffer[bytesRead] = '\0';
+
+ // 输出读取到的文件内容到控制台
+ printf("File content: %s\n", buffer);
+
+ // 将读取到的文件内容转换为 Java 字符串 (jstring) 并返回
+ return env->NewStringUTF(buffer);
+}
+
代码中用到的系统调用号 __NR_openat 对应的的 openat 方法签名如下
+
int openat(int dirfd, const char *pathname, int flags, ... /* mode_t mode */ );
+
具体可参考 Linux 手册:https://man7.org/linux/man-pages/man2/open.2.html
+
调用 native 方法读取文件并显示文件内容
+
package com.cyrus.example.syscall
+
+import android.os.Bundle
+import android.widget.Button
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import com.cyrus.example.R
+
+class SyscallActivity : AppCompatActivity() {
+
+ // 加载 native 库
+ init {
+ System.loadLibrary("syscall-lib")
+ }
+
+ external fun readFileWithSyscall(path: String): String
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_syscall)
+
+ findViewById<Button>(R.id.button_syscall).setOnClickListener {
+ // 指定文件路径
+ val filePath = "/data/local/tmp/test.txt"
+
+ // 调用 native 方法读取文件内容
+ val fileContent = readFileWithSyscall(filePath)
+
+ // 显示 Toast
+ Toast.makeText(this, fileContent, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+}
+
配置 CMakeLists.txt
+
cmake_minimum_required(VERSION 3.4.1)
+
+find_library( # Sets the name of the path variable.
+ log-lib
+
+ # Specifies the NDK library that you want CMake to locate.
+ log)
+
+add_library( # 设置库的名称
+ syscall-lib
+
+ # 设置库的类型
+ SHARED
+
+ # 设置源文件路径
+ syscall-lib.cpp)
+
+target_link_libraries( # 将 log 库链接到目标库
+ syscall-lib
+ ${log-lib})
+
进入 adb shell,创建文件 test.txt,并输入内容为 hello syscall
+
adb shell
+
+wayne:/ # cd /data/local/tmp/
+wayne:/data/local/tmp # echo "hello syscall" > test.txt
+
调用测试
+
+
内联汇编实现 syscall
+
直接内联汇编实现 syscall 调用可以让你跳过标准库的封装层,隐藏 syscall 调用,防 hook 增加逆向难度。
+
1. 找到 syscall 的汇编代码
+
把 Bionic 模块对应的 libc.so 拉取到本地
+
// 32 位库
+adb pull /system/lib/libc.so
+
+// 64 位库
+adb pull /system/lib64/libc.so
+// 或
+adb pull /apex/com.android.runtime/lib64/bionic/libc.so
+
用 IDA 打开 libc.so,在 Functions 窗口搜索 syscall
+
+
得到 syscall 的汇编代码如下
+
MOV R12, SP // R12 = SP (保存栈指针)
+PUSH {R4-R7} // 保存 R4-R7 到栈
+MOV R7, R0 // R7 = 系统调用号
+MOV R0, R1 // R0 = 第一个参数
+MOV R1, R2 // R1 = 第二个参数
+MOV R2, R3 // R2 = 第三个参数
+LDM R12, {R3-R6} // R3-R6 = 额外参数 (从栈中加载)
+SVC 0 // 触发系统调用
+POP {R4-R7} // 恢复 R4-R7
+CMN R0, #0x1000 // 检查返回值 (是否小于 0)
+BXLS LR // 成功则返回调用地址
+
由于我们内联汇编 syscall 需要和原来的汇编保持一致,不需要编译器自动生成的函数入口代码和退出代码,所有需要用到 “裸函数”(naked function)。
+
2. 裸函数(naked function)
+
在 C 和 C++ 编程中,attribute((naked)) 是 GCC(GNU Compiler Collection)和 Clang 编译器提供的一个属性,用于定义一个 “裸函数”(naked function)。
+
裸函数是一种特殊的函数,它允许你直接控制函数的汇编指令,而不会自动为你生成函数的入口代码(如保存寄存器、调整栈指针)和退出代码(恢复寄存器、恢复栈指针)。
+
attribute((naked)) 提供了一种方式,让开发者完全掌控函数的汇编指令布局,而不受编译器默认生成的代码影响。它适合在对性能要求极高或者需要直接操作硬件的情况下使用,例如系统调用、中断处理程序和上下文切换函数。
+
基本语法
+
__attribute__((naked)) void myFunction() {
+ // 手动编写汇编指令
+}
+
3. 编写内联汇编代码
+
__attribute__((naked)) long raw_syscall(long __number, ...) {
+ __asm__ __volatile__(
+ "MOV R12, SP\n"
+ "PUSH {R4-R7}\n"
+ "MOV R7, R0\n"
+ "MOV R0, R1\n"
+ "MOV R1, R2\n"
+ "MOV R2, R3\n"
+ "LDM R12, {R3-R6}\n"
+ "SVC 0\n"
+ "POP {R4-R7}\n"
+ "mov pc, lr");
+}
+
4. 读取文件内容并返回 kotlin 层调用
+
// 读取文件内容
+std::string read_file(const char *filePath) {
+ char buffer[1024] = {0};
+
+ // 调用 raw_syscall 打开文件
+ int fd = raw_syscall(SYS_openat, 0, filePath, O_RDONLY, 0);
+ if (fd < 0) {
+ return "Failed to open file";
+ }
+
+ // 调用 raw_syscall 读取文件
+ ssize_t bytesRead = raw_syscall(SYS_read, fd, buffer, sizeof(buffer) - 1);
+ if (bytesRead < 0) {
+ raw_syscall(SYS_close, fd);
+ return "Failed to read file";
+ }
+
+ // 关闭文件
+ raw_syscall(SYS_close, fd);
+
+ // 输出读取到的文件内容到控制台
+ LOGI("File content: %s\n", buffer);
+
+ return std::string(buffer);
+}
+
+
+extern "C"
+JNIEXPORT jstring JNICALL
+Java_com_cyrus_example_syscall_SyscallActivity_readFileWithAssemblySyscall(JNIEnv *env, jobject,
+ jstring path) {
+ // 将 Java 字符串 (jstring) 转换为 C 字符串 (const char *)
+ const char *filePath = env->GetStringUTFChars(path, nullptr);
+
+ std::string file_content = read_file(filePath);
+
+ // 释放通过 GetStringUTFChars 分配的资源
+ env->ReleaseStringUTFChars(path, filePath);
+
+ return env->NewStringUTF(file_content.c_str());
+}
+
CMakeLists.txt 加载汇编文件
+
按上面的方法找到 ARM64 和 AMR 的 syscall 的汇编代码。
+
在 cpp 目录 创建 syscall64.s (ARM64)汇编代码文件,定义 raw_syscall 汇编函数
+
.text // 表示接下来的代码段是可执行代码段
+ .global raw_syscall // 将 `raw_syscall` 设为全局符号,使其可以被其他文件引用
+ .type raw_syscall, @function // 指定 `raw_syscall` 是一个函数
+
+raw_syscall:
+ // 将第一个参数 (系统调用号) 传递给 X8 寄存器
+ MOV X8, X0 // X8 = X0, 系统调用号存储在 X8 中
+
+ // 将其余的参数从 X1-X6 依次向前移动一位 (为系统调用准备参数)
+ MOV X0, X1 // X0 = X1, 系统调用的第一个参数
+ MOV X1, X2 // X1 = X2, 系统调用的第二个参数
+ MOV X2, X3 // X2 = X3, 系统调用的第三个参数
+ MOV X3, X4 // X3 = X4, 系统调用的第四个参数
+ MOV X4, X5 // X4 = X5, 系统调用的第五个参数
+ MOV X5, X6 // X5 = X6, 系统调用的第六个参数
+
+ // 使用 SVC 指令触发系统调用 (Supervisor Call)
+ SVC 0 // 发起系统调用,中断进入内核态执行
+
+ RET // 返回
+
在 cpp 目录 创建 syscall32.s (ARM)汇编代码文件,定义 raw_syscall 汇编函数
+
.text
+ .global raw_syscall
+ .type raw_syscall,%function
+
+raw_syscall:
+ MOV R12, SP
+ STMFD SP!, {R4-R7}
+ MOV R7, R0
+ MOV R0, R1
+ MOV R1, R2
+ MOV R2, R3
+ LDMIA R12, {R3-R6}
+ SVC 0
+ LDMFD SP!, {R4-R7}
+ mov pc, lr
+
在 C++ 代码文件中添加 raw_syscall 函数声明
+
extern "C" long raw_syscall(long __number, ...);
+
配置 CMakeLists.txt
+
# 启用 C 和汇编语言的支持
+enable_language(C ASM)
+
+# 根据系统处理器架构选择不同的汇编文件
+if (CMAKE_SYSTEM_PROCESSOR MATCHES "aarch") # 检查当前系统是否为 AArch64 (ARM 64-bit) 架构
+ # 为 `syscall64.s` 设置编译标志
+ # `-x assembler-with-cpp` 表示使用 C 预处理器来编译汇编文件
+ set_source_files_properties(syscall64.s PROPERTIES COMPILE_FLAGS "-x assembler-with-cpp")
+
+ # 添加一个共享库 (Shared Library)
+ add_library(
+ syscall-lib # 设置库的名称为 `syscall-lib`
+
+ SHARED # 指定库的类型为共享库
+
+ syscall64.s # 添加 ARM64 汇编源文件
+
+ syscall-lib.cpp # 添加 C++ 源文件
+ )
+
+ # 如果系统处理器架构为 ARM (ARM 32-bit)
+elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "arm")
+ # 为 `syscall32.s` 设置编译标志
+ # `-x assembler-with-cpp` 表示使用 C 预处理器来编译汇编文件
+ set_source_files_properties(syscall32.s PROPERTIES COMPILE_FLAGS "-x assembler-with-cpp")
+
+ # 添加一个共享库 (Shared Library)
+ add_library(
+ syscall-lib # 设置库的名称为 `syscall-lib`
+
+ SHARED # 指定库的类型为共享库
+
+ syscall32.s # 添加 ARM 32 位汇编源文件
+
+ syscall-lib.cpp # 添加 C++ 源文件
+ )
+endif ()
+
调用 raw_syscall
+
// 读取文件内容
+std::string read_file(const char *filePath) {
+ char buffer[1024] = {0};
+
+ // 调用 raw_syscall 打开文件
+ int fd = raw_syscall(SYS_openat, 0, filePath, O_RDONLY, 0);
+ if (fd < 0) {
+ return "Failed to open file";
+ }
+
+ // 调用 raw_syscall 读取文件
+ ssize_t bytesRead = raw_syscall(SYS_read, fd, buffer, sizeof(buffer) - 1);
+ if (bytesRead < 0) {
+ raw_syscall(SYS_close, fd);
+ return "Failed to read file";
+ }
+
+ // 关闭文件
+ raw_syscall(SYS_close, fd);
+
+ // 输出读取到的文件内容到控制台
+ LOGI("File content: %s\n", buffer);
+
+ return std::string(buffer);
+}
+
+
+extern "C"
+JNIEXPORT jstring JNICALL
+Java_com_cyrus_example_syscall_SyscallActivity_readFileWithAssemblySyscall(JNIEnv *env, jobject,
+ jstring path) {
+ // 将 Java 字符串 (jstring) 转换为 C 字符串 (const char *)
+ const char *filePath = env->GetStringUTFChars(path, nullptr);
+
+ std::string file_content = read_file(filePath);
+
+ // 释放通过 GetStringUTFChars 分配的资源
+ env->ReleaseStringUTFChars(path, filePath);
+
+ return env->NewStringUTF(file_content.c_str());
+}
+
最后,运行测试
+
+
源码
+
完整源码地址:https://github.com/CYRUS-STUDIO/AndroidExample
+
+
+
+