看我如何给微信下钩子
之前用web微信协议做了个自动应答机器人但跑了不长时间就被封了于是转战安卓端。如果自己分析协议难度跟工作量就太大所以决定使用xposed。本文将通过一个完整的实例取得会话列表里的全部联系人讲解如何给微信下钩子。
思路
从开发的角度猜想微信的会话列表会保存在sqlite中在UI加载的时候从sqlite中读取出来并用ListView显示在Activity中。所以这里有两个点可以下钩子
- 从sqlite加载会话列表的相关方法。
- 通过ListView的adapter取出相关数据。
简单起见笔者决定使用第2个方案。本文使用的是wechat v6.5.8。
解包
微信没有加壳所以用apktool直接解包得到相关资源。并用dex2jar得到相关jar包方便跟源码。
定位过程
在会话列表中长按某个联系人会弹出ContextMenu。
我们就从这个”标为已读“入手在strings.xml中搜索得到
<string name="bpd">标为已读</string>
接着在public.xml中搜索bpd
得到
<public type="string" name="bpd" id="0x7f080d08" />
这个16进制id值就是该字符串在smali代码中的引用值。我们在smali中搜索0x7f080d08
得该字符串引用id在代码中的变量名
./smali/com/tencent/mm//R$m.smali:2610:.field public static final eHR:I = 0x7f080d08
这里简单解释下。在开发安卓应用时在strings.xml中创建一个字符串(假设取名为dummy值为hello)在layout或其他xml中可以通过@string/dummy引用到hello但在代码中只能通过Activity#getString(R.string.dummy)取得该字符串。如果没有混淆string是R的inner class所以编译后得到R$string.class但是微信混淆了代码所以string与dummy变量名都被替换了在smali中的引用语法为
Lcom/tencent/mm/R$m;->eHR:I
等价于java语法R.m.eHR
。使用grep -Hrn 'R$m;->eHR' ./smali/com/tencent/mm/
搜索找到有两个类引用了这个字符串
挨个查看发现类com.tencent.mm.ui.conversation.f实现了View.OnCreateContextMenuListener, AdapterView.OnItemLongClickListener所以我们可以猜测会话列表的Acitvity大致结构
public class ConversationAct extends Activity{
private ListView contactsView;
public void onCreate(Bundle savedInstanceState) {
//初始化UI
contactsView = (ListView)findViewById(R.id.listView);
contactsView.setAdapter(loadSqliteContacts2Adapter());
f listener = new f(...);
contactsView.setOnCreateContextMenuListener(listener); // 设置上下文回调
contectsView.setOnItemLongClickListener(listener); // 设置长按回调
}
}
f的构造函数
public f(g paramg, ListView paramListView, Activity paramActivity, int[] paramArrayOfInt)
{
...
}
发现入参里有ListView。到这里只要hook这个构造函数拿到paramListView的引用就可以得到它的adapter了。为了分析adapter内的数据继续跟。
对创建上下文菜单的逻辑分析
用grep -Hrn 'ui/conversation/f;-><init>' smali/com/tencent/mm/
搜索找到
smali/com/tencent/mm//ui/conversation/j.smali:1193: invoke-direct {v1, v2, v3, v4, v5}, Lcom/tencent/mm/ui/conversation/f;-><init>(Lcom/tencent/mm/ui/conversation/g;Landroid/widget/ListView;Landroid/app/Activity;[I)V
成功定位到会话列表的UI组件com.tencent.mm.ui.conversation.j
。这个j是一个Fragment里面ListView的初始化
this.uWH.setAdapter(this.uXk);
this.uWH.setOnItemClickListener(new e(this.uXk, this.uWH, aG()));
this.uWH.setOnItemLongClickListener(new f(this.uXk, this.uWH, aG(), this.uZC));
发现f的构造函数第一个参数就是adapter。这里没有设置OnCreateContextMenuListener于是跟进OnItemLongClickListener发现
new h(this.activity).a(paramView, paramInt, paramLong, this, this.mWS, this.uYc[0], this.uYc[1]);
这条语句内部直接调用onCreateContextMenu
方法创建上下文菜单。通过下面的分析发现之所以这样做是为了在onItemLongClick时取得长按item对应的联系人wxid之后在上下文菜单中实现对联系人的相关操作。
动态调试
在f的构造函数中发现将ListView的apater保存到了类的uXk变量中。所以在onItemLongClick回调中下断点观察这个变量就可以得到当前会话列表的内容。用idea载入smali在com.tencent.mm.ui.conversation.f.smali
的553行(或onItemLongClick方法的入口)下断点。
adb shell am start -D -n com.tencent.mm/com.tencent.mm.ui.LauncherUI
开启调试模式关于动态调试的详细步骤参见smali动态调试。在微信启动后长按会话列表中的一项触发回调代码在断点位置成功挂起。监视p0寄存器inspect uXk实例变量得到会话的wxid与昵称。
这个hashmap的key保存了会话的wxidvalue中的nickName是会话的用户昵称。用同样的方法调试onCreateContextMenu会发现代码
w localw = com.tencent.mm.model.c.wj().Oy(this.fVd);
作用是根据wxid取得用户的全部信息(地区、性别等)。但我这里只需要取昵称与wxid就行了如果想根据wxid查询联系人可以用到这个。
编码下钩子
经过上面的分析我们需要做的有
- hook com.tencent.mm.ui.conversation.f的构造函数取到adapter的引用
- 执行相关操作使被hook的代码执行(这里是打开微信拉起ui)。
示例代码如下
public class XMain implements IXposedHookLoadPackage{
public static final String TAG="WxHook ";
String targetPackage = "com.tencent.mm";
static Object adapterObj;
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
if(lpparam.packageName.equals(targetPackage)){
XposedBridge.log(TAG + ">>process: "+lpparam.processName);
if(lpparam.processName.equals(targetPackage)){
XposedBridge.log(TAG+">>开始hook微信主进程");
// get conversation demo
XposedHelpers.findAndHookConstructor(
"com.tencent.mm.ui.conversation.f",
lpparam.classLoader,
XposedHelpers.findClass("com.tencent.mm.ui.conversation.g", lpparam.classLoader),
ListView.class,
Activity.class,
int[].class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
// 创建listener时拿到adapter引用
adapterObj = param.args[0];
XposedBridge.log("成功取得adapter引用: " + adapterObj.getClass().getName());
}
}
);
XposedHelpers.findAndHookMethod(
"com.tencent.mm.ui.conversation.j",
lpparam.classLoader,
"onResume",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
// 在UI渲染后通过反射取得adapter内容。
HashMap<String, Object> map = (HashMap)XposedHelpers.getObjectField(adapterObj, "usf");
if(map==null || map.isEmpty()){
XposedBridge.log("Something went wrong!!!");
}
Stream.of(map)
.map(entry -> String.format("wxid: %s, nickname: %s",
entry.getKey(),
XposedHelpers.getObjectField(entry.getValue(), "nickName")))
.forEach(XposedBridge::log);
}
}
);
}
}
}
}
安装并激活插件后打开微信在渲染结束后可以看到已经成功拿到了会话列表
这里有个小问题就是在第一次渲染时没有拿到数据。重新打开界面就可以了。其实跟到这里离sqlite层面的查询已经很近了如果拿到sqlite层面的相关方法就不用关心ui是否渲染了。给大家提个线索:adapter内的hashmapuXk.usf内的数据并不是一下就全部加载的而是通过重写了adapter的getView方法实时加载的。有兴趣的可以试下。
下钩子的过程总共就两步:
1. 找到要在哪下钩子;
2. 写钩子。
通过hook可以实现消息收发、朋友圈转发、自动加人等批量操作。市面上也有很多通过xposed实现的“微信营销神器”。其实这些工具没什么神秘的代码拿出来也就是几个钩子。真正有技术含量的是第一步正如斯坦敏茨说的“用粉笔画一条线1美金。知道在那里画线9999美金。”
*本文作者:Manwu91,本文属 FreeBuf 原创奖励计划,未经许可禁止转载
寻找网络高手