因为笔者在此之前完全没有安卓逆向的工作经验,所以对方在面试结束后提出远程试岗七天,从而观察笔者的工作能力,结果因为笔者设备的问题无法达到对方的要求,只能说这大概就是有缘无份吧,这里做一份零基础的总结,会一步一步记录自己踩得每一个坑以及心路历程,希望能给后来的新人()一些指引,求加精。
项目名称 | WhatsApp插件开发 |
---|---|
作者 | 刘XX |
时间 | 2023年3月27日 |
机器 | Nexus5、小米6X |
开发一个 xposed 插件,可以在 whatsApp 中导入通讯录功能,输入是手机号,输出是这个手机号对应的id和个人信息,对方还跟贴心的给出了项目预览图,应该是对方近期接到的项目,也可以看出对方没有白嫖我的意思。
首先遇到的难题就是这是个国外的 APP,需要**上网,笔者电脑端目前使用的是付费代理配合 Clash,这里就说一下在电脑上开启代理后可以科学上网之后的设置,重点在于要开启允许局域网的选项和记录端口(记住7890这个端口之后有用)
打开cmd命令行输入ipconfig命令查看当前电脑的ip地址,笔者电脑当前的ip为 192.168.43.208
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
Microsoft Windows [Version
10.0
.
19044.1288
]
(c)
2019
Microsoft Corporation.
All
rights reserved.
C:\Users\Administrator>ipconfig
Windows IP Configuration
Ethernet adapter 以太网:
Media State . . . . . . . . . . . : Media disconnected
Connection
-
specific DNS Suffix . :
Wireless LAN adapter 本地连接
*
1
:
Media State . . . . . . . . . . . : Media disconnected
Connection
-
specific DNS Suffix . :
Wireless LAN adapter 本地连接
*
2
:
Media State . . . . . . . . . . . : Media disconnected
Connection
-
specific DNS Suffix . :
Ethernet adapter VMware Network Adapter VMnet1:
Connection
-
specific DNS Suffix . :
Link
-
local IPv6 Address . . . . . : fe80::
6842
:f609:
4368
:dd6f
%
8
IPv4 Address. . . . . . . . . . . :
192.168
.
241.1
Subnet Mask . . . . . . . . . . . :
255.255
.
255.0
Default Gateway . . . . . . . . . :
Ethernet adapter VMware Network Adapter VMnet8:
Connection
-
specific DNS Suffix . :
Link
-
local IPv6 Address . . . . . : fe80::fc33:c1cf:
5ea9
:f741
%
16
IPv4 Address. . . . . . . . . . . :
192.168
.
139.1
Subnet Mask . . . . . . . . . . . :
255.255
.
255.0
Default Gateway . . . . . . . . . :
Wireless LAN adapter WLAN:
Connection
-
specific DNS Suffix . : lan
IPv6 Address. . . . . . . . . . . :
240e
:
36c
:d9c:d300:
4c18
:
2120
:
55bd
:c26f
Temporary IPv6 Address. . . . . . :
240e
:
36c
:d9c:d300:
9b4
:f4ee:
7eb4
:ce25
Link
-
local IPv6 Address . . . . . : fe80::
4c18
:
2120
:
55bd
:c26f
%
18
IPv4 Address. . . . . . . . . . . :
192.168
.
43.208
Subnet Mask . . . . . . . . . . . :
255.255
.
255.0
Default Gateway . . . . . . . . . : fe80::
1
%
18
192.168
.
43.1
Ethernet adapter 蓝牙网络连接
2
:
Media State . . . . . . . . . . . : Media disconnected
Connection
-
specific DNS Suffix . :
C:\Users\Administrator>
|
在工作机 nexus 5 上连接wifi,要和电脑连接同一个wifi(确保ip的 C 段是一致的),连接成功后修改wifi的配置选项(长按或者点右侧的小箭头),将代理改为手动,主机名设置为电脑的 ip 地址,端口设置为 clash 的端口 7890,保存成功后这部手机就也可以走电脑的clash代理科学上网。
点开APP随便浏览了一下功能,根据对方给出的预览图,可以知道首先是需要定位这个界面的 onCreat 界面,首先考虑的就是直接搜字符串,比如“邀请使用”这四个字,但是拖入 jadx 一番搜索后什么也没有。
这时我就想到,会不是因为是国外的app,默认是英文所以没搜到,于是我把软件调整为英文,观察到英文界面存在Contants Help 等字样,并逐一进行了搜索,但依然没有结果,这里推测可能是对字符串进行了加密处理
字符串走不通就换条路,既然是定位界面,那么显然通过 adb 命令查看最上层的界面是个好办法,这里得有点耐心,多翻一翻找到 whatsapp 相关的地方,可以看到当前界面为 ACTIVITY com.whatsapp/.contact.picker.ContactPicker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
C:\Users\Administrator>adb shell dumpsys activity top
........
TASK com.whatsapp
id
=
167
userId
=
0
ACTIVITY com.whatsapp
/
.contact.picker.ContactPicker
9264d37
pid
=
4208
Local Activity
7762ff6
State:
mResumed
=
true mStopped
=
false mFinished
=
false
mChangingConfigurations
=
false
mCurrentConfig
=
{
1.0
?mcc?mnc [zh_CN] ldltr sw392dp w392dp h714dp
440dpi
nrml
long
port finger
-
keyb
/
v
/
h
-
nav
/
h winConfig
=
{ mBounds
=
Rect(
0
,
0
-
1080
,
2030
) mAppBounds
=
Rect(
0
,
0
-
1080
,
2030
) mWindowingMode
=
fullscreen mActivityType
=
standard} s.
8
themeChanged
=
0
themeChangedFlags
=
0
}
mLoadersStarted
=
true
Active Fragments
in
59ae076
:
#0: 05m{499b677 #0 androidx.lifecycle.LifecycleDispatcher.report_fragment_tag}
mFragmentId
=
#0 mContainerId=#0 mTag=androidx.lifecycle.LifecycleDispatcher.report_fragment_tag
mState
=
5
mIndex
=
0
mWho
=
android:fragment:
0
mBackStackNesting
=
0
mAdded
=
true mRemoving
=
false mFromLayout
=
false mInLayout
=
false
mHidden
=
false mDetached
=
false mMenuVisible
=
true mHasMenu
=
false
mRetainInstance
=
false mRetaining
=
false mUserVisibleHint
=
true
mFragmentManager
=
FragmentManager{
59ae076
in
HostCallbacks{
21522e4
}}
mHost
=
android.app.Activity$HostCallbacks@
21522e4
Child FragmentManager{
3f9834d
in
05m
{
499b677
}}:
FragmentManager misc state:
mHost
=
android.app.Activity$HostCallbacks@
21522e4
mContainer
=
android.app.Fragment$
1
@
1cc8e02
mParent
=
05m
{
499b677
#0 androidx.lifecycle.LifecycleDispatcher.report_fragment_tag}
mCurState
=
5
mStateSaved
=
false mDestroyed
=
false
|
在 jadx 中找到 ContactPicker 的 onCreat 方法,接下来只要直接 HOOK onCreat 方法就成功一半了
xposed的开发环境配置其实我在另一篇笔记里写过,这里为了大家方便()就粘贴过来一份
环境配置较为繁琐,分为以下步骤
复制 XposedBridgeApi-82.jar 到工程中供使用
切换至 Project 模式,在app目录下新建文件夹lib,将 XposedBridgeApi-82.jar 复制到 app/lib 文件夹下
配置依赖
新建 Empty Activity 并在 AndroidManifest.xml 中添加代码
1
2
3
|
<meta
-
data android:name
=
"xposedmodule"
android:value
=
"true"
/
>
<meta
-
data android:name
=
"xposeddescription"
android:value
=
"Xposed模块示例"
/
>
<meta
-
data android:name
=
"xposedminversion"
android:value
=
"54"
/
>
|
新建入口类 Main.java 并实现 IXposedHookLoadPackage 接口
1
2
3
4
5
6
7
8
9
10
11
12
|
package com.example.xposeddemo;
import
de.robv.android.xposed.IXposedHookLoadPackage;
import
de.robv.android.xposed.callbacks.XC_LoadPackage;
public
class
Main implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
}
}
|
复制入口类名
右键入口类 Main — Copy Path/Reference — Copy Reference
配置入口类名文件
app/src/main 文件夹下新建文件夹 assets,app/src/main/assets 文件夹下新建文件 xposed_init,将复制的入口类名粘贴在文件中即可
这里就不讲过多的理论了,jadx中右键想要hook的方法可以直接生成xposed的代码片段,这样我们就有了现成的框架
Main.java
1
2
3
4
5
6
7
8
9
10
11
|
XposedHelpers.findAndHookMethod(
"com.whatsapp.contact.picker.ContactPicker"
, classLoader,
"onCreate"
, android.os.Bundle.
class
, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super
.beforeHookedMethod(param);
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super
.afterHookedMethod(param);
Log.d(
"lxz"
,
"hook start"
);
}
});
|
本以为简单的hook却成了噩梦的开始,因!为!有!反!调!试!
最开始笔者是在nexus5中直接安装的xposed框架,应该是软件检测了这个框架,在jadx中可以看到是有一个Native层AbortHook方法的
正在笔者一筹莫展的时候,对方询问了一下我的进度,好嘛,打了瞌睡就有枕头,对方直接给了反调对抗思路,那就是用面具刷edxposed
./adb reboot bootloader
或关机状态同时按住“音量减”和“电源”直到手机开机,进入 bootloader。fastboot -w update image-hammerhead-lmy48b.zip
为:
我觉着在MagiskRoot前最好先刷一下机,因为这样才能保证你提取的boot文件和手机的系统是对应的,其实刷机还是蛮简单的,谷歌的手机双击bat就可以,小米手机官网有现成的刷机工具,这里就说两个坑,一个是fastboot模式中遇到 wait for devices 的问题,这其实是你的电脑还缺少一个驱动,根据我的经验,下载驱动精灵,它会提示你再安装一个驱动就可以了,另一个坑就是小米的刷机工具右下角默认是刷机后lock,这tm就简直是坑爹,记得改成双清,不然又tm把bl给锁上了(lock再刷机会有0s问题,需要重新解锁)。
在官网下载手机的刷机包,反复解压,直到找到其中的boot.img文件,把这个文件拷贝到手机中
在手机中安装 magisk.apk,依次点击,安装 — 选项 — 下一步 — 方式 — 选择修补一个文件 — 选择刚刚存放在手机中的 boot.img 文件 — 开始,等待执行结束你会发现在 boot.img 所在的目录中多了一个文件(有时候这个文件在电脑中看不见,在手机中重命名后就能看见了,不知道为啥),将这个文件拷贝到刷机包 boot.img 所在的目录,将刷机包原本的boot.img 重命名为 boot.img.bak ,将magisk 生成的这个文件重命名为 boot.img,此时刷机包中的 boot.img 就被 magisk 生成的 boot.img 替换了
有两种方式刷入修改后的 boot.img ,我喜欢偷懒直接刷机,毕竟点击鼠标更简单哈
fastboot flash boot boot.img
命令仅刷入boot.img笔者最开始使用的设备是nexus5,笔者先后经历了
这个安装过程大概历时三天,此时我的心态已然崩溃(因为试岗七天已经过了4天,买设备也来不及),直接开始躺平,这种状态一直持续到试岗失败,退出群聊
事情的转机来自于我老妈说她的小米6X电池不太行了,此时的我转念一想,换个手机我手里不就有个安卓9的手机了么,就这样,小米6X就变成了我的Android逆向工程机。
小米6X的edxposed安装依然遇到版本问题,这里我总结一下使用的版本:
Magisk-v23.0.apk
riru-v25.4.4-release.zip
EdXposed-v0.5.2.2_4683-master-release.zip
EdXposedManager-4.6.2-46200-org.meowcat.edxposed.manager-release.apk
安装xposed插件后,可以看到此时已经成功Hook(nexus5坑我不浅!!!)
此时我们先考虑在改界面添加一个TextView,那么问题就变成了获取Context的问题,根据之前学习的经验,可以通过 findAndHookConstructor来解决,下面上代码
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
package com.example.xposeddemo;
import
android.app.Activity;
import
android.content.ContentResolver;
import
android.content.Context;
import
android.database.Cursor;
import
android.net.Uri;
import
android.os.Bundle;
import
android.util.Log;
import
android.view.Gravity;
import
android.view.ViewGroup;
import
android.widget.FrameLayout;
import
android.widget.TextView;
import
android.widget.Toast;
import
android.content.ContentResolver;
import
java.lang.reflect.Method;
import
java.util.ArrayList;
import
java.util.
List
;
import
de.robv.android.xposed.IXposedHookLoadPackage;
import
de.robv.android.xposed.XC_MethodHook;
import
de.robv.android.xposed.XposedHelpers;
import
de.robv.android.xposed.callbacks.XC_LoadPackage;
public
class
Main implements IXposedHookLoadPackage {
private String packageName
=
"com.whatsapp"
;
private String className
=
packageName
+
".contact.picker.ContactPicker"
;
Context context;
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
hookMainAcivityInit(loadPackageParam);
XposedHelpers.findAndHookMethod(className,
loadPackageParam.classLoader,
"onCreate"
, android.os.Bundle.
class
, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super
.afterHookedMethod(param);
Log.d(
"lxz"
,
"hook start"
);
/
/
获取界面
final Activity mActivity
=
(Activity) param.thisObject;
/
/
创建一个 TextView
TextView textView
=
new TextView(context);
/
/
创建布局,设置参数
FrameLayout.LayoutParams params
=
new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
/
/
设置控件到底端的距离
params.bottomMargin
=
100
;
/
/
设置控件的位置
params.gravity
=
Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
/
/
设置控件文本
textView.setText(
"hook text"
);
/
/
添加 TextView 到 Activity 中
mActivity.addContentView(textView,params);
}
});
}
private void hookMainAcivityInit(XC_LoadPackage.LoadPackageParam loadPackageParam)
{
String packageName
=
loadPackageParam.packageName;
if
(!packageName.equals(packageName))
return
;
Class hookClass
=
XposedHelpers.findClass(
className,loadPackageParam.classLoader);
XposedHelpers.findAndHookConstructor(
hookClass,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super
.beforeHookedMethod(param);
context
=
(Context) param.thisObject;
}
}
);
}
}
|
重启手机后也如预期般的,显示了 hook text 字样
接下来我们只要遍历通讯录后把内容设置到 textView 上就可以了,这部分的内容在之前的 Android 安全笔记中也有提过,读写系统应用通讯录的ContentProvider,其重点在于以下几点:
读写系统应用通讯录的ContentProvider需要权限,分别为
android.permission.READ_CONTACTS 和 android.permission.READ_CONTACTS
数据库中直接看到的 mimetype_id 项并不存在,该项为多表查询,真实字段为 mimetype,可以通过在代码中遍历列名观察到
mimetype 是 String类型,而不是在数据库中看到的 int 类型
添加联系人时,应先在 raw_contacts 中添加一个空项,然后再在 data 中添加各种数据
注意:虽然添加了读写通讯录的权限,但依然要在手机中手动配置应用读写通讯录的权限,这里我踩过坑!!!
如果不会的话请移步我之前的笔记中 ContentProvider 部分,这里我们就不啰嗦了,直接上完整代码
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
package com.example.xposeddemo;
import
android.app.Activity;
import
android.content.ContentResolver;
import
android.content.Context;
import
android.database.Cursor;
import
android.net.Uri;
import
android.os.Bundle;
import
android.util.Log;
import
android.view.Gravity;
import
android.view.ViewGroup;
import
android.widget.FrameLayout;
import
android.widget.TextView;
import
android.widget.Toast;
import
android.content.ContentResolver;
import
java.lang.reflect.Method;
import
java.util.ArrayList;
import
java.util.
List
;
import
de.robv.android.xposed.IXposedHookLoadPackage;
import
de.robv.android.xposed.XC_MethodHook;
import
de.robv.android.xposed.XposedHelpers;
import
de.robv.android.xposed.callbacks.XC_LoadPackage;
public
class
Main implements IXposedHookLoadPackage {
private String packageName
=
"com.whatsapp"
;
private String className
=
packageName
+
".contact.picker.ContactPicker"
;
Context context;
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
hookMainAcivityInit(loadPackageParam);
hookAnonymousInternalClass(loadPackageParam);
}
private void hookMainAcivityInit(XC_LoadPackage.LoadPackageParam loadPackageParam)
{
String packageName
=
loadPackageParam.packageName;
if
(!packageName.equals(packageName))
return
;
Class hookClass
=
XposedHelpers.findClass(
className,loadPackageParam.classLoader);
XposedHelpers.findAndHookConstructor(
hookClass,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super
.beforeHookedMethod(param);
context
=
(Context) param.thisObject;
}
}
);
}
private void hookAnonymousInternalClass(XC_LoadPackage.LoadPackageParam loadPackageParam) {
if
(loadPackageParam.packageName.equals(packageName)){
Log.d(
"lxz"
,
"xposed loading"
);
final Class<?> mMainActivity
=
XposedHelpers.findClass(className,loadPackageParam.classLoader);
XposedHelpers.findAndHookMethod(mMainActivity,
"onCreate"
, Bundle.
class
, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super
.afterHookedMethod(param);
final Activity mActivity
=
(Activity) param.thisObject;
Log.d(
"lxz"
,
"onCreate已加载..."
);
/
/
创建一个 TextView
TextView textView
=
new TextView(context);
/
/
创建布局,设置参数
FrameLayout.LayoutParams params
=
new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
/
/
设置控件到底端的距离
params.bottomMargin
=
0
;
/
/
设置控件的位置
params.gravity
=
Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
List
<PersonInfo> personInfoList
=
new ArrayList<>();
ContentResolver resolver
=
context.getContentResolver();
Uri uri
=
Uri.parse(
"content://com.android.contacts/raw_contacts"
);
Cursor cursor
=
resolver.query(uri,new String[]{
"_id"
,
"display_name"
},null,null,null,null);
if
(cursor !
=
null)
{
while
(cursor.moveToNext())
{
int
id
=
cursor.getInt(
0
);
String name
=
cursor.getString(
1
);
Log.d(
"lxz"
,
"id = "
+
id
+
" name = "
+
name);
uri
=
Uri.parse(
"content://com.android.contacts/raw_contacts/"
+
id
+
"/data"
);
Cursor cursor2
=
resolver.query(uri,new String[]{
"mimetype"
,
"raw_contact_id"
,
"data1"
},null,null,null,null);
PersonInfo personInfo
=
new PersonInfo();
while
(cursor2.moveToNext())
{
String mimetype
=
cursor2.getString(
0
);
int
raw_contact_id
=
cursor2.getInt(
1
);
String data1
=
cursor2.getString(
2
);
Log.d(
"lxz"
,
"minetype = "
+
mimetype
+
" address = "
+
data1);
personInfo.set_id(raw_contact_id);
if
(mimetype.equals(
"vnd.android.cursor.item/phone_v2"
))
{
personInfo.setNumber(data1);
}
else
if
(mimetype.equals(
"vnd.android.cursor.item/postal-address_v2"
)) {
personInfo.setAddress(data1);
}
else
if
(mimetype.equals(
"vnd.android.cursor.item/email_v2"
)) {
personInfo.setEmail(data1);
}
else
if
(mimetype.equals(
"vnd.android.cursor.item/name"
)) {
personInfo.setName(data1);
}
}
personInfoList.add(personInfo);
}
}
String ss
=
new String();
for
(PersonInfo personInfo : personInfoList)
{
Log.d(
"lxz"
,personInfo.toString());
ss
=
ss
+
personInfo.toString();
}
textView.setText(ss);
/
/
添加 TextView 到 Activity 中
mActivity.addContentView(textView,params);
}
});
}
}
}
|
重启后可以看到通讯录的详细信息已经出现在了 whatsapp 中,主体框架已经搭建完毕,剩下就是一些排版和琐碎的工作,这里就不继续演示了(毕竟已经退出群聊了,而且我发现我好像 hook 错界面了,尴尬ing…)
这次的试岗可以说收获颇丰,学习()并巩固了非常多的知识点,这都是之前逆向 creakme 不曾遇到的问题,最重要的是 whatsapp 也算是知名度较高的 app 了,今后的面试官问起来也算是有逆向分析过大型 app 的经验,而且我在这里也给新人们说一个事情,那就是面试官非常喜欢在论坛发表过优秀文章的人,就比如说我之前的帖子被加为优秀后被我写在了简历里,之后面试的每一个面试官都对这个事情非常的感兴趣,好吧,我承认是我的简历平平无奇没有别的看点,但在这里也还是希望和我一样的新人在论坛多发文章一起交流,一起进步。
更多【记一次中联X科的试岗实战项目】相关视频教程:www.yxfzedu.com