0 背景
ida 调试时经常需要加载 待调试动态库 之前 attach 到进程,常用的方式是以调试模式(adb shell am set-debug-app -w [--persistent] <packagename>)启动 app,使其处于 Waiting For Debugger 状态,然后 ida attach 到 app,最后使用 jdb -attach 命令使 app 度过 Waiting For Debugger,之后就可以在 ida 中进行调试了。
但是这个模式有些缺点:
- 需要查找 pid,更新 tcp、jdwp 映射,尤其在需要多次调试时,频繁查找更新很烦
jdb -attach时遇到java.io.IOExceptio VirtualMachineManagerImpl.createVirtualMachine问题直接导致程序崩溃,这个在 Android 10 和 Android 12 上必现(其它版本没有试过),Android 9 就没有问题
第 1 个缺点还能忍受,第 2 个缺点就很致命,因为有些软件只有在 Android 10 及之上版本才能运行。
第 2 个问题有大神给出这个 解决方案 没看懂...,所以想到根据 Waiting For Debugger 机制的系统实现方式,修改下源码,这样可以不使用 jdb -attach 也能达到同样的效果。
1 方案目标
自主控制 app 启动是否进入阻塞状态,等待 ida attach 后解除阻塞状态。
2 实现
2.1 环境
手机:pixel 3, AOSP 12.0.0_r34
虚拟机:VMware? Workstation 17 Player(17.5.1 build-23298084)
2.2 系统实现
从 am set-debug-app 命令入手查找 Waiting For Debugger 系统实现,最终找到 set-debug-app 执行位置
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 | private void handleBindApplication(AppBindData data) { ... if (data.debugMode != ApplicationThreadConstants.DEBUG_OFF) { // XXX should have option to change the port. Debug.changeDebugPort(8100); if (data.debugMode == ApplicationThreadConstants.DEBUG_WAIT) { Slog.w(TAG, "Application " + data.info.getPackageName() + " is waiting for the debugger on port 8100..."); IActivityManager mgr = ActivityManager.getService(); try { mgr.showWaitingForDebugger(mAppThread, true); // 显示弹窗 } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } Debug.waitForDebugger(); // 阻塞线程 try { mgr.showWaitingForDebugger(mAppThread, false); // 隐藏弹窗 } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } } else { Slog.w(TAG, "Application " + data.info.getPackageName() + " can be debugged on port 8100..."); } ...} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public static void waitForDebugger() { if (!VMDebug.isDebuggingEnabled()) { //System.out.println("debugging not enabled, not waiting"); return; } if (isDebuggerConnected()) return; // if DDMS is listening, inform them of our plight System.out.println("Sending WAIT chunk"); byte[] data = new byte[] { 0 }; // 0 == "waiting for debugger" Chunk waitChunk = new Chunk(ChunkHandler.type("WAIT"), data, 0, 1); DdmServer.sendChunk(waitChunk); mWaiting = true; while (!isDebuggerConnected()) { try { Thread.sleep(SPIN_DELAY); } // SPIN_DELAY = 200 catch (InterruptedException ie) {} } mWaiting = false; ....} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | static final int WAIT_FOR_DEBUGGER_UI_MSG = 6;...case WAIT_FOR_DEBUGGER_UI_MSG: { synchronized (ActivityManagerService.this) { ProcessRecord app = (ProcessRecord)msg.obj; if (msg.arg1 != 0) { if (!app.waitedForDebugger) { Dialog d = new AppWaitingForDebuggerDialog( ActivityManagerService.this, mUiContext, app); app.waitDialog = d; app.waitedForDebugger = true; d.show(); } } else { if (app.waitDialog != null) { app.waitDialog.dismiss(); app.waitDialog = null; } } }} break; |
2.3 方案实现
模拟系统实现增加一套调试逻辑,难点在于 ActivityThread 如何知道当前 thread 是否需要等待,以及如何通知 ActivityThread 取消等待,跨进程通讯处理比较麻烦,简单起见使用系统属性处理。
增加系统属性
lgd.dbg.process:<list<string>>: 当前包名列表,ActivityThread 匹配到列表中的值就 sleeplgd.dbg.sleepSeconds: <int>:ActivityThread sleep 秒数
这种缺点就是,重启设备后,这些属性值会被重置成默认值。
修改 init.rc 增加系统属性,此处不修改也行,可在 adb shell 中直接 setprop 添加此值。
1 2 3 4 5 6 | # Define default initial receive window size in segments.setprop net.tcp.default_init_rwnd 60# lgd propssetprop lgd.dbg.process nonesetprop lgd.dbg.sleepSeconds 30 |
修改 ActivityThread 处理这俩属性
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 | private void handleBindApplication(AppBindData data) { ... StrictMode.initThreadDefaults(data.appInfo); StrictMode.initVmDefaults(data.appInfo); // 自定义逻辑开始 >>> String lgdDebugProcessStr = SystemProperties.get("lgd.dbg.process", "none"); if (!"none".equals(lgdDebugProcessStr)) { String curProcessName = data.processName; int lgdDebugSleepSeconds = SystemProperties.getInt("lgd.dbg.sleepSeconds", 30); String[] lgdDPs = lgdDebugProcessStr.split(","); for (String pn : lgdDPs) { if (pn.equals(curProcessName)) { try { for (int i = 0; i < lgdDebugSleepSeconds; i++) { Log.i("tag4LGD", curProcessName + " keep sleep: " + (lgdDebugSleepSeconds - i)); Thread.sleep(1000); } Log.i("tag4LGD", curProcessName + " running...."); } catch (InterruptedException e) { Log.e("tag4LGD", curProcessName + " do not want to sleep..", e); } } } } // >>> 自定义逻辑结束 if (data.debugMode != ApplicationThreadConstants.DEBUG_OFF) { ...} |
3 测试
Windows 11 ida7.5 以及 mac ida7.0 测试可行,但是 threads 窗口只显示一个线程,有大神 提到 ida android_server 判定是否加载 libc 是通过固定路径 /system/lib[64]/libc.so 比较的,Android 10 引入的模块化特性导致 libc 路径发生变化,所以匹配逻辑有问题,官方这个在 ida7.4sp1 版本增加了 IDA_LIBC_PATH 解决了这个问题。
ida7.4sp1 及之后版本只需要在执行 android_server64 之前,export ·IDA_LIBC_PATH· 就可以了,比如
1 2 3 4 | # adb shell 中执行如下操作export IDA_LIBC_PATH=/apex/com.android.runtime/lib64/bionic/libc.so/data/local/tmp/android_server64 -p9999 |
很遗憾,mac 是我的主力开发工具,使用的是 7.0 版本,所以需要修改 android_server64 二进制文件解决。经过调试 android_sever64 验证修改二进制的方式确实可以显示其他线程,调试及修改方式如下:
调试
- 将
android_server64拖入 ida64,查找字符串/system/lib64/libc.so,找到引用位置,可以看到如下汇编,打断点12345678910.text:000000000003AC68loc_3AC68 ; CODE XREF: sub_3AC00+A4↓j.text:000000000003AC68741E40F9 LDR X20, [X19,#0x38].text:000000000003AC6CE10315AA MOV X1, X21.text:000000000003AC70E00314AA MOV X0, X20.text:000000000003AC747F79FF97BL .strcmp.text:000000000003AC7880020034CBZ W0, loc_3ACC8 ; 打断点.text:000000000003AC7CE00314AA MOV X0, X20.text:000000000003AC80E10317AA MOV X1, X23.text:000000000003AC847B79FF97BL .strcmp.text:000000000003AC8800020034CBZ W0, loc_3ACC8 - 打开另一个 ida64,随便调试个 so 库(注意两个
android_server64要使用两个端口,不然无法启动) - 调试第二个 so 库后,会走到第 1 步打的断点上
- 执行 CBZ 命令前,主动修改 x0 寄存器,将其值设置为 0
- 观察第二个 ida threads 窗口,可以看到所有线程都现实出来了
所以,修改二进制只需要将断点处的 CBZ 修改成 CBNZ 就可以了,打补丁过程