如何使用 Xposed 保护自己的权益
本文仅是 Xposed 简单入门使用
我们在注册有些 App 时候会遇到一些关键性证件的上传,包括但不限于身份证,银行卡, 驾照等,有时还会伴有自己正面手持身份证免冠照。这些关键性信息被上传后怎么处置,对大多数人来说都是未知的,
这些信息有可能有滥用,而我们只能眼看着被滥用。所以这时候我们可能需要将照片打上水印,比如:仅限 XX App 注册审核使用类似的字样,但是不巧的时候,部分 App 并不能从相册选择图片,只能拍照,然后上传。这时候就 Xposed 大法好
本次针对某共享汽车 App 注册提交身份信息部分进行操作,想做到将它拍得的照片替换为我们已经处理过加上水印的照片
思路
- 简单逆向分析 App
- 寻找合适时机 Hook 函数
- 将拍摄的照片修改/替换为本地加上水印的图片
详细分析
App 经过使用可知整个拍照上传的逻辑依次是:长按进入拍照界面,点击拍照,跳转到预览界面,点击取消回到拍照界面,点击确定回到证件上传界面,然后立即开始上传,同样的步骤要进行三次,流程图如下
所以从上可知,Hook 的最佳时机就是拍完照之后预览之前,狸猫换太子,将拍照得到的数据替换为我们本地数据
简单逆向
我们使用 TopActivity 获取到 App 包名和证件类型选择并上传界面的名字以备用,打开 jadx-gui 分析 App:
- 根据包名和界面名字,定位至所需界面 CameraActivity
protected void a(Bundle bundle) {
setRequestedOrientation(0);
setHideVirtualKey(getWindow());
getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(
new OnSystemUiVisibilityChangeListener(this) {
final /* synthetic */ CameraActivity a;
{
this.a = r1;
}
public void onSystemUiVisibilityChange(int i) {
this.a.setHideVirtualKey(this.a.getWindow());
}
});
Intent intent = getIntent();
if (intent != null) {
this.j = intent.getIntExtra("type", 0);
switch (this.j) {
case 0:
this.i = intent.getIntegerArrayListExtra("list");
this.h = intent.getStringExtra("photo_type");
h();
return;
case 1:
case 2:
this.mEg.setVisibility(8);
this.mBg.setBackgroundResource(R.drawable.img_camera_frame_4);
this.textLayout.setVisibility(8);
return;
case 3:
this.mEg.setVisibility(8);
this.mBg.setBackgroundResource(R.drawable.img_camera_frame_1);
this.textLayout.setVisibility(8);
return;
case 4:
case 8:
this.mEg.setVisibility(8);
this.mBg.setBackgroundResource(R.drawable.img_camera_frame_7);
this.textLayout.setVisibility(8);
return;
case 5:
this.mEg.setVisibility(8);
this.mBg.setBackgroundResource(R.drawable.img_camera_frame_6);
this.textLayout.setVisibility(8);
return;
case 6:
this.mEg.setVisibility(8);
this.mBg.setBackgroundResource(R.drawable.img_camera_frame_3);
this.textLayout.setVisibility(8);
return;
case 7:
this.mEg.setVisibility(8);
this.mBg.setBackgroundResource(R.drawable.img_camera_frame_2);
this.textLayout.setVisibility(8);
return;
default:
return;
}
}
}
通过分析,基本可以肯定这部分代码就是 onCreate(Bundle bundle)。其中取出了 Intent 的数据进行界面渲染(这个界面在拍照时是通用的,只是拍照界面的蒙版不同,所以这里取出了 Intent 进行判断蒙版类型)。我们 Hook 这个界面,intnet 的数据也是需要的,我们在代码中需要对照片类型进行判断(主要是身份证正反面的判断),所以我在此处多留意了一下
- 既然是拍照,那去看看点击事件:
@OnClick(a = {2131296340, 2131296620, 2131297361})
public void onClick(View view) {
switch (view.getId()) {
case R.id.back:
onBackPressed();
return;
case R.id.flash:
this.e = (this.e + 1) % a.length;
this.mFlash.setImageResource(c[this.e]);
((a) this.a_).a(a[this.e]);
return;
case R.id.take_photo:
this.mTakePhoto.setEnabled(false);
((a) this.a_).a(new i<byte[]>(this) {
final /* synthetic */ CameraActivity a;
{
this.a = r1;
}
public void a(byte[] bArr, Camera camera) {
this.a.getBitMap(bArr, new File(this.a.getCacheDir(), System.currentTimeMillis() + ".jpg"));
}
});
return;
default:
return;
}
}
根据资源名字可以得知监听了返回键、闪光灯、拍照键、这也和我们界面对得上
接下来重点关注拍照,可以看到有个回调 public void a(byte[] bArr, Camera camera),这应该就是拍照之后的回调了,其中的 bArr 包含的就是相机拍照后携带的数据,回调内的 this.a.getBitMap(byte[] b , File file) 猜测应该是将
byte[]
图片数据转为Bitmap
的,我们继续跟
- this.a.getBitMap(byte[] b , File file)
public void getBitMap(byte[] bArr, File file) {
FileUtil.byte2File(bArr, file);
b(file);
}
果不其然,通过相机回调中的入参到这里可以知道,拍照结束后,初始化了一个空的 File 出来,文件路径位于
data/data/包名/cache
下,文件名为系统当前时间.jpg
。紧接着在 getBitMap() 中通过 FileUtil 将照片的 byte[] 数据写入到 File 中。其实可以在这里进行 hook 了,我们只需要将入参 byte[] bArr 拦截修改为自己本地的数据就好,但我嫌麻烦....毕竟还要转成字节,于是跟着看了看 b(File file)
- b(File file)
private void b(final File file) {
f.a((Context) this).a(file).b(100).a(new g(this) {
final /* synthetic */ CameraActivity b;
public void a() {
}
public void a(File file) {
if (this.b.j == 0) {
this.b.k = BitmapFactory.decodeFile(file.getPath());
this.b.a(file);
return;
}
this.b.mTakePhoto.setEnabled(true);
Intent intent = new Intent(this.b, PreviewActivity.class);
intent.putExtra(au.a, file.getPath());
intent.putExtra("type", this.b.j);
this.b.startActivity(intent);
}
public void a(Throwable th) {
if (this.b.j == 0) {
this.b.k = FileUtil.getHortalBitmap(file.getPath());
this.b.a(file);
return;
}
this.b.mTakePhoto.setEnabled(true);
Intent intent = new Intent(this.b, PreviewActivity.class);
intent.putExtra(au.a, file.getPath());
intent.putExtra("type", this.b.j);
this.b.startActivity(intent);
}
}).a();
}
这段代码就有意思了,用过鲁班的人应该就比较清楚了(我也是这段时间重新把我们项目中的鲁班看了看,刚好有印象),这段代码极像鲁班的调用方式,最后的
.a()
不出意外应该是鲁班的.launch()
方法,后面我特地去验证了下,确实是鲁班,哈哈,当然这是题外话了。内部有点像 Rx 的回调,当然这个也不是重点...
寻找合适的 Hook 时机
看到这,流程基本清楚了:通过点击拍照,然后初始化一个 File,通过辅助工具类将图片数据写入 File 中,然后紧接着使用鲁班进行压缩,完毕后带着图片路径和拍照类型进入到预览界面
到此,Hook 入手点就有了,谋求简单的话,直接 Hook 方法 b(File file)
的入参,我们将入参修改为我们自己的本地图片即可
开始 Hook
-
导入 Xposed 框架
主工程下的 Gradle 下引入依赖
compileOnly 'de.robv.android.xposed:api:82'
当然也可以一并在引入源码,方面查阅
-
新建一个类实现
IXposedHookLoadPackage
接口并实现其方法class Hook : IXposedHookLoadPackage { @Throws(Throwable::class) override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { } }
-
在主工程下新建
assets
目录,然后在其中新建一个无后缀名的文件 xposed_init,然后在其中将上面步骤新建的 Hook 类的完整类名写入com.hidetag.xposed.test.Hook
-
在
handleLoadPackage
回调中,我们先用回调参数lpparam
取出包名,然后和我们的目标 App(需要进行 Hook 的 App)的包名进行比较,排除其他 App 干扰if ("com.x.y" == lpparam.packageName) { // 骚操作 }
-
Xposed 给我们提供了很多个 Hook 方式,在这里我们采用
XposedHelpers.findAndHookMethod()
XposedHelpers.findAndHookMethod("com.x.y.module.camera.CameraActivity", lpparam.classLoader, "b", File::class.java, object : XC_MethodHook() { @Throws(Throwable::class) override fun beforeHookedMethod(param: XC_MethodHook.MethodHookParam) { // 骚操作 } })
入参分别是:Hook 的目标界面完整包名,classLoader(可不传),需要精确 Hook 的函数名,函数入参的参数列表,Xposed Hook 函数的回调
这里我们目标完整包名是
com.x.y.module.camera.CameraActivity
,需要 Hook 的函数是b(File file)
(上面逆向时已经知道),参数列表只有一个File -
上面提到过,这个拍照界面是公用的,所以为了偷懒,我想一次性把图片都丢进去,免得多次配置。于是我需要知道当前界面的拍照类型(上面 onCreate(Bundle bundle))有提过那个 Intent
val activity = lpparam.thisObject as Activity val intent = activity.intent val type = intent.getIntExtra("type", 0)
根据
lpparam
可以获取当前环境的Context
,我们在 Activity 中玩耍的,所以可以强转为 Activity,然后根据上面逆向的结果,取出 intent 中的 type那我们怎么知道我们那个界面是什么 type 呢?方法有二:
-
根据我们的点击/长按将这个 type 打印出来,获取我们所需的 type
-
根据上面逆向中的
switch case
中的资源文件分辨this.mBg.setBackgroundResource(R.drawable.img_camera_frame_4);
。我之前说过,这个界面的蒙版是动态根据 type 设置上去的,所以我们去看看资源文件就明白了 -
反编译 App 之后根据上述资源文件名字找到图片,再反向找到我们的身份证和国徽页面蒙版,根据这两个蒙版的名字反向到逆向获得的代码中找到
case
对应的 type,过程略过,我通过这种方式找到,身份证人像页面 type 为1,身份证国徽页为6 -
XposedBridge.log("(type = 1 身份证头像页;type = 6 身份证国徽页)type:$type")
-
-
接下来就是狸猫换太子的过程了,我们将目标 App 中的 File 替换为手机
SD
卡的图片private fun getPic(name: String): File? { val isSdCardExist = Environment.MEDIA_MOUNTED == Environment.getExternalStorageState() if (isSdCardExist) { val sdpath = Environment.getExternalStorageDirectory().absolutePath val filepath = sdpath + File.separator + name return File(filepath) } return null }
-
最后我们将这个狸猫设置回去
private fun setParams(param: XC_MethodHook.MethodHookParam, localPic: File) { if (param.args[0] is File) { param.args[0] = localPic } }
-
完整代码如下
class Hook : IXposedHookLoadPackage { @Throws(Throwable::class) override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { if ("com.gvsoft.gofun" == lpparam.packageName) { XposedHelpers.findAndHookMethod("com.x.y.module.camera.CameraActivity", lpparam.classLoader, "b", File::class.java, object : XC_MethodHook() { @Throws(Throwable::class) override fun beforeHookedMethod(param: XC_MethodHook.MethodHookParam) { val activity = param.thisObject as Activity val intent = activity.intent val type = intent.getIntExtra("type", 0) XposedBridge.log("(type = 1 身份证头像页;type = 6 身份证国徽页)type:$type") when (type) { 1 -> getPic("1.png") 6 -> getPic("2.png") else -> null }?.let { setParams(param, it) } } }) XposedHelpers.findAndHookMethod("com.x.y.module.camera.CameraVerticalActivity", lpparam.classLoader, "a", File::class.java, object : XC_MethodHook() { @Throws(Throwable::class) override fun beforeHookedMethod(param: XC_MethodHook.MethodHookParam) { XposedBridge.log("手持证件照") getPic("3.png")?.let { setParams(param, it) } } }) } } private fun getPic(name: String): File? { val isSdCardExist = Environment.MEDIA_MOUNTED == Environment.getExternalStorageState() if (isSdCardExist) { val sdpath = Environment.getExternalStorageDirectory().absolutePath val filepath = sdpath + File.separator + name return File(filepath) } return null } private fun setParams(param: XC_MethodHook.MethodHookParam, localPic: File) { if (param.args[0] is File) { param.args[0] = localPic } } }
-
里面的 1.png 2.png 3.png 都是我自己为了简单而命名的,直接写死,方便。。如果做成公用的,可以考虑开放名字和图片选择..
-
这样就完成了狸猫换太子,在目标 App 的
b(File file)
函数执行之前,我们将入参替换为自己的数据,之后依靠原逻辑进行运算,产生我们想要的结果 -
至于后面另一个
com.x.y.module.camera.CameraVerticalActivity#b(File file)
同上,因为有个是手持身份证的照片,我希望也将这个替换了,所以又 Hook 了一波,原理和思路不变
结果
Hook 成功,替换成功,我的带有水印的身份证也上传成功了,但遗憾的是没有通过审核,说我图片不清晰(毕竟加了水印) 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂
番外篇,如何使用 PS 给图片加水印
-
首先新建一个空白文档,大小自定,我一般是 5CM x 5CM
-
接下来输入你想打在照片上的水印内容,颜色无妨,随意即可
-
调整文字,选中状态下按住 command 或者 control 键,然后鼠标移到四个顶点任意位置,开始旋转文本,一般斜着 45° 就行,然后文字放中间,调整字号大小到自己满意就行
-
给文本加点效果,右键右边文字图层,选择图层样式,在打开的界面里选择描边,秒变颜色随意,一般都是灰色棕色之类的,我这边是
d0cfcb
,然后点击确定,查看文本状态 -
最后调整效果,将文本图层的填充调整为0,也可以不是0,不透明度调低点,免得最后的水印挡住原图太多,一般百分之六十左右足矣
-
最后一步,保存为图案,编辑->定义图案->输入名字(随意)->保存
-
使用:先打开需要打水印的图片,然后编辑->填充->选择图案->再选择自己保存的图案,点击确定