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...

相关推荐
chlk12320 小时前
Linux文件权限完全图解:读懂 ls -l 和 chmod 755 背后的秘密
linux·操作系统
阿巴斯甜20 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
舒一笑20 小时前
Ubuntu系统安装CodeX出现问题
linux·后端
Kapaseker20 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
改一下配置文件21 小时前
Ubuntu24.04安装NVIDIA驱动完整指南(含Secure Boot解决方案)
linux
xq952721 小时前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
深紫色的三北六号1 天前
Linux 服务器磁盘扩容与目录迁移:rsync + bind mount 实现服务无感迁移(无需修改配置)
linux·扩容·服务迁移
SudosuBash1 天前
[CS:APP 3e] 关于对 第 12 章 读/写者的一点思考和题解 (作业 12.19,12.20,12.21)
linux·并发·操作系统(os)
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读