如何使用Apk-Medit对APK进行内存搜索和数据修改

如何使用Apk-Medit对APK进行内存搜索和数据修改

本文转自 Alpha_h4ck 并作补充

写在前面的话

内存修改往往是在游戏中实现作弊的最简单方法,而且内存也是我们在安全测试过程中必须要去面对和检测的一个内容。虽然现在社区也已经有类似GameGuardian这种的工具了,但现在还没有针对非Root设备的类似工具。Apk-Medit这款工具是一个针对可调式APK的内存搜索和数据修改工具,该工具专为移动端游戏安全测试而设计,我们可以在非Root设备(无需NDK)中使用该工具。

工具安装

首先,我们需要访问该项目的【GitHub Releases页面】来下载该项目源码。下载完成之后,我们需要将代码拷贝至目标安卓设备的/data/local/tmp/目录下:

1
2
3
$ adb push medit /data/local/tmp/medit

medit: 1 file pushed. 29.0 MB/s (3135769 bytes in 0.103s)

代码构建

我们可以使用make命令来完成代码的构建,这里要求使用Go编译器。代码构建完成之后,使用adb连接设备,它将会把构建好的代码推送至目标Android设备的/data/local/tmp/目录下:

1
2
3
4
5
6
7
$ make

GOOS=linux GOARCH=arm64 GOARM=7 go build -o medit

/bin/sh -c "adb push medit /data/local/tmp/medit"

medit: 1 file pushed. 23.7 MB/s (3131205 bytes in 0.126s)

工具命令

搜索

在内存中搜索特定的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
> find 999982

Search UTF-8 String...

Target Value: 999982([57 57 57 57 56 50])

Found: 0!

------------------------

Search Word...

parsing 999982: value out of range

------------------------

Search Double Word...

Target Value: 999982([46 66 15 0])

Found: 1!

Address: 0xe7021f70

我们还可以指定目标数据类型,比如说字符串、dword和qword等等:

1
2
3
4
5
6
7
8
9
> find dword 999996

Search Double Word...

Target Value: 999996([60 66 15 0])

Found: 1!

Address: 0xe7021f70

过滤

我们可以对搜索结果进行过滤,并匹配当前的搜索值:

1
2
3
4
5
6
7
8
9
> filter 993881

Check previous results of searching dword...

Target Value: 993881([89 42 15 0])

Found: 1!

Address: 0xe7021f70

数据修改

我们可以直接修改目标地址的数据值:

1
2
3
> patch 10

Successfully patched!

ps命令

寻找目标进程,如果只有一个的话,我们可以使用ps命令来自动指定:

1
2
3
4
5
> ps

Package: jp.aktsk.tap1000000, PID: 4398

Target PID has been set to 4398.

绑定进程

如果目标PID是通过ps命令设置的,我们就可以跟目标进程进行绑定,并通过ptrace来终止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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
> attach

Target PID: 4398

Attached TID: 4398

Attached TID: 4405

Attached TID: 4407

Attached TID: 4408

Attached TID: 4410

Attached TID: 4411

Attached TID: 4412

Attached TID: 4413

Attached TID: 4414

Attached TID: 4415

Attached TID: 4418

Attached TID: 4420

Attached TID: 4424

Attached TID: 4429

Attached TID: 4430

Attached TID: 4436

Attached TID: 4437

Attached TID: 4438

Attached TID: 4439

Attached TID: 4440

Attached TID: 4441

Attached TID: 4442

如果目标PID没有设置的话,我们就需要在命令行中专门指定了:

1
> attach <pid>

解绑进程

解绑已绑定的进程:

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
> detach

Detached TID: 4398

Detached TID: 4405

Detached TID: 4407

Detached TID: 4408

Detached TID: 4410

Detached TID: 4411

Detached TID: 4412

Detached TID: 4413

Detached TID: 4414

Detached TID: 4415

Detached TID: 4418

Detached TID: 4420

Detached TID: 4424

Detached TID: 4429

Detached TID: 4430

Detached TID: 4436

Detached TID: 4437

Detached TID: 4438

Detached TID: 4439

Detached TID: 4440

Detached TID: 4441

Detached TID: 4442

导出

显示内存导出数据:

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
> dump 0xf0aee000 0xf0aee300

Address range: 0xf0aee000 - 0xf0aee300

----------------------------------------------

00000000 34 32 20 61 6e 73 77 65 72 20 28 74 6f 20 6c 69 |42 answer (to li|

00000010 66 65 20 74 68 65 20 75 6e 69 76 65 72 73 65 20 |fe the universe |

00000020 65 74 63 7c 33 29 0a 33 31 34 20 70 69 0a 31 30 |etc|3).314 pi.10|

00000030 30 33 20 61 75 64 69 74 64 20 28 61 76 63 7c 33 |03 auditd (avc|3|

00000040 29 0a 31 30 30 34 20 63 68 61 74 74 79 20 28 64 |).1004 chatty (d|

00000050 72 6f 70 70 65 64 7c 33 29 0a 31 30 30 35 20 74 |ropped|3).1005 t|

00000060 61 67 5f 64 65 66 20 28 74 61 67 7c 31 29 2c 28 |ag_def (tag|1),(|

00000070 6e 61 6d 65 7c 33 29 2c 28 66 6f 72 6d 61 74 7c |name|3),(format||

00000080 33 29 0a 31 30 30 36 20 6c 69 62 6c 6f 67 20 28 |3).1006 liblog (|

00000090 64 72 6f 70 70 65 64 7c 31 29 0a 32 37 31 38 20 |dropped|1).2718 |

000000a0 65 0a 32 37 31 39 20 63 6f 6e 66 69 67 75 72 61 |e.2719 configura|

000000b0 74 69 6f 6e 5f 63 68 61 6e 67 65 64 20 28 63 6f |tion_changed (co|

000000c0 6e 66 69 67 20 6d 61 73 6b 7c 31 7c 35 29 0a 32 |nfig mask|1|5).2|

000000d0 37 32 30 20 73 79 6e 63 20 28 69 64 7c 33 29 2c |720 sync (id|3),|

000000e0 28 65 76 65 6e 74 7c 31 7c 35 29 2c 28 73 6f 75 |(event|1|5),(sou|

000000f0 72 63 65 7c 31 7c 35 29 2c 28 61 63 63 6f 75 6e |rce|1|5),(accoun|

退出

如需退出该工具,可以使用exit命令或按下Ctrl+D:

1
2
3
> exit

Bye!

工具测试

我们可以使用make命令来运行测试代码:

1
$ make test

工具使用Demo

image

image

Android 禁止应用多开

Android 禁止应用多开

本文转自 九音 并作补充

Android多开

原理

一种是从多开App中直接加载被多开的App,如平行空间、VirtualApp等,另一种是让用户新安装一个App,但这个App本质上就是一个壳,用来加载被多开的App,其原理和前一种是一样的,市面上多开分身这款App是用的这种形式,用户每分身一个App需新安装一个包名为dkmodel.xxx.xxx的App

Android检测方案

1、检查files目录路径

App的私有目录是/data/data/包名/或/data/user/用户号/包名通过Context.getFilesDir()方法可以拿到私有目录下的files目录。

但是在多开环境下,获取到目录会变为/data/data/多开App的包名/xxxxxxxx或/data/user/用户号/多开App的包名/xxxxxxxx。

示例:

正常使用App上面的代码获取到的路径:

/data/user/0/top.darkness463.virtualcheck/files。

多开路径:

/data/user/0/dkmodel.zom.rxo/virtual/data/user/0/top.darkness463.virtualcheck/files。

2、应用列表检测

应用列表检测不是指简单的遍历应用列表判断是不是安装了多开App,我们并不阻止用户安装多开App并多开其他App,我们只是不希望用户多开我们自己的App,因此不能检测到用户安装了多开App就把他干掉。

多开App都会对context.getPackageName()进行处理,让这个方法返回原始App的包名,因此在被多开的App看来,多开App的包名和原始的那个App的包名一样,因此在多开环境下遍历应用列表时会发现包名等于原始App的包名的应用会有两个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private boolean checkPkg(Context context){
try{
if (context == null){
return false;
}
int count = 0;
String packageName = context.getPackageName();
PackageManager pm = context.getPackageManager();
List<PackageInfo> pkgs = pm.getInstalledPackages(0);
for (PackageInfo info : pkgs){
if (packageName.equals(info.packageName)){
count++;
}
}
return count > 1;
} catch (Exception ignore){}
return false;
}

缺点:

只对部分多开App有效,例如360的分身大师,不少多开App会绕过这项检测

3、Maps检测

读取/proc/self/maps,多开App会加载一些自己的so到内存空间

比如说:

360的分身大师加载了其目录下的某个so,/data/app/com.qihoo.magic-gdEsg8KRAuJy0MuY18BlqQ==/lib/arm/libbreakpad-jni-1.5.so,通过对各种多开App的包名的匹配,如果maps中有多开App的包名的东西,那么当前就是运行在多开环境下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Set<String> virtualPkgs;
private boolean check(){
BufferedReader bufr = null;
try {
bufr = new BufferedReader(new FileReader("/proc/self/maps"));
String line;
while ((line = bufr.readLine()) != null){
for (String pkg : virtualPkgs){
if (line.contains(pkg)){
return true;
}
}
}
} catch (Exception ignore){}
finally {
if (bufr != null){
try {
bufr.close();
} catch (IOException e){}
}
}
return false;
}

缺点:

目前没有发现多开App绕过该项检测,但缺点是需要收集所有多开App的包名,一旦多开App改个包名就失效了。

4、ps检测

通过执行ps命令并以自己的uid进行过滤,得到类似下面的结果:

image

多开环境下:会获取到自己的包名和多开App的包名这2个包名,通过这些包名去/data/data/下找会找到2个目录

而正常情况下只能在/data/data/下找到自己的App的目录

具体方法网址:

(https://blog.csdn.net/shdhenghao3/article/details/94409299)

https://www.sohu.com/a/242918900_659256

四种方案测试结果

image

测试方案顺序1234,测试结果X代表未能检测O成功检测多开;

virtual app测试版本是git开源版,商用版已经修复uid的问题;

image

为了避免歧义,我们接下来所说的app都是指的同一款软件,并定义普通运行的app叫做本体,运行在多开软件上的app叫克隆体。并提出以下两个概念

狭义多开

只要app是通过多开软件打开的,则认为多开,即使同一时间内只运行了一个app

广义多开:

无论app是否运行在多开软件上,只要app在运行期间,有其余的『自己』在运行,则认为多开

最终方案

第1步:扫描本地端口(扫描tcp文件并格式化端口的关键代码)

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
>String tcp6 =CommandUtil.getSingleInstance().exec("cat /proc/net/tcp6");

>if(TextUtils.isEmpty(tcp6))return;

String[] lines =tcp6.split("n");

ArrayListportList =newArrayList<>();

for(inti =0, len = lines.length; i < len; i++) {

intlocalHost = lines[i].indexOf("0100007F:");

//127.0.0.1:的位置

if(localHost <0)continue;

StringsinglePort = lines[i].substring(localHost +9, localHost +13);

//截取端口

Integer port =Integer.parseInt(singlePort,16);

//16进制转成10进制

portList.add(port);

}

第2步:发起连接请求

接下来向每个端口都发起一个线程进行连接,并发送自定义消息,该段消息用app的包名就行了(多开软件很大程度会hook getPackageName方法,干脆就顺着多开软件做)

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
try{

//发起连接,并发送消息

Socket socket=newSocket("127.0.0.1",port);

socket.setSoTimeout(2000);

OutputStreamoutputStream = socket.getOutputStream();

outputStream.write((secret+"n").getBytes("utf-8"));

outputStream.flush();

socket.shutdownOutput();

//获取输入流,这里没做处理,纯打印

InputStreaminputStream = socket.getInputStream();

BufferedReaderbufferedReader =newBufferedReader(newInputStreamReader(inputStream));

String info=null;

while((info = bufferedReader.readLine())!=null) {

Log.i(TAG,"ClientThread: "+ info);

}

bufferedReader.close();

inputStream.close();

socket.close();

}catch(ConnectException e) {

Log.i(TAG, port+"port refused");

}

主动连接的过程完成,先于自己启动的app(可能是本体or克隆体)接收到消息并进行处理。

第3步:成为接收端,等待连接

接下来就是成为接收端,监听某端口,等待可能到来的app连接(可能是本体or克隆体)。

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
privatevoidstartServer(String

secret){

Random random=newRandom();

ServerSocketserverSocket =null;

try{

serverSocket=newServerSocket();

serverSocket.bind(newInetSocketAddress("127.0.0.1",

random.nextInt(55534) +10000));

//开一个10000~65535之间的端口

while(true) {

Socket socket =serverSocket.accept();

ReadThreadreadThread =newReadThread(secret, socket);

//假如这个方案很多app都在用,还是每个连接都开线程处理一些

readThread.start();

//

serverSocket.close();

}

}catch(BindException e) {

startServer(secret);//may be loop forever

}catch(IOException e) {

e.printStackTrace();

}

}

开启端口时为了避免开一个已经开启的端口,主动捕获BindExecption,并迭代调用,可能会因此无限循环,如果怕死循环的话,可以加一个类似ConcurrentHashMap最坏尝试次数的计数值。不过实际测试没那么衰,随机端口范围10000~65535,最多尝试两次就好了。

每一个处理线程,做的事情就是匹配密文,对应上了就是某个克隆体or本体发送的密文,这里是接收端主动运行一个空指针异常,杀死自己。处理方式有点像《三体》的黑暗森林法则,谁先暴露谁先死。

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
privateclassReadThreadextendsThread{

privateReadThread(String

secret, Socket socket){

InputStreaminputStream =null;

try{

inputStream =socket.getInputStream();

bytebuffer[] =newbyte[1024*4];

inttemp =0;

while((temp = inputStream.read(buffer)) !=-1) {

String result=newString(buffer,0, temp);

if(result.contains(secret)) {

//

System.exit(0);

//

Process.killProcess(Process.myPid());

nullPointTV.setText("");

}

}

inputStream.close();

socket.close();

}catch(IOException e) {

e.printStackTrace();

}

}

}

*因为端口通信需要Internet权限,本库不会通过网络上传任何隐私

本文方案已经集成到

github地址:

https://github.com/lamster2018/EasyProtector

fuzz AndroidManifest.xml 实现反编译对抗

fuzz AndroidManifest.xml 实现反编译对抗

本文转自 枫糖甜酒 并作补充

有的恶意APK为了防止被apktool反编译,就会在AndroidManifest.xml里面进行一些特殊处理,来干扰apktool反编译,实现安装运行APK没问题,但是apktool 反编译的时候会出现异常并退出
例如下面这个APK,在apktool 2.8.1版本下,就无法正常反编译,但是却能够adb install安装

image

这篇文章Android免杀小结中提到过可以通过修改AndroidManifest.xml二进制文件中的某一位来干扰apktool的判断,但是告诉我们如何寻找这种能够干扰反编译软件的位,所以本篇会针对单位修改AndroidManifest.xml文件对抗反编译进行讨论

环境准备

本机使用windows系统,测试机 AOSP Android 11,这里的反编译工具是Apktool,截止到今天最新版本是 v2.9.0

image

本地更新一下jar包

image

010Editor,用到这个是为了查看AndroidManifest.xml 的二进制数据格式
现在环境就OK了

AndroidManifest.xml 简介

如果不了解AndroidManifest.xml 文件结构就暴力fuzz未免太粗鲁了
AndroidManifest.xml 是 Android 应用程序的清单文件,用于描述应用程序的基本信息、声明组件和权限等内容,是安卓应用开发中非常重要的一个文件
以之前写的一个AndroidManifest.xml 文件为例:

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
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="hi.beautifulz.myapplication">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature android:name="android.hardware.microphone" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:exported="true"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver
android:name=".MainBroadcastReceiver"
android:label="MainBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service android:name=".MyService" android:exported="true" />
</application>

</manifest>

简单介绍一下该文件:

  • 元素是必须的,它定义了整个文件的根元素,并且包含了一些必要的属性,例如 package

  • 元素用于声明应用程序所需的权限

  • 元素用于声明应用程序所需的设备功能和硬件特性

  • 元素是应用程序的核心元素,它包含了所有的组件和各种配置信息,例如主 activity、自定义 theme、icon 等等。

    • 元素用于声明应用程序中的 Activity 组件
    • 元素用于声明应用程序中的 Service 组件
    • 元素用于声明应用程序中的 Broadcast Receiver 组件
    • 元素用于声明应用程序中的 Content Provider 组件
    • …….

AndroidManifest.xml二进制文件结构

文件大纲

MindMac师傅在看雪发的图

image

当然没基础的话,直接看这个图其实没什么卵用
根据附件里面的AndroidManifest.xml文件生成二进制文件,跟着MindMac的思路使用010Editor进行分析
编码前的xml文件内容如下

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
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.test"
android:versionCode="1"
android:versionName="1.0" >

<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="19" />

<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="false"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.android.test.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

用这个xml生成APK,APK再解压之后得到AndroidManifest.xml二进制文件,丢到010editor里面,以十六进制的格式查看

image

感觉跟MindMac的内容有点不太一样,难道是因为版本的问题,加载AndroidManifest.bt Template

image

可以看到已经把AndroidManifest文件架构列出来了
MindMac把文件分为了五个结构,这里的Magic Number和File Size其实都属于header

image

header内容为

image

所以可以分为四个部分

  • Header : 包括文件魔数和文件大小
  • String Chunk : 字符串资源池
  • ResourceId Chunk : 系统资源 id 信息
  • XmlContent Chunk : 清单文件中的具体信息,其中包含了五个部分

接下来简单分析一下这几个部分

Header

image

AndroidManifest的魔数为 0x00080003

关于魔数
二进制文件的魔数(Magic Number)是一种固定值,用于标识文件类型或格式。不同的文件类型通常具有不同的魔数。

以下是一些常见的二进制文件魔数示例:

  • ELF(可执行和共享目标文件):0x7F 0x45 0x4C 0x46
  • JPEG(图片文件):0xFF 0xD8
  • PNG(可移植网络图形):0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A
  • PDF(便携式文档格式):0x25 0x50 0x44 0x46
  • ZIP(压缩文件):0x50 0x4B 0x03 0x04
  • GIF(图形交换格式):0x47 0x49 0x46 0x38

另外为什么这里是 0x00080003 而不是 0x03000800
是因为清单文件是小端表示的

在早期的apktool会识别AndroidManifest文件,如果魔数不为0x00080003则反编译失败,该方法也用在了某些恶意APK上,比如链安的这篇文章https://www.liansecurity.com/#/main/news/IPONQIoBE2npFSfFbCRf/detail

image

其中修改魔数为 00 00 08 00 则可以实现干扰
该方法在新版本的apktool测试已失效

该文件的filesize为0x00000904即2308字节

image

Other

其他的模块就不一一赘述,如果想要自己跟着分析每一块内容可以参考

总而言之,AndroidManifest里面的每一位都有自己的作用

手动修改AndroidManifest文件

手动修改在010Editor里面修改AndroidManifest,例如这里修改为 00

image

然后压缩成zip文件,修改zip后缀为apk,就能够生效了(这个时候只是修改,并没有干扰反编译软件)

自动化fuzz

手动是不可能手动的

自动化fuzz的AndroidManifest.xml文本内容如下:

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
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.test"
android:versionCode="1"
android:versionName="1.0" >

<uses-sdk android:targetSdkVersion="29" />

<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="false"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.android.test.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

获取apktool解析结果

之前先知上面的这篇文章 https://xz.aliyun.com/t/12893#toc-10 stringPoolSize陷阱,修改字符串个数 stringCount 字段,导致跟实际对应不上,会造成AndroidManifest.xml解析出现问题,但是这个问题 2.9.0已经修复了,我们在2.8.1上先捕捉一下这个错误

image

使用python获取apktool的运行结果,为啥这里写的这么复杂是因为apktool的运行结果直接获取不到,需要Press any key to continue . . .
需要获取实时的运行流才可以确认结果

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
def apktoolDecode() -> bool:
"""
获取apktool的扫描结果
:rtype: object
True 扫描出错
False 扫描成功
"""
apktool_cmd = f"apktool d -f {sign_name} "
process = subprocess.Popen(apktool_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

# 定义一个标志位来表示命令是否已经执行完成
command_completed = threading.Event()

def handle_output(stream, prefix):
for line in stream:
print(f"{prefix}: {line.strip()}")
command_completed.set() # 设置标志位,表示命令已完成

stderr_thread = threading.Thread(target=handle_output, args=(process.stderr, "STDERR"))
stderr_thread.start()
timeout_seconds = 5
command_completed.wait(timeout_seconds)

if not command_completed.is_set():
process.terminate()
return False
else:
process.terminate()
return True

遇到解析不了的APK的时候就会返回True,正常解析的就会返回False

image

优化

简单计算一下会有多少种可能性,前面提到过该文件有2308个字节,一个字节修改范围为 0x00 - 0xFF,即256,所以一共有590848种可能性,如果是单线程运行的话需要八百多个小时

image

蒽….
考虑已知的干扰位置,我们对每一个字节的修改范围变成下面两种可能来缩减范围:

  • 0x00 比如魔数,把 0x03修改为了0x00
  • 跟原本位置不同的数字,比如stringCount原来是0x23 我们修改为0x24

在这个基础上 可能性缩减到了4616

获取结果

在前面的思路上编写出脚本运行就可以了,能够造成apktool 2.9.0 干扰的位置有很多,但是有的位置修改了之后会导致手机也安装不上,出现错误

adb: failed to install .\app-debug.apk: Failure [INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION: Failed to parse /data/app/vmdl1242571071.tmp/base.apk: Corrupt XML binary file]

image

所以我们不仅要能够干扰apktool,还需要修改之后能够正常安装
在原来的基础上添加了自动签名代码

1
2
3
4
def signApk():
subprocess.run(
['jarsigner', '-verbose', '-sigalg', 'SHA1withRSA', '-digestalg', 'SHA1', '-keystore', "./spring.keystore",
'-storepass', "123456", '-keypass', "123456", '-signedjar', "sign.apk", "./app-debug.apk", "spring"])

验证是否能正常安装代码

1
2
3
4
5
6
def installApp():
adb_install_cmd = f'adb install {sign_name}'
result = os.system(adb_install_cmd)
if result == 0:
return True
return False

跑了一会fuzz脚本之后就出现了结果,这里给出一个apktool2.9.0的干扰结果
在String Offsets数组里面(存储每个字符串在字符串池中的相对偏移量),修改0X00000198为0X00005098,为什么是这个值,这里只是找一个能让数组越界的下标值,因为fuzz出来是这个我就填这个了

image

修改之后

image

保存后重新打包成zip,并且签名
安装和运行没问题

image

image

使用apktool 2.9.0 进行反编译,反编译失败

image

jadx对抗

本来准备结束了,Red256问我能不能对抗jadx

image

因为没有遇到我(吐舌

使用jadx最新版本1.4.7,设置前面给出的干扰位置,把重新压缩的APK丢到jadx里面

image

AndroidManifest.xml解析失败,对抗成功
给APK签名后检查能否安装

jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ./spring.keystore -storepass 123456 -keypass 123456 -signedjar sign.apk ./app-debug.apk spring

安装成功

image

小结

对于文件有增删查改四种操作,除了查操作之外,其他的三种操作都有机会对抗反编译软件,本篇也只是对改操作里面的单位操作进行了fuzz分析,除单位之外,还可以进行两位、三位….的修改,组合的情况也就更多了

具体为什么反编译软件会出现报错,我们查看反编译软件的报错

apktool报错

image

jadx-gui报错

image

其实都是指向同一个问题

java.lang.ArrayIndexOutOfBoundsException

查看apktool源代码,具体位置在

1
2
3
4
5
6
7
8
9
10
11
12
private static int[] getUtf16(byte[] array, int offset) {
int val = ((array[offset + 1] & 0xFF) << 8 | array[offset] & 0xFF);

if ((val & 0x8000) != 0) {
int high = (array[offset + 3] & 0xFF) << 8;
int low = (array[offset + 2] & 0xFF);
int len_value = ((val & 0x7FFF) << 16) + (high + low);
return new int[] {4, len_value * 2};

}
return new int[] {2, val * 2};
}

错误在这一行

int val = ((array[offset + 1] & 0xFF) << 8 | array[offset] & 0xFF);

所以是传入的恶意偏移量导致了数组越界产生了异常并退出

参考链接

Android加固之后Apk重签名

Android加固之后Apk重签名

本文转自 ganzhijie 并作补充

Java:jarsigner java自带的jar签名,也就是我们Android打包的v1签名,签名方案只能v1。
Android:apksigner Android特有的签名,也就是打包的v2签名,支持多种签名方案(v1~v4)。

需要注意的是,因为 apksigner 是Google在 Android 7.0 Nougat 推出的,所以我们的版本号的选择需要 >= 24.0.3 ,否则只能选择 jarsigner 方式打v1包。

如果您使用的是 apksigner,则必须在为 APK 文件签名之前使用 zipalign。如果您在使用 apksigner 为APK 签名之后对 APK 做出了进一步更改,签名便会失效。
如果您使用的是 jarsigner,则必须在为 APK 文件签名之后使用 zipalign。

下面是检查apk是否对齐的方法,打开终端输入:
zipalign -c -v 4 apk路径
例如:
zipalign -c -v 4 /android/tools/jiagu/apk/jiagu.apk

接下来就是利用终端,实现apk对齐操作,在打开的终端输入:
zipalign -v 4 「需要对齐操作的apk地址」 「对齐之后生成的地址」
例如:
zipalign -v 4 /android/tools/jiagu/apk/jiagu.apk /android/tools/jiagu/apk/zipaligned.apk
出现 “Verification succcessful” 为对齐成功。

下面是检查是否签名的终端语句,在打开的终端输入:

apksigner verify -v 检查的apk路径
例如:
apksigner verify -v /android/tools/jiagu/apk/zipaligned.apk

接下来继续在终端输入「apksigner」重签名语句

apksigner sign -verbose –ks 「jks文件路径」 –v1-signing-enabled (「true/false」v1打包开启/关闭) –v2-signing-enabled (「true/false」v2打包开启/关闭) -ks-key-alias (jks别名 key-alias) –ks-pass pass: (jks密码,key store password) –key-pass pass:(key 密码,key password) –out 「生成的apk路径,重签名后的」 「对齐之后的apk路径」
例如:
apksigner sign -verbose –ks /android/tools/jiagu/apk/ks/abc.keystore –v1-signing-enabled true –v2-signing-enabled true –ks-key-alias abc.keystore –ks-pass pass:123456 –key-pass pass:123456 –out /android/tools/jiagu/apk/signed.apk /android/tools/jiagu/apk/zipaligned.apk
终端运行结果如下:出现 Signed 则为重签名成功。

总结:apksigner签名流程。

加固并下载 ===> 检查是否对齐 ===> 对齐「zipalign」 ===> 检查是否对齐 ===> 检查apk是否签名 ===> 「apksigner」重签名 ===> 是否重签名成功

灰产-如何破解请求验签(下)

灰产-如何破解请求验签(下)

image

写在前面

再来个前情提要:灰产-如何破解请求验签(上)

以下都是情节模拟,并非具体情况,请细加甄别

阿哈!Showmak……啊不,是新的 Sign 替换后不久就,两周左右就又被破解了呢——

“我们新的加签,可是从多个随机数池取值生成的 sign1,然后把sign1和多个参数混合在一起按时间戳不同取模排列生成的sign2,怎么可能……”

啊,虽然很想吐槽这反派一般被反击了感叹自己部署被打乱了的感觉,但是来不及为已经逝去的 sign2 感到悲伤了,接下来赶到战场的是……

是我这个苦力!

我打灰产?真的假的?要上吗?

image

总而言之

总而言之,在解释了旧 Sign 是怎么泄露的后,调查新 Sign 是怎么泄露的这个任务又又又落到我这里了= =,已经不想吐槽了

实现漏洞

“怎么实现的……额,实际上我们等于是接了一个生成库,你可以理解为类似SDK,里面有各种 Sign1 和 Sign2 的实现方式,什么移位啊取模啊都多久前的了,我都快忘了,你想看的话文档给你你可以看一下”

“有没有可能是这个实现的模块泄露了或者可以被调用啥的?”

“我觉得没有吧,感觉也有可能是 Hook 了,但就不知道是怎么弄的”

然后直到最后我除了在研发显示屏上看到了一眼实现文档,我再也没看到那实现方式一眼了

image

摸索开始了——

首先通过请求日志我发现,灰产用到的 sign1 有一批量重复的(说好的随机池随机取呢),但是生成的 sign2 是不重复的(sign1 只作为一个参数参与生成 sign2 的计算),我们并没有对 sign1 进行校验,所以导致这个 sign1 可以在请求中被替换,重复利用。

顺带一提的是,在其他应用里很早就出现这个情况了,不是我的话都还没发现……

image

定向Hook

sign1 的问题因为没有校验可以重复利用解决了,那 sign2 呢?

前端没有找到泄露 sign2 计算点,因为前端都可能没用新的 Sign;

应用端源码没看到 sign2 的计算方式,因为做了较好的混淆;

……

那选择就只有一个了!(折断拐杖)

对它使用 Hook 吧!

image

早在 MobSF移动安全测试框架|自动化静动态app分析框架 的使用中,我就接触了 Frida,why?因为一开始装不上,并且因为种种原因(MobSF 自带证书冲突等)执行动态分析的效果很差,虽然后来还是能试出来。

扯远了,回到现在,利用 Frida 对应用进行 Hook,我是在 Genymotion 上做的,当然,由于应用配置的关系,你可能需要先:

这里就要说了,为什么涉及到 Brida?

因为一开始是想用 Brida 直接 Hook 调用函数,传到 BurpSuite 直接修改请求,但是后来发现你根本不知道是哪个函数计算 sign2 ,别说改了,Hook 都不知道 Hook 谁,怎么办?

image

java.security.MessageDigest

还记得旧 Sign 最后的加密算法吗?SHA1-Hash

再看看新 Sign,尤其是 sign2 的样式,分明也是 SHA1!

嘛,灵光一现:不如利用 js 跟踪“java.security.MessageDigest”的调用,并输出加密入参与之后的值,js 参考:

image

入参值反解出来是一堆字节码,需要用 java 重新转一下回去:

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
public class exchange {
public static void main(String[] args) {
byte[] b = { 108, ……, 102,
105, ……, 105,
111, ……, 107,
45, ……, 61,
50, ……, 52,
32, ……, 117,
115, ……, 118,
105, ……, 110,
118, ……, 73,
70, ……, 78,
74, ……, 85,
85, ……, 108,
107, ……, 107,
45, ……, 110,
61, ……, 48,
67, ……, 45,
116, ……, 112,
61, ……, 104,
73, ……, 57 };
String s = new String(b);
System.out.println(s);
}
}



Output:
……data={"userId":"……"}……sign1=GORNJPBOGXJXZQRRKYKYZNGZXZSYOUUSCMCM……timestamp=1714030269543……

sign1&timestamp

ok,入参也出来了,现在只剩一个问题:sign1 在入参中的位置。

其实到这里直接去遍历都可以,但是我还是采集了几份 sign1 在 sign2 生成位置与 timestamp 的关系进行比对,然后发现比对了个寂寞——差一位数的时间戳里 sign1 的插入位置可以相同的 = =:

image

所以不整了,收工!

image

写在后面

下篇也写完了,感觉好像也就这样,后续把加固啥的弄上议程吧,不然好像还有些硬编码问题也是头疼,但还要验证一下效果。

沉默,然后……事已至此,先吃(pao)饭(lu)吧

image

参考引用

灰产-如何破解请求验签(上)

MobSF移动安全测试框架|自动化静动态app分析框架

Genymotion_A11_libhoudini

How to install Xposed/EdXposed/LSPosed + Magisk with Genymotion Desktop?

brida从0配置

安卓逆向 - Frida Hook(抓包实践)

Frida java层自吐加密算法

灰产-如何破解请求验签(上)

灰产-如何破解请求验签(上)

image

前情提要

先来个前情提要:灰产-如何对APK修改后重签

在写那篇文章的我,是否会想到我竟然还有有关“灰产”的下文来写?而且就标题来说,还有分上下两篇!所以这4个月不到的时间我又经历了什么呢?

image

写在前面

以下都是情节模拟,并非具体情况,请细加甄别

让我们把时间拨回到年后。

年后回来一阵子,接到业务风控同学的反馈,我们有用户发现自己的账户被盗了,自己购买了不是自己的商品(不产生金额)并对这个商品统一匿名评价……

我第一反应卧槽哥们我们应用不是做了端校验,不同设备必须验证码登录,怎么连手机一起丢了才有后面能说漏洞导致账密泄漏的事?

image

后面通过网关日志、应用日志、接口日志等排查,发现这个用户的登录凭证(token)有从境外服务器请求服务的情况,可以基本确认是 token 的泄漏;

再和用户沟通以及进一步的深入排查,我们发现 token 的泄漏源于用户被钓鱼,这里细节不表;被社工得到的 token 会先从 ip 1 被收集并转发给 ip 2 进行购买请求与评论……

这里涉及到一个问题,我们的请求在各端做了验签,Sign 参数会根据不同端走不同的计算方式,想要更改 token 并发起购买请求,不重新计算 Sign是不可能通过校验的,只有两种可能:

  • 我们的 Sign 计算方式被破解
  • 我们的 Sign 计算方式可被任意调用

总而言之

总而言之,这个任务又落到我这里了= =

会议讨论对齐的时候,研发一致认为是在 H5 端的 Sign 计算方式太简单了,如何计算已经被破解,应该换上现在端上更安全的 Sign 计算就可以了!

直接破解一个计算方法理论上是不太容易的,我更在意的是是否是哪个地方泄漏了 Sign 的计算方法,或者在什么地方的计算代码可以被调用,直接入参入值然后得到结果。

但这里就要提到另一个事情了,研发不告诉你!

他们只表述这个好像比较简单就能破,但问他们怎么破的,不知道;问他们在哪里可能泄漏了,不知道:

“大概是从前端 JS 泄漏的?你看我们这里都做了混淆了,你看是不是就是这个位置,sign 关键词都命中了……”“可是为啥我调试/下断点没反应啊?”

image

后来嘛,在把钓鱼的口子堵了一下后,换上“更安全”的 Sign 计算方式后,喜闻乐见的钓鱼的口子也被绕过了,“更安全”的 Sign 计算也被破解了……

然后呢?

遭殃的还是我好吧,本来他们都认为可行的解决办法没用的时候,我就不得不花时间和精力来看看不在我 OKR 上的问题了

image

泄漏发现

算研发很配合安全工作,虽然告诉了我一个错误的泄漏位置,但起码提醒了我可能是 JS 就已经泄露了,没必要一来就去逆向找源码。

开始的思路还是去利用前端给到的 JS 复用:Burp Suite作为代理,复用目标站点的js代码处理指定的内容

后来发现 JSFlash 必须要能够指定调用代码函数才行,前端 JS 混淆了,指向不出来,直接分析不了

然后我就在前端登录页 Source - js 找到了 Sign 计算点,并且下断点步进调试看到了所有 Sign 的计算入参:

image

image

它端验证

在前端 JS 调试的过程中能看到有一个 secret 参数是不会暴露在 request 里的,其他端的如何获得?

Jadx 逆向反编译一下应用端,以 SIGN 为关键词,在源代码中发现了硬编码在其他端使用的 secret,以及使用的 Sign 最后加签算法:

image

image

写在后面

上篇就到这里,写的好像很简单,解过程也没遇到什么麻烦,但是还得说和大伙配合的还是不好,我还是认为不管是锻炼或者检验个人能力也好,工作任务忙不想接手深入这个事情也好,如果是以把事情完成的结论来说,相互配合是很重要的。

以旧 Sign 为例,大家说的好像每个人都知道这个是怎么生成的,但就是没人和你说,告诉的还是错误的位置,即使真的可能知道是怎么生成的不知道是在哪里泄露的,那信息汇通还是更重要的。研发完全可以告诉你“我猜是在前端 JS 泄漏的,位置是在这里,他的调用是这样的……”给你演示出来,如果真的在前端直接 Debug 下断点走一遍,就会知道一开始的位置告诉错了,并且一下就把泄漏点找出来了。

不会浪费双方的时间的。

image

参考引用

灰产-如何对APK修改后重签

Burp Suite作为代理,复用目标站点的js代码处理指定的内容

安卓逆向 - Frida Hook(抓包实践)

安卓逆向 - 基础入门教程

本文转自小馒头yy 并作补充

一、引言

上篇文章:安卓逆向 - 基础入门教程_小馒头yy的博客-CSDN博客 介绍了Frida的安装、基本使用,今天我们来看看Frida常用Hook和基于Frida抓包实践。

二、Frida常用 Hook脚本

1、hook java.net.URL

1
2
3
4
5
6
7
8
function hook1() {
var URL = Java.use('java.net.URL');
URL.$init.overload('java.lang.String').implementation = function (a) {
console.log('加密前:' + a)
showStacks()
this.$init(a)
}
}

2、hook okhttp3 HttpUrl

1
2
3
4
5
6
7
8
9
10
function hookOkhttp3() {
var Builder = Java.use('okhttp3.Request$Builder');
Builder.url.overload('okhttp3.HttpUrl').implementation = function (a) {
console.log('a: ' + a)
var res = this.url(a);
showStacks()
console.log("res: " + res)
return res;
}
}

3、hook okhttp3 addHeader

1
2
3
4
5
6
7
8
9
10
11
function hook() {
var Builder = Java.use("okhttp3.Request$Builder");
Builder["addHeader"].implementation = function (str, str2) {
console.log("key: " + str)
console.log("val: " + str2)
showStacks()
var result = this["addHeader"](str, str2);
console.log("result: " + result);
return result;
};
}

4、打印堆栈

1
2
3
4
5
function showStacks() {
Java.perform(function () {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
});
}

5、hook Base64

1
2
3
4
5
6
7
8
9
function hookBase() {
// Base64
var Base64Class = Java.use("android.util.Base64");
Base64Class.encodeToString.overload("[B", "int").implementation = function (a, b) {
var rc = this.encodeToString(a, b);
console.log(">>> Base64 " + rc);
return rc;
}
}

6、hook HashMap

1
2
3
4
5
6
7
8
function hookMap() {
var Build = Java.use("java.util.HashMap");
Build["put"].implementation = function (key, val) {
console.log("key : " + key)
console.log("val : " + val)
return this.put(key, val)
}
}

三、某麦网抓包实践

本篇以某麦网帖子详情接口,演示如何基于Frida hook抓包

1、安装某麦网8...apk

2、搭建Frida hook 环境,注入 hook java.net.URL脚本

3、点进帖子详情打印出如下堆栈,我们可以根据打印出的信息,跟栈分析该接口的请求头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
https://acs.m.taobao.com/gw/mtop.damai.wireless.discovery.detail.get/1.4/?source=10101&version=6000168&type=originaljson&data=%7B%22contentId%22%3A%2211088650%22%2C%22appType%22%3A%221%22%2C%22source%22%3A%2210101%22%2C%22osType%22%3A%222%22%2C%22pageSize%22%3A%2230%22%2C%22pageIndex%22%3A%221%22%2C%22version%22%3A%226000168%22%2C%22channel_from%22%3A%22damai_market%22%7D&appType=1&osType=2&channel_from=damai_market
java.lang.Exception
at java.net.URL.<init>(Native Method)
at tb.yy0.m(Taobao:1)
at anet.channel.request.a.p(Taobao:2)
at anet.channel.session.TnetSpdySession.w(Taobao:18)
at anetwork.channel.unified.NetworkTask.sendRequest(Taobao:6)
at anetwork.channel.unified.NetworkTask.run(Taobao:44)
at anetwork.channel.unified.UnifiedRequestTask$a.proceed(Taobao:15)
at com.taobao.orange.sync.NetworkInterceptor.intercept(Taobao:30)
at anetwork.channel.unified.UnifiedRequestTask$a.proceed(Taobao:7)
at anetwork.channel.unified.UnifiedRequestTask$3.run(Taobao:2)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:428)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
at java.lang.Thread.run(Thread.java:761)

4、使用Jadx打开某麦网apk

从at tb.yy0.m(Taobao:1)跟栈分析

image

根据调用栈往上走,定位到如下位置,注意这行代码:

1
ALog.f("awcn.TnetSpdySession", "", aVar.n(), "request headers", aVar.g());

image

代码注释很清楚了 request headers,我们跟进 hook aVar.g() 这个方法

1
2
3
public Map<String, String> g() {
return Collections.unmodifiableMap(this.f);
}

hook之,对象输出可以使用 JSONObject转一下

1
var JSONObject = Java.use("com.alibaba.fastjson.JSONObject");
1
2
3
4
5
6
7
8
9
function hook6() {
var JSONObject = Java.use("com.alibaba.fastjson.JSONObject");
var a = Java.use("anet.channel.request.a");
a["g"].implementation = function () {
var result = this["g"]();
console.log("result : " + JSONObject.toJSON(result).toString());
return result;
};
}

打印出如下内容:

image

请求 URL、 请求方法这边都写得很清楚啦。

image

收工!

安卓逆向 - 基础入门教程

安卓逆向 - 基础入门教程

本文转自小馒头yy 并作补充

一、引言

1、我们在采集app数据时,有些字段是加密的,如某麦网x-mini-wua x-sgext x-sign x-umt x-utdid等参数,这时候我们需要去分析加密字段的生成。本篇将以采集的角度讲解入门安卓逆向需要掌握的技能、工具。

2、安卓(Android)是一种基于Linux内核操作系统,架构图(了解即可)

image

3、安卓应用程序使用JAVA语言编写(重要),作为一名安卓逆向人员我们必须掌握Java语言基础(基本语法、类、接口、面向对象、网络类库、加密解密等等)

4、安卓逆向是对已经打包好的APP进行反编译,分析源码了解应用逻辑的一门技术,这一部分我们需要学习静态分析,动态分析。各种反编译工具(推荐 jadx),Frida Hook。

二、静态分析,学习jadx反编译

1、安装jadx,github地址: Releases · skylot/jadx · GitHub,下载 jadx-1.4.7.zip 解压即可。

image

2、调整jadx最大内存,打开bin目录下的 jadx-gui.bat文件,搜索DEFAULT_JVM_OPTS。

将 -XX:MaxRAMPercentage=70.0 修改成 -Xmx16g

image

3、jadx使用,打开apk,jadx会自动反编译apk

image

全局搜索:点击左上角的导航 - 文本搜索,输入关键字,并且下方可以筛选检索的位置

image

根据关键字定位到关键代码后,我们就可以进行分析加密参数的生成啦。

三、动态分析,Frida 初使用

1、安装 adb (Android Debug Bridge),通过命令行使用adb,对安卓设备进行操作

2、下载模拟器,我使用的逍遥模拟器 (有Root好的真机更棒)

模拟器启动后,在命令行依次执行命令:

1
2
3
4
adb forward tcp:27043 tcp:27043
adb forward tcp:27042 tcp:27042
adb connect 127.0.0.1:21503
adb devices

再执行 adb shell,即可进入模拟器, #字符代表已 Root

image

3、安装Frida

本地直接使用 pip install frida 命令安装 (需有Python环境)

模拟器(手机)端下载对应的Frida-server: Releases · frida/frida · GitHub

低版本安卓推荐下载:12.8.13,高版本安卓直接下载最新版

模拟器是 x86架构,真机是 arm架构,请下载对应的 frida-server

4、开启Hook,动态分析应用运行时数据

  • 将frida-server传到手机 /data/local/tmp 目录,转到本地frida-server存放的目录,依次执行以下命令
1
2
3
4
5
adb push frida-server-12.8.13-android-x86 /data/local/tmp
adb shell
cd /data/local/tmp
chmod 777 frida-server-12.8.13-android-x86
./frida-server-12.8.13-android-x86
  • 编写 hook脚本,使用js方式启动,本例为 hook URL的构造方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function hook1(){
var URL = Java.use('java.net.URL');
URL.$init.overload('java.lang.String').implementation= function(a){
console.log('加密前:'+a)
console.log(' ')
this.$init(a)
}
}

function main(){
Java.perform(function(){
hook();
})
}


setImmediate(main);
  • 开始 hook,注意高版本 frida不需要带 –no-pause
1
frida -U -l dm_hook.js -f cn.damai --no-pause

其中 dm_hook.js是 hook脚本的文件名,cn.damai是apk的包名。

滑动应用,正常打印出 url信息。

灰产-如何对APK修改后重签

灰产-如何对APK修改后重签

image

写在前面

前段时间没有事做的时候,组织上仿佛看穿了我很咸,突然让我去研究下灰产包,让我分析用了什么技术&做出了什么改动。

image

总而言之

虽然入职就听说了公司应用之前有被灰产“薅羊毛”的问题,但真让我弄我也没专门弄过啊,而且这部分之前是交给风控组去做的,这么久了整不来靠我嘛?而且我也想吐槽这灰产包你们又是怎么弄来的,还有两份不同技术实现的……

image

签名篡改

如果想对apk内部内容进行修改,从而达到实现某些功能或规避某些审查的目的,就一定要对签名进行重签,这一点上只需要和正版签名比对一下便能发现。如何检测使用了这些篡改应用的用户等不在这次的任务范围内,篡改功能点内容也不展开讨论,此处仅讨论绕过审查重签的技术。

image

MT APP签名检查及绕过

对灰产包简单的逆向后,我在第一个包很快地就发现了一个KillApplication.java

image

很显然,做篡改的兄弟还不够细,或者根本没懂相应的原理性知识,留下了如此明显的痕迹,并且在URL参数直接暴露了篡改方式:

ApkSignatureKillerEx

image

算是让我直接回忆起之前用过的MT管理器,好像酷安就能下到,不过我已经很久没用过了,不知道MT现在是自带这种签名方式还是作为外部插件使用的。

而MT APP签名检查及绕过在Android逆向-获取APP签名 一文中有详细表述,在此不做赘述(懒)

NP APP签名检查及绕过

那我们再来看下一个,啊,还不用我找,MobSF都给我翻出来了:

image

你是?

image

顺着FuckSign.java2863678687@qq.com 这两个信息(痕迹也是太明显了),我找到了另一种篡改方式:

NP-Manager

image

直接获取下来也很容易就复现了篡改与绕过:

image

写在后面

首先啊,风控安全真是和什么都斗争不断,除开apk包篡改,也还有模拟GPS定位、虚拟用户接码手机号等等,还好不是我直接负责:

image

然后,之前文章提到过外网可能是MobSF官方开的一个在线检测的平台,直接暴露了非常多的检测应用报告,无独有偶,我在做这次灰产分析的时候,在搜索引擎寻找FuckSign.java,发现了国内一个仿MobSF的在线检测平台,从搜索引擎记录来看,市场上很多其他应用也不堪灰产侵扰:

image

我在发现这个检测平台的时候,它给检测项部分“需要开通VIP”解锁,该说是刻意而为导致规避了部分问题还是啥呢……总之,我在几周后重新访问的时候,他已经是崩溃状态,嘛,不好评价:

image

最后,要做灰产黑产,就要从原理性把痕迹去除或转化,不能留太明显的技术痕迹,虽然可以说这些最后找到的都是开源项目,是卖刀者不是作恶人,但淹死的多是会水的,这里分享一篇文章:灰产,赚点快钱?

image

参考引用

ApkSignatureKillerEx

Android逆向-获取APP签名

NP-Manager

Firebase Installations API 密钥硬编码

灰产,赚点快钱?

与 Firebase 安装服务器 API 通信时的 Android 错误

与 Firebase 安装服务器 API 通信时的 Android 错误

本文转自Segmentfault 并作补充

Questions

我在应用程序启动时收到一条错误消息,说明日志如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
W/Firebase-Installations: Error when communicating with the Firebase Installations server API. HTTP response: [400 Bad Request: {
"error": {
"code": 400,
"message": "API key not valid. Please pass a valid API key.",
"status": "INVALID_ARGUMENT",
"details": [
{
"@type": "type.googleapis.com/google.rpc.Help",
"links": [
{
"description": "Google developers console",
"url": "https://console.developers.google.com"
}
]
}
]
}
}
]
2020-04-27 12:42:34.621 22226-23596/in.co.androidapp.g7 E/Firebase-Installations: Firebase Installations can not communicate with Firebase server APIs due to invalid configuration. Please update your Firebase initialization process and set valid Firebase options (API key, Project ID, Application ID) when initializing Firebase.

大约一周前我收到一封电子邮件,我应该更新我的 google_services.json 文件,我已经更新了 4-5 次。没有改进。它已经工作了大约一年。自从我在应用程序中遇到此问题以来只有 2-3 天。

随后,Firebase Cloud Messaging 和其他 Firebase 服务无法正常工作。我没有进行编程初始化(即,使用 FirebaseOptions 对象提供这些值),只是默认初始化使用 FirebaseApp.initializeApp(this);

我试过 https://github.com/firebase/firebase-android-sdk/blob/master/firebase-installations/API_KEY_RESTRICTIONS.md

提前致谢。

原文由 Daksh Agrawal 发布,翻译遵循 CC BY-SA 4.0 许可协议

Answers

如果您的 API 密钥有问题,您可以在 Cloud Console 中创建一个新的 API 密钥:

  • 转到 谷歌云控制台
  • 选择相关项目(即您用于申请的项目)
  • 打开菜单并转到 APIs & ServicesCredentials
  • 在页面顶部点击 + CREATE CREDENTIALSAPI key
  • 用新创建的 API 密钥替换应用程序中的 API 密钥

如果您使用 google-services.json 来自 Firebase 控制台 的配置文件,您首先必须删除或限制当前 google-services.json 中使用的 API 密钥,以便使 Firebase 更新配置文件和使用新的 API 密钥。

  • 在您的 google-services.json 配置文件中识别 API 密钥。
  • 通过根据 Firebase Installations API 指标页面 检查 API 密钥的使用情况,确认 API 密钥正在创建错误请求。 API 密钥的列 Usage with this service 应显示大于 0 的数字。
  • 通过单击 bin 符号删除该 API 密钥,或通过单击铅笔符号将 Application restrictions 添加到该 API 密钥。 !!警告!! 不要删除应用程序现有安装所需的 API 密钥,以用于其他 Firebase 服务,例如 Firebase Auth 或 Realtime-Database。

等待几分钟,让 Google 服务器更新。下次下载 google-service.json 配置文件应该包含一个新的 API 密钥。

您可以使用以下 CURL 命令测试您的配置。你得到的 错误 是什么? (注意:如果您看到的是 JSON 数据,则说明您的配置成功)

测试您的配置是否适用于以下 CURL 命令:

1
2
3
4
5
 api_key=<YOUR_API_KEY>;
project_identifier=<YOUR_PROJECT_ID>;
app_id=<YOUR_FIREBASE_APP_ID_EXAMPLE_1:12345678:android:00000aaaaaaaa>;

curl -H "content-type: application/json" -d "{appId: '$app_id', sdkVersion: 't:1'}" https://firebaseinstallations.googleapis.com/v1/projects/$project_identifier/installations/?key=$api_key;

关于 API 密钥和 Firebase Installations API 的其他相关链接:

原文由 Andreas Rayo Kniep 发布,翻译遵循 CC BY-SA 4.0 许可协议