Android下反调试与反反调试

news2024/9/24 18:53:48

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

反调试检测

反调试检测的几种方式。

1. TrackerId

首先,通过 IDA Pro 的调试器附加到当前 app 进程
image.png
关于IDA Pro调试android app的详细教程可以参考这篇文章【使用IDA Pro动态调试Android APP】

使用 top 命令查看进程状态

top | grep com.cyrus.example

17305 u0_a137      10 -10 4.8G 104M  42M t  0.0   1.8   0:02.02 com.cyrus.example

在输出中,S 表示进程状态,17305 是 PID。

通过head /proc/[pid]/status 可以查看详细的进程状态。

head -n 6 /proc/17305/status

Name:   m.cyrus.example
State:  S (sleeping)
Tgid:   17305
Pid:    17305
PPid:   728
TracerPid:      16208

TracerPid: 16208 说明当前的进程正在被进程 16208 调试或跟踪,否则没有被调试值应该为0。

2. stat

这时我们断点调试 app
image.png

再通过 head /proc/[pid]/status 可以查看详细的进程状态,包括是否被调试等信息。

head -n 6 /proc/17305/status

Name:   m.cyrus.example
State:  t (tracing stop)
Tgid:   17305
Pid:    17305
PPid:   728
TracerPid:      16208

在输出中,t (tracing stop) 表示 app 停止(被调试或其他暂停)

3. wchan

使用 cat 命令查看 /proc/[pid]/wchan 文件,该文件显示进程当前正在等待的内核函数。

cat /proc/17305/wchan

ptrace_stop

在输出中,ptrace_stop 表示进程 17305 当前正在被调试器暂停,等待调试器发出的命令。

4. android app 反调试检测

在 Android app 中实现反调试检测

package com.cyrus.example.antidebug

import android.os.Bundle
import android.os.Debug
import android.util.Log
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.cyrus.example.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.File
import java.io.FileReader
import java.io.IOException
import java.net.InetSocketAddress
import java.net.Socket

class AntiDebugActivity : AppCompatActivity() {

    private val TAG = "AntiDebug"
    private lateinit var debugInfoTextView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_anti_debug)

        // 绑定 TextView
        debugInfoTextView = findViewById(R.id.debugInfoTextView)

        // 使用协程来执行调试检测
        CoroutineScope(Dispatchers.Main).launch {
            val debugInfo = checkDebugInfo()

            // 将调试信息显示到 TextView
            debugInfoTextView.text = debugInfo

            // 打印调试信息到日志
            Log.d(TAG, debugInfo)
        }
    }

    // 检查所有的调试信息
    private suspend fun checkDebugInfo(): String {
        val debugInfoBuilder = StringBuilder()

        val debuggerConnected = isDebuggerConnected()
        val waitingForDebugger = isWaitingForDebugger()

        // 获取TrackerId(TracerPid)
        val tracerPid = hasTracerPid()
        // 从 /proc/self/stat 获取调试状态
        val debugStatus = getProcStatStatus()
        // 获取wchan trace标识
        val wchanStatus = getWchanStatus()

        // 检测 JDWP 端口时使用协程的 IO 线程
        val jdwpDetected = withContext(Dispatchers.IO) {
            detectJDWP()
        }

        debugInfoBuilder.append("Debugging Information:\n")
        debugInfoBuilder.append("Debugger Connected: ").append(debuggerConnected).append("\n")
        debugInfoBuilder.append("Waiting for Debugger: ").append(waitingForDebugger).append("\n")
        debugInfoBuilder.append("JDWP Port (Debugger Attached): ").append(jdwpDetected).append("\n")
        debugInfoBuilder.append("TracerPid: ").append(tracerPid).append("\n")
        debugInfoBuilder.append("状态: ").append(debugStatus).append("\n")
        debugInfoBuilder.append("Wchan 状态: ").append(wchanStatus).append("\n")

        if (debuggerConnected || waitingForDebugger || tracerPid != 0 || jdwpDetected
            || debugStatus == "停止(可能是被调试状态)" || wchanStatus.contains("trace")) {
            debugInfoBuilder.append("\nApp is being debugged!\n")
        } else {
            debugInfoBuilder.append("\nApp is not being debugged.\n")
        }

        return debugInfoBuilder.toString()
    }

    // 方法 1: 使用 Debug.isDebuggerConnected()
    private fun isDebuggerConnected(): Boolean {
        return Debug.isDebuggerConnected()
    }

    // 方法 2: 检查 Debug.waitingForDebugger()
    private fun isWaitingForDebugger(): Boolean {
        return Debug.waitingForDebugger()
    }

    // 方法 3: 返回 TracerPid 的值
    private fun hasTracerPid(): Int {
        try {
            BufferedReader(FileReader("/proc/self/status")).use { reader ->
                var line: String?
                while (reader.readLine().also { line = it } != null) {
                    if (line!!.startsWith("TracerPid:")) {
                        return line!!.split(":")[1].trim().toInt()
                    }
                }
            }
        } catch (e: IOException) {
            e.printStackTrace()
        }
        return 0 // 如果没有找到 TracerPid,返回 0 表示没有被调试
    }

    // 方法 4: 检测调试端口(JDWP),在后台线程中运行
    private fun detectJDWP(): Boolean {
        return try {
            Socket().use { socket ->
                socket.connect(InetSocketAddress("127.0.0.1", 8700), 1000)
            }
            true
        } catch (e: IOException) {
            // 没有调试器连接
            false
        }
    }

    // 从 /proc/self/wchan 获取进程的等待状态
    private fun getWchanStatus(): String {
        try {
            // 读取 /proc/self/wchan 文件
            val wchanFile = File("/proc/self/wchan")
            if (wchanFile.exists()) {
                return wchanFile.readText().trim()
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return "无法获取 Wchan 状态"
    }


    // 解析 /proc/self/stat 获取进程状态
    private fun getProcStatStatus(): String {
        try {
            // 读取 /proc/self/stat 文件
            val statFile = File("/proc/self/stat")
            val statContent = statFile.readText()

            // /proc/self/stat 的内容格式是以空格分隔的字段
            // 第3个字段是进程状态
            val statFields = statContent.split(" ")
            if (statFields.size > 2) {
                val processState = statFields[2] // 进程状态字段
                return when (processState) {
                    "R" -> "运行中"
                    "S" -> "睡眠中"
                    "D" -> "不可中断睡眠中"
                    "T" -> "停止(可能是被调试状态)"
                    "Z" -> "僵尸进程"
                    else -> "未知状态: $processState"
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return "无法获取调试状态"
    }

}

当调试器附加到 app
image.png

源码地址:https://github.com/CYRUS-STUDIO/AndroidExample

修改Android内核,绕过反调试

1. TrackerId

编辑 fs/proc/array.c

修改tpid(TrackerId)固定返回0
image.png

2. stat

编辑 fs/proc/array.c

直接把状态标识T(stopped)和t(tracing stop),修改为S(sleeping)

static const char * const task_state_array[] = {
    "R (running)",    /*   0 */
    "S (sleeping)",       /*   1 */
    "D (disk sleep)",  /*   2 */
    "T (stopped)",    /*   4 */
    "t (tracing stop)",    /*   8 */
    "X (dead)",       /*  16 */
    "Z (zombie)",     /*  32 */
};

修改后

static const char * const task_state_array[] = {
    "R (running)",    /*   0 */
    "S (sleeping)",       /*   1 */
    "D (disk sleep)",  /*   2 */
    "S (sleeping)",       /*   4 */
    "S (sleeping)",    /*   8 */
    "X (dead)",       /*  16 */
    "Z (zombie)",     /*  32 */
};

3. wchan

编辑 fs/proc/base.c

修改 proc_pid_wchan 函数,去掉 trace 检测标识

static int proc_pid_wchan(struct seq_file *m, struct pid_namespace *ns,
            struct pid *pid, struct task_struct *task)
{
    unsigned long wchan;
    char symname[KSYM_NAME_LEN];

    wchan = get_wchan(task);

    if (wchan && ptrace_may_access(task, PTRACE_MODE_READ_FSCREDS)
          && !lookup_symbol_name(wchan, symname))
       seq_printf(m, "%s", symname);
    else
       seq_putc(m, '0');

    return 0;
}

修改后

static int proc_pid_wchan(struct seq_file *m, struct pid_namespace *ns,
            struct pid *pid, struct task_struct *task)
{
    unsigned long wchan;
    char symname[KSYM_NAME_LEN];

    wchan = get_wchan(task);

    if (wchan && ptrace_may_access(task, PTRACE_MODE_READ_FSCREDS)
          && !lookup_symbol_name(wchan, symname))
       // 判断 symname 中是否包含 "trace"
       if (strstr(symname, "trace")) {
          // sys_epoll_wait 是内核中实现 epoll_wait 系统调用的具体函数。
          // 这个表示进程正在等待 ep_poll 函数(通常与 I/O 事件相关)。
          seq_printf(m, "%s", "sys_epoll_wait");
       } else {
          seq_printf(m, "%s", symname);
       }
    else
       seq_putc(m, '0');

    return 0;
}

修改 ro.debugble 使全局可调试

编辑 device/{vendor}/{device}/common_prop.mk

找到 ro.debuggable 这一行。如果没有这一行,你可以手动添加

# Debug
PRODUCT_PROPERTY_OVERRIDES += \
    ro.debuggable=1 \

修改完成后,你就可以调试设备上所有 app 了。

编译和刷新系统

关于如何编译和刷新系统可以参考这篇文章【使用 release key 对 LineageOS 进行编译和签名】

测试

打开 IDA Pro 的调试器附加到当前 app 进程并 Pause process 。

通过命令行读取进程 TracerPid、State 和 wchan 信息检测是否修改成功。

adb shell

# 查看进程信息
top | grep com.cyrus.example
 6780 root         20   0  32M 1.4M 1.0M S  0.0   0.0   0:00.00 grep com.cyrus.example
 5256 u0_a137      10 -10 4.9G 105M  43M S  0.0   1.8   0:02.69 com.cyrus.example

# 查看进程状态
head -n 6 /proc/5256/status
Name:   m.cyrus.example
State:  S (sleeping)
Tgid:   5256
Pid:    5256
PPid:   738
TracerPid:      0

# 查看wchan状态   
wayne:/ # cat /proc/5256/wchan
sys_epoll_wait

wayne:/ # cat /proc/5256/wchan
SyS_epoll_wait

通过APP读取 TracerPid、State 和 wchan 信息,检测是否修改成功。
image.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2161130.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

必应广告投放推广收费标准和流程

在当今竞争激烈的商业环境中,如何精准高效地推广产品与服务,成为企业面临的重大挑战。微软必应Bing广告平台,凭借其强大的技术实力和精准的数据分析能力,已成为众多企业广告推广的首选。云衔科技作为业界领先的数字化营销服务商&a…

【机器学习-无监督学习】聚类

【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈Python机器学习 ⌋ ⌋ ⌋ 机器学习是一门人工智能的分支学科,通过算法和模型让计算机从数据中学习,进行模型训练和优化,做出预测、分类和决策支持。Python成为机器学习的首选语言,…

安卓系统升级后,关于Fiddler工具不能抓取https接口问题

问题原因? 目前安卓手机可以抓取的https接口都在安卓7.0版本以下,有时候抓取Android7.0版本或以上的接口抓取不到 因为Android7.0之后常规手段不能抓Https的包,应用会默认不信任用户安装的证书(手机里自己安装的证书),只信任系统…

最新版FaceFusion3.0.0,最强AI换脸,表情修改,视频换脸,年龄修改,多人换脸,面部遮挡换脸,参数调优

主要修改:表情修改,视频换脸,年龄修改,多人换脸,面部遮挡换脸,参数调优 变更日志 改造一切皆工作的建筑介绍pixel boost换脸者为面部检测器添加多角度处理引入年龄修正处理器推出 Live Portrait 表情恢复处理器推出由 Live Portrait 提供支持的脸部编辑处理器用res…

视频制作软件哪个好?前十名推荐!

在视频制作领域,选择合适的软件是提升创作效率和作品质量的关键。本文将根据软件的适用人群:新手入门和专业领域,以及推荐的书籍,为您详细介绍视频制作软件的前十名。 新手入门级别: 1.影忆 功能特点:新手入…

浙大数据结构:05-树9 Huffman Codes

这道题难度挺大,写起来较为费劲,这里我依然使用了STL库,使得代码量大幅减少不过百行,便于大家理解。 机翻: 1、条件准备 数组存储字符对应频率,n,student存储输入多少字符,有多少学生测试。 …

【Transformers基础入门篇2】基础组件之Pipeline

文章目录 一、什么是Pipeline二、查看PipeLine支持的任务类型三、Pipeline的创建和使用3.1 根据任务类型,直接创建Pipeline,默认是英文模型3.2 指定任务类型,再指定模型,创建基于指定模型的Pipeline3.3 预先加载模型,再…

用二维码收集信息时,在后台可以查看、统计哪些数据?

大家都知道,在二维码上关联表单,就可以扫码填写信息了。那么,收集到的数据在哪里查看?具体可以查看到哪些数据呢? 如果是用草料二维码平台搭建的二维码,前往后台,在表单列表中找到对应的表单&a…

智能Ai语音机器人的应用价值有哪些?

随着时间的推移,人工智能的发展越来越成熟,智能时代也离人们越来越近,近几年人工智能越来越火爆,人工智能的应用已经开始渗透到各行各业,与生活交融,成为人们无法拒绝,无法失去的一个重要存在。…

【解密 Kotlin 扩展函数】命名参数和默认值(十三)

导读大纲 1.0.1 命名参数1.0.2 默认参数值 上一节讲述如何自定义 joinToString 函数来代替集合的默认字符串表示 文末遗留下几个待优化问题–传送门 1.0.1 命名参数 我们要解决的第一个问题涉及函数调用的可读性 例如,请看下面的joinToString调用: joinToString(collection,&…

MyBatis深度剖析:从入门到精通的实践指南

前言 什么是mybatis? MyBatis是一款优秀的持久层框架,用于简化Java应用程序与数据库之间的交互 什么是框架,为什么需要框架技术? 框架技术 是一个应用程序的半成品提供可重用的公共结构按一定规则组织的一组组件框架优势&#x…

【自动化测试】Appium 生态工具以及Appium Desktop如何安装和使用

引言 Appium 是一个开源的自动化测试框架,用于测试原生、移动 Web 和混合应用程序。它支持 iOS、Android 和 Windows 平台。Appium 生态系统包含多个工具和库,这些工具和库可以与 Appium 一起使用,以提高移动应用的自动化测试效率 文章目录 引…

Java面试指南(基础篇)

文章目录 前言01 Java语言的特点02 JVM、JRE及JDK的关系03 Java和C的区别04 基本数据类型05 类型转换06 自动装箱与拆箱07 String的不可变性08 字符常量和字符串常量的区别09 字符串常量池10 String 类的常用方法11 String和StringBuffer、StringBuilder的区别12 switch 是否能…

舒服了!学大模型必看的学习书籍来了

最近整理了日前市面上一大波大模型的书,已经打包成pdf了,大家有需要的,可以自行添加获取,纯福利,无套路,添加后说明是哪本书,会直接给大家!(文末获取) 部分书…

IO 多路转接之 epoll

文章目录 IO 多路转接之 epoll1、IO 多路转接之 poll1.1、poll 函数1.2、poll 函数返回值1.3、Socket 就绪条件1.3.1、读就绪1.3.2、写就绪1.3.3、异常就绪 1.4、poll 的优点1.5、poll 的缺点1.6、poll 改写 select 2、IO 多路转接之 epoll2.1、epoll 函数2.2、epoll_create2.3…

Leetcode 反转链表

使用递归 /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val val; }* ListNode(int val, ListNode next) { this.val val; this.next next; }* }*/ class S…

超低排放燃气锅炉

在全球环保浪潮的推动下,超低排放燃气锅炉以其卓越的环保性能和高效能源利用,正逐渐成为现代热能供应的主力军。作为传统锅炉的升级版,超低排放燃气锅炉不仅在技术上实现了质的飞跃,更在环保和节能方面树立了新的标杆。朗观视觉小…

linux入门到实操-10 控制台显示和输出重定向、监控文件变化、软连接

教程来源:B站视频BV1WY4y1H7d3 3天搞定Linux,1天搞定Shell,清华学神带你通关_哔哩哔哩_bilibili 整理汇总的课程内容笔记和课程资料(包含课程同版本linux系统文件等内容),供大家学习交流下载:…

【Delphi】扩展现有组件创建新的 FireMonkey 组件(步骤一)

本例中演示将TLabel控件扩展成TClockLabel新控件。具体如下: 步骤 1 - 使用新建组件向导创建组件 1. 菜单选择 Component -> New Component。 2. 在新建组件向导的第一页,选择 FireMonkey for Delphi : 3. 在 “Ancestor Component ”页…

【最新华为OD机试E卷-支持在线评测】爱吃蟠桃的孙悟空(100分)多语言题解-(Python/C/JavaScript/Java/Cpp)

🍭 大家好这里是春秋招笔试突围 ,一枚热爱算法的程序员 💻 ACM金牌🏅️团队 | 大厂实习经历 | 多年算法竞赛经历 ✨ 本系列打算持续跟新华为OD-E/D卷的多语言AC题解 🧩 大部分包含 Python / C / Javascript / Java / Cpp 多语言代码 👏 感谢大家的订阅➕ 和 喜欢�…