背景
经常会遇到各种各样需要使用jdwp知识的场景,比如调试Java源码、比如抓帧等等,这些关联知识点通常都会极其复杂,如果不能很好的了解jdwp协议的相关知识,那么做进一步的逆向将极其困难,本文将对jdwp进行详细的介绍和使用介绍。
简介
JDWP(Java Debug Wire Protocol)即JAVA调试器无线协议,它定义了调试器(Debugger)和被调试的JAVA虚拟机(Target vm)之间的通信协议。Debugger 和Target vm 分别在各自的进程中运行,他们之间的通信协议就是JDWP。JDWP 与其他许多协议不同,它仅仅定义了数据传输的格式,但并没有指定具体的传输方式。这就意味着一个JDWP 的实现可以不需要做任何修改就正常工作在不同的传输方式上。JDWP 是语言无关的。理论上我们可以选用任意语言实现JDWP。然而我们注意到,在JDWP 的两端分别是Target vm 和Debugger。Target vm 端,JDWP 模块必须以Agent library 的形式在Java 虚拟机启动时加载,并且它必须通过Java 虚拟机提供的JVMTI 接口实现各种debug 的功能,所以必须使用C/C++ 语言编写。而Debugger 端就没有这样的限制,可以使用任意语言编写,只要遵守JDWP 规范即可。JDI(Java Debug Interface)就包含了一个Java 的JDWP debugger 端的实现,JDK 中调试工具jdb 也是使用JDI 完成其调试功能的。
使用
- 获取调试进程PID
adb jdwp - 转发端口
adb forward tcp:<port> jdwp:<pid> - 附加调试
jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=<port> jdb -attach IP:PORT
命令集合
> help
** 命令列表 **
connectors -- 列出此 VM 中可用的连接器和传输
run [class [args]] -- 开始执行应用程序的主类
threads [threadgroup] -- 列出线程
thread <thread id> -- 设置默认线程
suspend [thread id(s)] -- 挂起线程 (默认值: all)
resume [thread id(s)] -- 恢复线程 (默认值: all)
where [<thread id> | all] -- 转储线程的堆栈
wherei [<thread id> | all]-- 转储线程的堆栈, 以及 pc 信息
up [n frames] -- 上移线程的堆栈
down [n frames] -- 下移线程的堆栈
kill <thread id> <expr> -- 终止具有给定的异常错误对象的线程
interrupt <thread id> -- 中断线程
print <expr> -- 输出表达式的值
dump <expr> -- 输出所有对象信息
eval <expr> -- 对表达式求值 (与 print 相同)
set <lvalue> = <expr> -- 向字段/变量/数组元素分配新值
locals -- 输出当前堆栈帧中的所有本地变量
classes -- 列出当前已知的类
class <class id> -- 显示已命名类的详细资料
methods <class id> -- 列出类的方法
fields <class id> -- 列出类的字段
threadgroups -- 列出线程组
threadgroup <name> -- 设置当前线程组
stop in <class id>.<method>[(argument_type,...)]
-- 在方法中设置断点
stop at <class id>:<line> -- 在行中设置断点
clear <class id>.<method>[(argument_type,...)]
-- 清除方法中的断点
clear <class id>:<line> -- 清除行中的断点
clear -- 列出断点
catch [uncaught|caught|all] <class id>|<class pattern>
-- 出现指定的异常错误时中断
ignore [uncaught|caught|all] <class id>|<class pattern>
-- 对于指定的异常错误, 取消 'catch'
watch [access|all] <class id>.<field name>
-- 监视对字段的访问/修改
unwatch [access|all] <class id>.<field name>
-- 停止监视对字段的访问/修改
trace [go] methods [thread]
-- 跟踪方法进入和退出。
-- 除非指定 'go', 否则挂起所有线程
trace [go] method exit | exits [thread]
-- 跟踪当前方法的退出, 或者所有方法的退出
-- 除非指定 'go', 否则挂起所有线程
untrace [methods] -- 停止跟踪方法进入和/或退出
step -- 执行当前行
step up -- 一直执行, 直到当前方法返回到其调用方
stepi -- 执行当前指令
下一步 -- 步进一行 (步过调用)
cont -- 从断点处继续执行
list [line number|method] -- 输出源代码
use (或 sourcepath) [source file path]
-- 显示或更改源路径
exclude [<class pattern>, ... | "none"]
-- 对于指定的类, 不报告步骤或方法事件
classpath -- 从目标 VM 输出类路径信息
monitor <command> -- 每次程序停止时执行命令
monitor -- 列出监视器
unmonitor <monitor
read <filename> -- 读取并执行命令文件
lock <expr> -- 输出对象的锁信息
threadlocks [thread id] -- 输出线程的锁信息
pop -- 通过当前帧出栈, 且包含当前帧
reenter -- 与 pop 相同, 但重新进入当前帧
redefine <class id> <class file name>
-- 重新定义类的代码
disablegc <expr> -- 禁止对象的垃圾收集
enablegc <expr> -- 允许对象的垃圾收集
!! -- 重复执行最后一个命令
<n> <command> -- 将命令重复执行 n 次
help (或 ?) -- 列出命令
version -- 输出版本信息
exit (或 quit) -- 退出调试器
<class id>: 带有程序包限定符的完整类名
<class pattern>: 带有前导或尾随通配符 ('*') 的类名
<thread id>: 'threads' 命令中报告的线程编号
<expr>: Java(TM) 编程语言表达式。
支持大多数常见语法。
可以将启动命令置于 "jdb.ini" 或 ".jdbrc" 中
位于 user.home 或 user.dir 中
源码调用
想要自定义JDWP-Thread,首先需要修改gJdwpOptions的值,该值是在debugger.cc中通过Dbg::ParseJdwpOptions方法来设置的,所以只要用新的配置重新调用一次ParseJdwpOptions即可。
如何找到Dbg::ParseJdwpOptions这个函数地址呢?为了保证每个函数、变量名都有唯一的标识,编译器在将源代码编译成目标文件时会对变量名或函数名进行名字修饰。
nm -D libart.so | grep ParseJdwpOptions 001778d0 T _ZN3art3Dbg16ParseJdwpOptionsERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE
Android N以下
void *handler = dlopen("/system/lib/libart.so", RTLD_NOW);
if(handler == NULL){
LOGD(LOG_TAG,env->NewStringUTF(dlerror()));
}
void (*allowJdwp)(bool);
allowJdwp = (void (*)(bool)) dlsym(handler, "_ZN3art3Dbg14SetJdwpAllowedEb");
allowJdwp(true);
void (*pfun)();
pfun = (void (*)()) dlsym(handler, "_ZN3art3Dbg8StopJdwpEv");
pfun();
bool (*parseJdwpOptions)(const std::string&);
parseJdwpOptions = (bool (*)(const std::string&)) dlsym(handler,
"_ZN3art3Dbg16ParseJdwpOptionsERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE");
std::string options = "transport=dt_socket,address=8000,server=y,suspend=n";
parseJdwpOptions(options);
pfun = (void (*)()) dlsym(handler, "_ZN3art3Dbg9StartJdwpEv");
pfun();
Android N以后
fd = open(libpath, O_RDONLY);
size = lseek(fd, 0, SEEK_END);
if(size <= 0) fatal("lseek() failed for %s", libpath);
elf = (Elf_Ehdr *) mmap(0, size, PROT_READ, MAP_SHARED, fd, 0);
close(fd);
fd = -1;
if(elf == MAP_FAILED) fatal("mmap() failed for %s", libpath);
ctx = (struct ctx *) calloc(1, sizeof(struct ctx));
if(!ctx) fatal("no memory for %s", libpath);
ctx->load_addr = (void *) load_addr;
shoff = ((char *) elf) + elf->e_shoff;
for(k = 0; k < elf->e_shnum; k++) {
shoff = (char *)shoff + elf->e_shentsize;
Elf_Shdr *sh = (Elf_Shdr *) shoff;
log_dbg("%s: k=%d shdr=%p type=%x", __func__, k, sh, sh->sh_type);
switch(sh->sh_type) {
case SHT_DYNSYM:
if(ctx->dynsym) fatal("%s: duplicate DYNSYM sections", libpath);
ctx->dynsym = malloc(sh->sh_size);
if(!ctx->dynsym) fatal("%s: no memory for .dynsym", libpath);
memcpy(ctx->dynsym, ((char *) elf) + sh->sh_offset, sh->sh_size);
ctx->nsyms = (sh->sh_size/sizeof(Elf_Sym)) ;
break;
case SHT_STRTAB:
if(ctx->dynstr) break;
ctx->dynstr = malloc(sh->sh_size);
if(!ctx->dynstr) fatal("%s: no memory for .dynstr", libpath);
memcpy(ctx->dynstr, ((char *) elf) + sh->sh_offset, sh->sh_size);
break;
case SHT_PROGBITS:
if(!ctx->dynstr || !ctx->dynsym) break;
ctx->bias = (off_t) sh->sh_addr - (off_t) sh->sh_offset;
break;
}
}
munmap(elf, size);
void *fake_dlsym(void *handle, const char *name)
{
int k;
struct ctx *ctx = (struct ctx *) handle;
Elf_Sym *sym = (Elf_Sym *) ctx->dynsym;
char *strings = (char *) ctx->dynstr;
for(k = 0; k < ctx->nsyms; k++, sym++)
if(strcmp(strings + sym->st_name, name) == 0) {
return (char *)ctx->load_addr + sym->st_value - ctx->bias;
}
return 0;
}
参考
Android远程调试的探索与实现
深入理解Android逆向调试原理
|