Frida 调用 kill 命令挂起&恢复 Android 线程

版权归作者所有,如有转发,请注明文章出处:cyrus-studio.github.io/blog/

Android 线程相关命令

获取 PID

复制代码
adb shell pidof com.shizhuang.duapp

查看线程信息

方法一:进入 /proc//task

进入 adb shell 执行下面命令

bash 复制代码
cd /proc/$(pidof com.shizhuang.duapp)/task
ls

这个目录中每一个子目录的名字就是该 App 的一个线程的 TID(Thread ID)。

你还可以进一步查看每个线程的状态:

bash 复制代码
cat /proc/$(pidof com.shizhuang.duapp)/task/<tid>/status

例如:

bash 复制代码
cat /proc/$(pidof com.shizhuang.duapp)/task/22432/status

方法二:使用 top 或 htop 查看线程

使用 top 查看线程信息

css 复制代码
top -H -p $(pidof com.shizhuang.duapp)
  • -H 表示以线程方式查看

  • -p 指定 PID

或者使用 htop 查看线程信息

css 复制代码
htop -p $(pidof com.shizhuang.duapp)

通过 kill 命令 停止 / 挂起 / 恢复线程

kill 是 Linux 系统中用来向进程发送信号的命令,最常用于终止进程。虽然它名字叫 "kill",但它可以发送多种信号,不只是"终止"。

进入 adb shell,比如线程/进程 id 为 22432

bash 复制代码
# 停止指定进程/线程 
kill 22432

# 强制停止进程/线程
kill -9 22432

# 强制停止 com.cyrus.example
kill -9 $(pidof com.cyrus.example)

# 挂起进程/线程
kill -19 22432

# 继续进程/线程
kill -18 22432

kill 命令详细介绍:man7.org/linux/man-p...

常见信号类型(默认是 SIGTERM):

信号名 数值 含义
SIGHUP 1 挂起信号,通常用于重启进程配置
SIGINT 2 中断信号,类似 Ctrl+C
SIGQUIT 3 退出信号,类似 Ctrl+\
SIGKILL 9 强制终止信号(无法捕获)
SIGTERM 15 终止信号(可捕获、默认)
SIGSTOP 19 暂停进程(无法捕获)
SIGCONT 18 恢复被暂停的进程

使用 Frida 调用 kill 命令

kill 在 C 语言中是定义在 <signal.h> 中的一个标准函数,它本质上是一个系统调用的封装函数。

kill 函数(C 标准库中的定义)

arduino 复制代码
#include <signal.h>

int kill(pid_t pid, int sig);
  • pid:要发送信号的进程 ID。

  • sig:要发送的信号编号,比如 SIGKILL, SIGTERM, SIGSTOP 等。

  • 返回值:返回 0 表示成功。返回 -1 表示失败,并设置 errno。

在底层,kill() 实际上会触发系统调用(比如 Linux 的 syscall kill),让内核发送信号给指定的进程。

它是 UNIX/Linux 系统里最常用的进程间通信(IPC)手段之一。

1. JS + NativeFunction 调用 libc 中的 kill

查找 libc 中的 kill 函数,并使用 NativeFunction 封装引用

ini 复制代码
const killPtr = Module.findExportByName(null, 'kill');
const kill = new NativeFunction(killPtr, 'int', ['int', 'int']);

2. 在 JS 中调用这个函数

挂起线程

arduino 复制代码
// 挂起进程
const result = kill(22432, 19);

if (result === 0) {
    console.log("✅ Signal sent!");
} else {
    console.log("❌ Failed to send signal.");
}

恢复线程

arduino 复制代码
// 恢复线程
const result = kill(22432, 18);

if (result === 0) {
    console.log("✅ Signal sent!");
} else {
    console.log("❌ Failed to send signal.");
}

3. suspendThread / resumeThread

封装 suspendThread / resumeThread 函数用于挂起和恢复线程。

kill.js:

ini 复制代码
// 加载 libc 中的 kill 函数
const killPtr = Module.findExportByName(null, 'kill');
const kill = new NativeFunction(killPtr, 'int', ['int', 'int']);

// 信号常量
const SIGSTOP = 19;  // 暂停进程(不可被捕获)
const SIGCONT = 18;  // 继续执行进程

// 封装 suspend/resume 函数
function suspendThread(pid) {
    const result = kill(pid, SIGSTOP);
    if (result === 0) {
        console.log(`✅ 成功挂起 PID=${pid}`);
    } else {
        console.log(`❌ 挂起失败 PID=${pid}`);
    }
}

function resumeThread(pid) {
    const result = kill(pid, SIGCONT);
    if (result === 0) {
        console.log(`✅ 成功恢复 PID=${pid}`);
    } else {
        console.log(`❌ 恢复失败 PID=${pid}`);
    }
}

执行脚本:

r 复制代码
frida -H 127.0.0.1:1234 -F -l kill.js

调用示例:

scss 复制代码
 // 挂起线程 
suspendThread(22432);  // 替换为目标线程或进程的 PID

// 恢复线程
resumeThread(22432);

suspendOtherThread

实现一个 suspendOtherThread 函数,挂起除了 excludeList 中指定名字的所有线程。

1. 读取线程列表

遍历 /proc/${pid}/task/ 目录下所有目录的名称得到 线程id

ini 复制代码
const taskDir = `/proc/${pid}/task/`;

var dirList = Java.use('java.io.File').$new(taskDir).listFiles()

for (let i = 0; i < dirList.length; i++) {

    // 线程id
    const tid = parseInt(dirList[i].getName());
  
}

2. 读取线程名称

线程名称就在 /proc/ <math xmlns="http://www.w3.org/1998/Math/MathML"> p i d / t a s k / {pid}/task/ </math>pid/task/{tid}/comm 文件中,读取 comm 文件内容得到线程名称。

ini 复制代码
/**
 * 读取文件内容并返回字符串
 *
 * @param path 文件路径
 * @returns {string|null} 文件内容
 */
function readFileAsString(path) {
    const File = Java.use("java.io.File");
    const FileInputStream = Java.use("java.io.FileInputStream");
    const InputStreamReader = Java.use("java.io.InputStreamReader");
    const BufferedReader = Java.use("java.io.BufferedReader");
    const StringBuilder = Java.use("java.lang.StringBuilder");

    const file = File.$new(path);
    if (!file.exists()) {
        console.log(`❌ 文件不存在: ${path}`);
        return null;
    }

    const fis = FileInputStream.$new(file);
    const isr = InputStreamReader.$new(fis, "UTF-8");
    const reader = BufferedReader.$new(isr);
    const sb = StringBuilder.$new();

    let line;
    while ((line = reader.readLine()) !== null) {
        sb.append(line);
    }

    reader.close();
    return sb.toString();
}

/**
 * comm 文件内容就是线程名称
 *
 * @param pid   进程 id
 * @param tid   线程 id
 * @returns {string} 线程名称
 */
function readComm(pid, tid) {
    const path = `/proc/${pid}/task/${tid}/comm`;
    return readFileAsString(path);
}

相关文档:

3. 挂起线程

挂起除了 excludeList 中指定名字的所有线程

ini 复制代码
function suspendOtherThread(pid, excludeList) {
    Java.perform(function () {
        const taskDir = `/proc/${pid}/task/`;

        var dirList = Java.use('java.io.File').$new(taskDir).listFiles()

        for (let i = 0; i < dirList.length; i++) {

            const tid = parseInt(dirList[i].getName());
            if (isNaN(tid)) continue;

            const threadName = readComm(pid, tid);
            if (!threadName) continue;

            // 只要 threadName 包含 excludeList 中的任意关键字,就会被排除(跳过不挂起)
            if (excludeList.some(keyword => threadName.includes(keyword))) {
                console.log(`🟢 跳过线程 "${threadName}" (TID=${tid})`);
            } else {
                console.log(`🛑 挂起线程 "${threadName}" (TID=${tid})`);
                suspendThread(tid);
            }
        }
    })
}

4. 调用示例

ini 复制代码
// 示例:只保留 excludeList 中指定的线程运行,其它全部挂起
const pid = Process.id;
const excludeList = [
    "main",
    "RenderThread",
    "m.cyrus.example",
    "frida",
];

suspendOtherThread(pid, excludeList);

执行脚本

r 复制代码
frida -H 127.0.0.1:1234 -F -l kill.js

效果如下:

resumeOtherThread

恢复线程也是类似。

ini 复制代码
/**
 * 恢复除了 excludeList 中指定名字的所有线程
 * @param {number} pid - 当前进程 PID
 * @param {string[]} excludeList - 不需要恢复的线程名字数组
 */
function resumeOtherThread(pid, excludeList) {
    Java.perform(function () {
        const taskDir = `/proc/${pid}/task/`;

        var dirList = Java.use('java.io.File').$new(taskDir).listFiles()

        for (let i = 0; i < dirList.length; i++) {

            const tid = parseInt(dirList[i].getName());
            if (isNaN(tid)) continue;

            const threadName = readComm(pid, tid);
            if (!threadName) continue;

            // 只要 threadName 包含 excludeList 中的任意关键字,就会被排除(跳过不恢复)
            if (excludeList.some(keyword => threadName.includes(keyword))) {
                console.log(`🟢 跳过线程 "${threadName}" (TID=${tid})`);
            } else {
                console.log(`🔄 恢复线程 "${threadName}" (TID=${tid})`);
                resumeThread(tid);
            }
        }
    })
}

执行脚本

r 复制代码
frida -H 127.0.0.1:1234 -F -l kill.js

调用示例:

ini 复制代码
// 示例:只保留 excludeList 中指定的线程运行,其它全部挂起
const pid = Process.id;
const excludeList = [
    "main",
    "RenderThread",
    "m.cyrus.example",
    "frida",
];

resumeOtherThread(pid, excludeList);

输出如下:

ini 复制代码
🟢 跳过线程 "m.cyrus.example" (TID=31263)
🔄 恢复线程 "Jit thread pool" (TID=31269)
✅ 成功恢复 PID=31269
🔄 恢复线程 "Signal Catcher" (TID=31274)
✅ 成功恢复 PID=31274
🔄 恢复线程 "ADB-JDWP Connec" (TID=31275)
✅ 成功恢复 PID=31275
🔄 恢复线程 "HeapTaskDaemon" (TID=31276)
✅ 成功恢复 PID=31276
🔄 恢复线程 "ReferenceQueueD" (TID=31277)
✅ 成功恢复 PID=31277
🔄 恢复线程 "FinalizerDaemon" (TID=31278)
✅ 成功恢复 PID=31278
🔄 恢复线程 "FinalizerWatchd" (TID=31279)
✅ 成功恢复 PID=31279
🔄 恢复线程 "Binder:31263_1" (TID=31280)
✅ 成功恢复 PID=31280
🔄 恢复线程 "Binder:31263_2" (TID=31281)
✅ 成功恢复 PID=31281
🔄 恢复线程 "Binder:31263_3" (TID=31282)
✅ 成功恢复 PID=31282
🔄 恢复线程 "Profile Saver" (TID=31283)
✅ 成功恢复 PID=31283
🟢 跳过线程 "RenderThread" (TID=31284)
🔄 恢复线程 "Binder:31263_4" (TID=31287)
✅ 成功恢复 PID=31287
🟢 跳过线程 "m.cyrus.example" (TID=31293)
🟢 跳过线程 "gmain" (TID=31294)
🔄 恢复线程 "gdbus" (TID=31296)
✅ 成功恢复 PID=31296
🔄 恢复线程 "Thread-20" (TID=31297)
✅ 成功恢复 PID=31297
🟢 跳过线程 "m.cyrus.example" (TID=31263)
🔄 恢复线程 "Jit thread pool" (TID=31269)
✅ 成功恢复 PID=31269
🔄 恢复线程 "Signal Catcher" (TID=31274)
✅ 成功恢复 PID=31274
🔄 恢复线程 "ADB-JDWP Connec" (TID=31275)
✅ 成功恢复 PID=31275
🔄 恢复线程 "HeapTaskDaemon" (TID=31276)
✅ 成功恢复 PID=31276
🔄 恢复线程 "ReferenceQueueD" (TID=31277)
✅ 成功恢复 PID=31277
🔄 恢复线程 "FinalizerDaemon" (TID=31278)
✅ 成功恢复 PID=31278
🔄 恢复线程 "FinalizerWatchd" (TID=31279)
✅ 成功恢复 PID=31279
🔄 恢复线程 "Binder:31263_1" (TID=31280)
✅ 成功恢复 PID=31280
🔄 恢复线程 "Binder:31263_2" (TID=31281)
✅ 成功恢复 PID=31281
🔄 恢复线程 "Binder:31263_3" (TID=31282)
✅ 成功恢复 PID=31282
🔄 恢复线程 "Profile Saver" (TID=31283)
✅ 成功恢复 PID=31283
🟢 跳过线程 "RenderThread" (TID=31284)
🔄 恢复线程 "Binder:31263_4" (TID=31287)
✅ 成功恢复 PID=31287
🟢 跳过线程 "m.cyrus.example" (TID=31293)
🟢 跳过线程 "gmain" (TID=31294)
🔄 恢复线程 "gdbus" (TID=31296)
✅ 成功恢复 PID=31296
🔄 恢复线程 "Thread-38" (TID=31297)
✅ 成功恢复 PID=31297

完整源码

ini 复制代码
// 加载 libc 中的 kill 函数
const killPtr = Module.findExportByName(null, 'kill');
const kill = new NativeFunction(killPtr, 'int', ['int', 'int']);

// 信号常量
const SIGSTOP = 19;  // 暂停进程(不可被捕获)
const SIGCONT = 18;  // 继续执行进程

/**
 * 挂起线程
 *
 * @param pid 线程 id
 */
function suspendThread(pid) {
    const result = kill(pid, SIGSTOP);
    if (result === 0) {
        console.log(`✅ 成功挂起 PID=${pid}`);
    } else {
        console.log(`❌ 挂起失败 PID=${pid}`);
    }
}

/**
 * 恢复线程
 *
 * @param pid 线程 id
 */
function resumeThread(pid) {
    const result = kill(pid, SIGCONT);
    if (result === 0) {
        console.log(`✅ 成功恢复 PID=${pid}`);
    } else {
        console.log(`❌ 恢复失败 PID=${pid}`);
    }
}

/**
 * 读取文件内容并返回字符串
 *
 * @param path 文件路径
 * @returns {string|null} 文件内容
 */
function readFileAsString(path) {
    const File = Java.use("java.io.File");
    const FileInputStream = Java.use("java.io.FileInputStream");
    const InputStreamReader = Java.use("java.io.InputStreamReader");
    const BufferedReader = Java.use("java.io.BufferedReader");
    const StringBuilder = Java.use("java.lang.StringBuilder");

    const file = File.$new(path);
    if (!file.exists()) {
        console.log(`❌ 文件不存在: ${path}`);
        return null;
    }

    const fis = FileInputStream.$new(file);
    const isr = InputStreamReader.$new(fis, "UTF-8");
    const reader = BufferedReader.$new(isr);
    const sb = StringBuilder.$new();

    let line;
    while ((line = reader.readLine()) !== null) {
        sb.append(line);
    }

    reader.close();
    return sb.toString();
}

/**
 * comm 文件内容就是线程名称
 *
 * @param pid   进程 id
 * @param tid   线程 id
 * @returns {string} 线程名称
 */
function readComm(pid, tid) {
    const path = `/proc/${pid}/task/${tid}/comm`;
    return readFileAsString(path);
}

/**
 * 挂起除了 excludeList 中指定名字的所有线程
 * @param {number} pid - 当前进程 PID
 * @param {string[]} excludeList - 不需要挂起的线程名字数组
 */
function suspendOtherThread(pid, excludeList) {
    Java.perform(function () {
        const taskDir = `/proc/${pid}/task/`;

        var dirList = Java.use('java.io.File').$new(taskDir).listFiles()

        for (let i = 0; i < dirList.length; i++) {

            const tid = parseInt(dirList[i].getName());
            if (isNaN(tid)) continue;

            const threadName = readComm(pid, tid);
            if (!threadName) continue;

            // 只要 threadName 包含 excludeList 中的任意关键字,就会被排除(跳过不挂起)
            if (excludeList.some(keyword => threadName.includes(keyword))) {
                console.log(`🟢 跳过线程 "${threadName}" (TID=${tid})`);
            } else {
                console.log(`🛑 挂起线程 "${threadName}" (TID=${tid})`);
                suspendThread(tid);
            }
        }
    })
}

/**
 * 恢复除了 excludeList 中指定名字的所有线程
 * @param {number} pid - 当前进程 PID
 * @param {string[]} excludeList - 不需要恢复的线程名字数组
 */
function resumeOtherThread(pid, excludeList) {
    Java.perform(function () {
        const taskDir = `/proc/${pid}/task/`;

        var dirList = Java.use('java.io.File').$new(taskDir).listFiles()

        for (let i = 0; i < dirList.length; i++) {

            const tid = parseInt(dirList[i].getName());
            if (isNaN(tid)) continue;

            const threadName = readComm(pid, tid);
            if (!threadName) continue;

            // 只要 threadName 包含 excludeList 中的任意关键字,就会被排除(跳过不恢复)
            if (excludeList.some(keyword => threadName.includes(keyword))) {
                console.log(`🟢 跳过线程 "${threadName}" (TID=${tid})`);
            } else {
                console.log(`🔄 恢复线程 "${threadName}" (TID=${tid})`);
                resumeThread(tid);
            }
        }
    })
}

// 示例:只保留 excludeList 中指定的线程运行,其它全部挂起
const pid = Process.id;
const excludeList = [
    "main",
    "RenderThread",
    "m.cyrus.example",
    "frida",
];

// suspendOtherThread(pid, excludeList);
resumeOtherThread(pid, excludeList);

执行脚本

r 复制代码
frida -H 127.0.0.1:1234 -F -l kill.js

开源地址:github.com/CYRUS-STUDI...

相关推荐
恋猫de小郭31 分钟前
Android Studio Cloud 正式上线,不只是 Android,随时随地改 bug
android·前端·flutter
old_iron4 小时前
vim定位有问题的脚本/插件的一般方法
linux·编辑器·vim
匹马夕阳6 小时前
(十八)安卓开发中的后端接口调用详讲解
android
做测试的小薄7 小时前
Nginx 命令大全:Linux 与 Windows 系统的全面解析
linux·自动化测试·windows·nginx·环境部署
影龙帝皖8 小时前
Linux网络之局域网yum仓库与apt的实现
linux·服务器·网络
Pigwantofly8 小时前
鸿蒙ArkTS实战:从零打造智能表达式计算器(附状态管理+路由传参核心实现)
android·华为·harmonyos
碎忆8 小时前
在VMware中安装虚拟机Ubuntu
linux·ubuntu
农民小飞侠8 小时前
ubuntu 安装pyllama教程
linux·python·ubuntu
Gracker9 小时前
Android Weekly #202514
android
binderIPC9 小时前
Android之JNI详解
android