Category Archives: mobile security

WiFi万能钥匙蹭网原理详细剖析

注:该文最早受乌云编辑约稿,发表在http://drops.wooyun.org/papers/4976

wifi万能钥匙究竟有没有获取root之后偷偷上传密码?

本次测试版本号为3.2.3,首先通过可疑shell语句定位到疑问的问题代码:类名com.snda.wifilocating.f.ba

source_code_decomplied

source_code_decomplied

这段代码的作用是在有了root权限的情况下 将系统的wifi.conf拷贝出来到应用自己的目录,并赋予其全局可读写权限(其实这是个漏洞了…)。

对其做cross-ref查找引用之后可以发现,该函数主要在两个地方被直接调用。一个是com.snda.wifilocating.e.av:

savePrivateApsReq

savePrivateApsReq


这是一个api接口,主要功能是用于用户注册了之后备份自己的ap密码,同时在
cross-refs

cross-refs


WpaConfUploadActivity直接调用、GetBackupActivity中间接调用。第一个Activity在分析的版本中已经被从AndroidManifest中删除,而第二个Activity则是用户备份私有wifi时的对应的界面。这证实了备份的时候密码确实会被上传,而且从下文来看这个密码是完全可逆的。

不过在使用过程中,该应用并没有其他可疑的root行为操作。笔者打开了SuperSu的root执行监控,短暂的使用过程中也只发现了执行了上述的这一条命令。

supersu-monitor

Android系统Wifi连接API概述

Android系统通过WifiManager类来提供对Wifi的扫描、连接接口。应用在请求相应权限之后可以扫描、连接、断开无线等。在连接无线功能中,客户端基本上只要指定SSID,Pre-shared-key(即密码),就可以用代码的方式连接无线。连接一个WPA(2)无线典型代码如下,

wifiConfiguration.SSID = "\"" + networkSSID + "\"";
wifiConfiguration.preSharedKey = "\"" + networkPass + "\"";
wifiConfiguration.hiddenSSID = true;
wifiConfiguration.status = WifiConfiguration.Status.ENABLED;
wifiConfiguration.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
wifiConfiguration.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
wifiConfiguration.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
wifiConfiguration.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
wifiConfiguration.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
wifiConfiguration.allowedProtocols.set(WifiConfiguration.Protocol.RSN);
wifiConfiguration.allowedProtocols.set(WifiConfiguration.Protocol.WPA);
int res = wifiManager.addNetwork(wifiConfiguration);
Log.d(TAG, "### add Network returned " + res);

wifi万能钥匙是怎么连接上无线的,密码从哪里来?

这也是争议较大的地方,首先该应用肯定是有云端存储了很多密码,因为应用会引导用户备份自己的密码,但这些密码有没有被滥用我们在客户端就不得而知了。在2月底的这次测试中,笔者先私有备份了自己建立的测试无线(注意不是分享),然后使用另外一个手机安装该客户端测试,该客户端的API请求接口并没有返回这个测试的无线的密码。不过这也可能只是个例说明不了什么,还是建议各位自行测试,但注意测试前清除保存的无线并给测试无线设定一个弱密码以免真的泄露了自己的密码。

无线密码获取分析

回到正题,笔者通过代理拦截到了该应用获取wifi密码的请求。应用发送目标的ssid,mac信息向云端做查询,获取到的密码到本地之后并不是明文的,而是一个AES加密。首先为了证明其在本地最终还是会以明文出现,先取了个巧,没有去逆这个算法(虽然逆下也不会很困难),而是直接hook了系统添加无线的代码(回忆上文里密码就在NetworkConfiguration.preSharedKey里)。

部分HOOK代码:

Class wifimgr = XposedHelpers.findClass(
        "android.net.wifi.WifiManager",
        lpparam.classLoader);
XposedBridge.hookAllMethods(wifimgr, "addNetwork",
        new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param)
                    throws Throwable {
                WifiConfiguration configuration = (WifiConfiguration) param.args[0];
                if(configuration.preSharedKey != null)
                {
                    Log.e("FUCKFUCK", "psk: "+configuration.preSharedKey);
                }
            }
        });
XposedBridge.hookAllMethods(wifimgr, "updateNetwork",
        new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param)
                    throws Throwable {
                WifiConfiguration configuration = (WifiConfiguration) param.args[0];
                if(configuration.preSharedKey != null)
                {
                    Log.e("FUCKFUCK", "psk: "+configuration.preSharedKey);
                }
            }
        });
}

这是一个万能钥匙上传wifi ssid以及mac以请求密码的截图:

query_pwd_by_ssid

query_pwd_by_ssid

响应截图:

get_ret_pwd

get_ret_pwd

密码以AES可逆加密的形式通过pwd这个json key传递了回来。

同时,在其尝试通过这个密码连接目标无线的时候,本地hook模块也获取到了真实的明文密码:

hook_pwd_output

hook_pwd_output

个人备份分析

而个人备份模块,也就是直接会读取wifi.conf的模块,是通过findprivateap和saveprivateap这两个json api method进行,具体的http请求逻辑在com.snda.wifilocating.e.av中可以找到,这个类也基本上囊括了所有万能钥匙的api请求逻辑。

备份时的请求:把整个wifi.conf全部上传了上去。

upload_wpa_supplicant_conf

upload_wpa_supplicant_conf

而恢复备份时,只是将密码从云端拖了下来。

其他连接方式分析

除此之外,Wifi万能钥匙还自带了2000条的数据库记录在ap8.db中,记录了常见的弱密码。
weak-pwd-2
例如
weak-pwd-1

这些密码用在所谓的“深度连接”功能中,其实按代码逻辑来看就是一个wifi密码爆破,每次在字典中尝试10个密码。看下logcat就很明显。

I/wpa_supplicant( 884): wlan0: WPA: 4-Way Handshake failed - pre-shared key may be incorrect
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-TEMP-DISABLED id=1 ssid="aaaaaaaaa" auth_failures=2 duration=20
D/SupplicantStateTracker( 818): Failed to authenticate, disabling network 1
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-REENABLED id=1 ssid="aaaaaaaaa"
I/wpa_supplicant( 884): wlan0: Trying to associate with 5c:a4:8a:4d:09:a0 (SSID='aaaaaaaaa' freq=2412 MHz)
I/wpa_supplicant( 884): wlan0: Associated with 5c:a4:8a:4d:09:a0
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-DISCONNECTED bssid=5c:a4:8a:4d:09:a0 reason=23
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-TEMP-DISABLED id=1 ssid="aaaaaaaaa" auth_failures=1 duration=10
I/wpa_supplicant( 884): wlan0: WPA: 4-Way Handshake failed - pre-shared key may be incorrect
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-TEMP-DISABLED id=1 ssid="aaaaaaaaa" auth_failures=2 duration=20
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-REENABLED id=1 ssid="aaaaaaaaa"
I/wpa_supplicant( 884): wlan0: Trying to associate with 5e:aa:aa:aa:aa:aa (SSID='aaaaaaaaa' freq=2462 MHz)
I/wpa_supplicant( 884): wlan0: Associated with 5e:aa:aa:aa:aa:aa
D/dalvikvm(13893): GC_CONCURRENT freed 356K, 4% free 18620K/19220K, paused 9ms+2ms, total 29ms
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-DISCONNECTED bssid=5e:aa:aa:aa:aa:aa reason=23
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-TEMP-DISABLED id=1 ssid="aaaaaaaaa" auth_failures=1 duration=10
I/wpa_supplicant( 884): wlan0: WPA: 4-Way Handshake failed - pre-shared key may be incorrect
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-TEMP-DISABLED id=1 ssid="aaaaaaaaa" auth_failures=2 duration=20

Wifi密码加解密分析

当然真正去逆向加密代码也不是很困难,简单的搜寻即可得到解密代码:(部分直接从反编译的代码中抠出,风格未做修饰)

public class AESFun {
  String a =//略去;
  String b = //略去;
  String c = //略去;
  Cipher cipher;
  IvParameterSpec spec;
  SecretKeySpec secretKeySpec;
  void init() throws NoSuchAlgorithmException, NoSuchPaddingException {
        spec = new IvParameterSpec(b.getBytes());
        secretKeySpec = new SecretKeySpec(a.getBytes(), "AES");
        cipher = Cipher.getInstance("AES/CBC/NoPadding");
  }
  public final String b(String arg7) throws Exception {
    byte[] array_b1;
    byte[] array_b = null;
    int i = 2;
    String string = null;
    {
        try {
            this.cipher.init(2, secretKeySpec, spec);
            Cipher cipher = this.cipher;
            if(arg7 != null && arg7.length() >= i) {
                int i1 = arg7.length() / 2;
                array_b = new byte[i1];
                int i2;
                for(i2 = 0; i2 < i1; ++i2) {
                    String string1 = arg7.substring(i2 * 2, i2 * 2 + 2);
                    array_b[i2] = ((byte)Integer.parseInt(string1, 0x10));
                }
            }
            array_b1 = cipher.doFinal(array_b);
        }
        catch(Exception exception) {
            StringBuilder stringBuilder = new StringBuilder("[decrypt] ");
            string = exception.getMessage();
            StringBuilder stringBuilder1 = stringBuilder.append(string);
            string = stringBuilder1.toString();
            exception.printStackTrace();
            throw new Exception(string);
        }
        string = new String(array_b1);
    }
    return string;
}

将API请求中获取的16进制pwd字段代入解密程序,得到的结果是如下格式:[length][password][timestamp]的格式,如下图所示,中间就是目标无线明文密码。

decrypted_pwd

decrypted_pwd

此外接口请求中有一个sign字段是加签,事实上是把请求参数合并在一起与预置的key做了个md5,细节就不赘述了。这两个清楚了之后其实完全可以利用这个接口实现一个自己的Wifi钥匙了。

总结

此版本的WiFi万能钥匙不会主动把root之后手机保存的无线密码发向云端但在做备份操作(安装时默认勾选自动备份)时会发送,当有足够的用户使用该应用时,云端就拥有了一个庞大的WiFi数据库,查询WiFi的密码时,应用会发送目标的ssid,mac信息向云端做查询,获取到的密码到本地之后并不是明文的,而是一个AES加密,本地解密后连接目标WiFi。同时内置了常见的2000条WiFi弱口令,在云端没有该WiFi密码的时候,可以尝试爆破目标的密码。

[译]当EFBFBD来敲门 – 对Java中字符串与byte数组转换的观察及相关的安全隐患

当EFBFBD来敲门 – 对Java中字符串与byte数组转换对一个观察及相关的安全隐患

我(指原作者,下文同)正在进行一个激动人心的Android应用安全审计,发现了如此多的漏洞以至于我觉得对于开发来说全部推倒重写都比修掉发现的这么多漏洞省事。正在我洋洋得意的时候,忽然发现在hook的输出中有如下有些奇怪的加密key,喜悦和兴奋变成了疑惑和好奇:

Entryption Key:

EFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD

为什么这个加密密钥有如此多重复的’EFBFBD’字符串模式?难道是subustrate hook代码写错了,还是开发者在生成key的时候犯了错误,还是说这压根是打印错了并不是应用真正用来加密的密钥?

从源代码寻找原因

虽然我并没有这个应用的源代码,但不是可以反编译的嘛。通过包括动态hook和静态阅读混淆过的源代码在内的一系列方法和手段,我们能确认说hook代码并没有错,输出也就是我们想要的加密key. 那么下一步就是需要分析这个奇怪的key的生成方式,隐患也就在这里隐藏着。

出于简便起见,我们编写了一个简单的样例应用来复现这个问题。代码如下:

public static String genKey(){
PBEKeySpec localPBEKeySpec = new PBEKeySpec(
    getPassword().toCharArray(), getSalt(), 20000, 256);
try {
    byte[] theKey =
        SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
            .generateSecret(localPBEKeySpec).getEncoded();
    return new String(theKey);
} catch (InvalidKeySpecException e) {
    e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
}
return null;
}

你发现问题在哪里了吗?没有的话也不要担心,我们下面会来具体进行分析。

大体上来说,这个函数使用用户的密码派生出一个加密密钥。这个加密密钥用来加密保护所有保存在设备中的该应用私有信息资源。首先这个应用将用户的密码,盐值,迭代次数(这里是20000轮)和想要得到的密钥长度(256bits)一起作为参数创建PBEKeySpec密钥生成器,随后用它生成一个加密密钥并存储到byte数组中。到这里来说一切都还是正常的,但是下一句看似无辜的将byte array转换成String的语句就是真正的罪魁祸首。

我们来在这段代码中插入bytesToHex这样十六进制输出语句,以获得key十六进制的输出并进行前后对比

public static String genKey(){
PBEKeySpec localPBEKeySpec = new PBEKeySpec(
    getPassword().toCharArray(), getSalt(), 20000, 256);
try {
    byte[] theKey =
        SecretKeyFactory.getInstance(“PBKDF2WithHmacSHA1”)
            .generateSecret(localPBEKeySpec).getEncoded();
    Log.i(_TAG, “Key (Before Casting): ” +
        bytesToHex( theKey ) );
    return new String(theKey);
}
..snip..
}
public static void test() {
String theKey = genKey();
Log.i(_TAG, “Key (After Casting): ” +
    bytesToHex(theKey.getBytes()));
}

得到的输出是:转换前

B7B0F88D603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3

转换后

EFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD

Android中byte数组到字符串的转换

目前来说我们已经能够确定,这个奇怪的EFBFBD是因为bytearray到string的转换引入的,那EFBFBD究竟是个什么意思?(译者注:其实就是�符号)

根据Android文档,”Android平台默认的编码是UTF-8(注意这和一些旧的编码基于用户地区的系统相反).” 在UTF-8中,’EFBFBD’这个十六进制字符串代表替换字符(replacement character).简单来说,一个替换字符的作用是代替一个未知或无法表示的传入字符(译者注:Unicode字符单字节是0x00-0x7F的范围)。这意味着转换后的String中,每一个EFBFBD序列都对应着原始字符串中的一个无法表示的byte(或者说两个hex position)。为了证实这个想法,我们将转换后的字符串中的EFBFBD做一个替换:

B7B0F88D603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3
!!******603466**7B**6C24E2B2AA576A******0C6B********76**********

(注:”!!”是Android在转换时没有包括第一个byte,为了对齐而加上) 这个对比直观地展示了非法的Unicode byte都被星号所代替,也就是EFBFBD的位置。

其他系统中byte数组到字符串的转换

在观察了Android系统中这个并不寻常的行为之后,我们来看看其他平台上的行为。首先看看Windows。

这里要注意的是我们加了一句获取charset的代码,因为根据Java文档:”Java程序中默认字符集在JVM启动的时候设置,一般由操作系统的区域和字符集决定”。

public static String genKey(){
PBEKeySpec localPBEKeySpec = new PBEKeySpec(
    getPassword().toCharArray(), getSalt(), 20000, 256);
try {
    byte[] theKey =
        SecretKeyFactory.getInstance(“PBKDF2WithHmacSHA1”)
            .generateSecret(localPBEKeySpec).getEncoded();
    System.out.println(“Key (Before Casting): ” +
    bytesToHex( theKey ) );
    return new String(theKey);
 }
 ..snip..
 }
 return null;
 }
public static void main(String[] args) {
System.out.println(“Default Charset: ” +
    Charset.defaultCharset().name());
String theKey = genKey();
System.out.println(“Key (After Casting): ” +
bytesToHex(theKey.getBytes()));
}

在Eclipse, Windows 8.1(x64)上运行之后,我们得到了如下的输出:

`\ Default Charset:

windows-1252

Key (Before Casting):

B7B0F88D603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3

Key (After Casting):

B7B0F83F603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3 `\

虽然转换前后key看起来基本相同,第四个字节却略有区别。原始字符串中是8D,转换之后变成了3F。这是因为在这次测试中,目标操作系统的字符集是Windows-1252/CP-1252,根据MSDN文档,0x81, 0x8D, 0x8F, 0x90和0x9D是未定义并被Windows保留的。因此String转换发生之后,8D字符就被替换为了3F字符,也就是’?’——问号

其他操作系统结果列举

转换前的key(这个对于每个平台当然都是相同的)

B7B0F88D603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3

Android 4.3 Key (转换后):

EFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD

Windows 8.1 (转换后):

B7B0F83F603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3

Ubuntu 12.04 (转换后):

EFBFBDEFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD

Mac OS X 10.10 (转换后):

EFBFBDEFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD

总结

本文分析了加密代码使用中常出现的一种错误转换问题,并指出了这种安全隐患的危害:会导致生成的密钥中出现大量的重复内容,增加了被爆破的风险。譬如说本文中提到的这个测试应用,它使用用户的密码生成一个加密密钥,但因为这个漏洞导致了密钥出现重复单元,降低了信息熵,也就是被爆破的难度。

参考

原文: http://blog.gdssecurity.com/labs/2015/2/18/when-efbfbd-and-friends-come-knocking-observations-of-byte-a.html

  • http://developer.android.com/reference/java/nio/charset/Charset.html
  • http://www.fileformat.info/info/unicode/char/0fffd/index.htm
  • http://docs.oracle.com/javase/7/docs/api/java/nio/charset/Charset.html#defaultCharset()
  • http://en.wikipedia.org/wiki/Windows-1252
  • http://msdn.microsoft.com/en-us/goglobal/cc305145.aspx
  • http://en.wikipedia.org/wiki/UTF-8

Denial of App -PackageManager占位拒绝安装漏洞-Google Bug 13416059 分析:

Denial of App – Google Bug 13416059 分析:
Author: hqdvista,转载注明出处。该文也已发布在drops.wooyun.org.
Soot作者Eric Bodden所在的实验室, Secure Software Engineering最近宣布他们将在SPSM’14上讲述名为Denial-of-App-Attack的Android系统漏洞,影响4.4.3之前的机型,并给出了poc和对应的google commit id. 这个在googlecode上对应的链接是https://code.google.com/p/android/issues/detail?id=65790。
POC:https://github.com/secure-software-engineering/denial-of-app-attack
该问题可以导致攻击者可以指定应用使其无法安装在手机上,除非有root权限或者factory reset手机。可以被木马用来占位拒绝杀毒软件的安装,或者占位拒绝竞品安装。下面是根据commit diff和poc给出的漏洞具体分析。
###问题现象:
下载安装这个POC,可以看到其实就是指定一个packagename,例如com.taobao.taobao,然后生成了一个malformed的APK并执行安装,由于该APK的dex是非法的,安装的时候会报告INSTALL_FAILED_DEXOPT并安装失败。但如果随后安装真正的com.taobao.taobao时,即使指定了重新安装选项(pm install -r),却会报INSTALL_FAILED_UID_CHANGED,导致后续安装失败,而在被占位的手机上已安装应用中却找不到com.taobao.taobao,自然也无法清除掉占位的幽灵,造成真正的淘宝应用完全无法安装,推而广之可以用在360等杀毒软件上。
正常应用无法安装
正常应用无法安装
POC
POC
###问题本质:
Google的diff对此问题的描述是:

We&#039;d otherwise leave the data dirs & native libraries lying around. This will leave the app permanently broken because the next install of the app will fail with INSTALL_FAILED_UID_CHANGED. Also remove an unnecessary instance variable. Cherry-pick from master Bug 13416059

通过观察可以发现,第一次安装(所谓“占位”)结束的时候,在/data/data/目录下已经有了com.taobao.taobao目录并分配了一个uid,例如u70(10070),但第二次安装的时候,PackageManager却出现了UID_CHANGED的error,而没有复用u70,这是为什么?
uid
INSTALL_FAILED_DEXOPT和UID_CHANGED是在如下代码块中:

3622    private PackageParser.Package scanPackageLI(PackageParser.Package pkg,
3623            int parseFlags, int scanMode, long currentTime, UserHandle user) {
//....
4141        if ((scanMode&SCAN_NO_DEX) == 0) {
4142            if (performDexOptLI(pkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0)
4143                    == DEX_OPT_FAILED) {
4144                mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT;
4145                return null;
4146            }
4147        }

scanPackageLI函数流程大概如下:

/**/
//检查是否系统应用
/**/
//检查Package是否重复,否则抛出PackageManager.INSTALL_FAILED_DUPLICATE_PACKAGE
  // Initialize package source and resource directories
3686        File destCodeFile = new File(pkg.applicationInfo.sourceDir);
3687        File destResourceFile = new File(pkg.applicationInfo.publicSourceDir);
//...
 // Just create the setting, don&#039;t add it yet. For already existing packages
3812            // the PkgSetting exists already and doesn&#039;t have to be created.
3813            pkgSetting = mSettings.getPackageLPw(pkg, origPackage, realName, suid, destCodeFile,
3814                    destResourceFile, pkg.applicationInfo.nativeLibraryDir,
3815                    pkg.applicationInfo.flags, user, false);
//在这之后uid已经被指定了
/**/
//检查签名
//检查Provider权限
//开始创建目录
   final long scanFileTime = scanFile.lastModified();
3926        final boolean forceDex = (scanMode&SCAN_FORCE_DEX) != 0;
3927        pkg.applicationInfo.processName = fixProcessName(
3928                pkg.applicationInfo.packageName,
3929                pkg.applicationInfo.processName,
3930                pkg.applicationInfo.uid);
3931
3932        File dataPath;
3933        if (mPlatformPackage == pkg) {
//omit
3937        } else {
3938            // This is a normal package, need to make its data directory.
3939            dataPath = getDataPathForPackage(pkg.packageName, 0);
3940
3941            boolean uidError = false;
3942
3943            if (dataPath.exists()) {
3944                int currentUid = 0;
3945                try {
3946                    StructStat stat = Libcore.os.stat(dataPath.getPath());
3947                    currentUid = stat.st_uid;
3948                } catch (ErrnoException e) {
3949                    Slog.e(TAG, "Couldn&#039;t stat path " + dataPath.getPath(), e);
3950                }
3951
3952                // If we have mismatched owners for the data path, we have a problem.
3953                if (currentUid != pkg.applicationInfo.uid) {
3954                    boolean recovered = false;
3955                    if (currentUid == 0) {
3956                     //omit...
3969                    }
3970                    if (!recovered && ((parseFlags&PackageParser.PARSE_IS_SYSTEM) != 0
3971                            || (scanMode&SCAN_BOOTING) != 0)) {
3972                        // If this is a system app, we can at least delete its
3973                        // current data so the application will still work.
3974                        //omit...
4001                    } else if (!recovered) {
4002                        // If we allow this install to proceed, we will be broken.
4003                        // Abort, abort!
4004                        mLastScanError = PackageManager.INSTALL_FAILED_UID_CHANGED;
4005                        return null;
4006                    }
                 } else {//目录不存在,新建立
4029                if (DEBUG_PACKAGE_SCANNING) {
4030                    if ((parseFlags & PackageParser.PARSE_CHATTY) != 0)
4031                        Log.v(TAG, "Want this data dir: " + dataPath);
4032                }
4033                //invoke installer to do the actual installation
4034                int ret = createDataDirsLI(pkgName, pkg.applicationInfo.uid);//建立目录
4035                if (ret < 0) {
4036                    // Error from installer
4037                    mLastScanError = PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
4038                    return null;
4039                }
4040
4041                if (dataPath.exists()) {
4042                    pkg.applicationInfo.dataDir = dataPath.getPath();
4043                } else {
4044                    Slog.w(TAG, "Unable to create data directory: " + dataPath);
4045                    pkg.applicationInfo.dataDir = null;
4046                }
4047            }
//omit...
//拷贝nativeLibrary
//omit...
//进行DexOpt
4141        if ((scanMode&SCAN_NO_DEX) == 0) {
4142            if (performDexOptLI(pkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0)
4143                    == DEX_OPT_FAILED) {
4144                mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT;
4145                return null;
4146            }
4147        }

那么漏洞的原理就很清楚了,第一次占位安装时,故意让PMS在数据目录已分配uid并写入了/data/data/下之后走到dexopt时使其报错,导致安装异常终止,此时已放置的数据目录却没有被清除掉。第二次安装的时候package被分配了新的的uid,但此时已有同名却不同uid的数据目录存在,导致uid_changed错误,安装失败。
为什么第二次安装的时候就会被分配不同的uid?关键在于 mSettings.getPackageLPw,辗转ref到/frameworks/base/services/java/com/android/server/pm/Settings.java

private PackageSetting getPackageLPw(String name, PackageSetting origPackage,
359            String realName, SharedUserSetting sharedUser, File codePath, File resourcePath,
360            String nativeLibraryPathString, int vc, int pkgFlags,
361            UserHandle installUser, boolean add, boolean allowInstall) {
//omit...
    } else {
423                p = new PackageSetting(name, realName, codePath, resourcePath,
424                        nativeLibraryPathString, vc, pkgFlags);
425                p.setTimeStamp(codePath.lastModified());
426                p.sharedUser = sharedUser;
427                // If this is not a system app, it starts out stopped.
428                if ((pkgFlags&ApplicationInfo.FLAG_SYSTEM) == 0) {
429                    if (DEBUG_STOPPED) {
430                        RuntimeException e = new RuntimeException("here");
431                        e.fillInStackTrace();
432                        Slog.i(PackageManagerService.TAG, "Stopping package " + name, e);
433                    }
434                    List<UserInfo> users = getAllUsers();
435                    if (users != null && allowInstall) {
436                        for (UserInfo user : users) {
437                            // By default we consider this app to be installed
438                            // for the user if no user has been specified (which
439                            // means to leave it at its original value, and the
440                            // original default value is true), or we are being
441                            // asked to install for all users, or this is the
442                            // user we are installing for.
443                            final boolean installed = installUser == null
444                                    || installUser.getIdentifier() == UserHandle.USER_ALL
445                                    || installUser.getIdentifier() == user.id;
446                            p.setUserState(user.id, COMPONENT_ENABLED_STATE_DEFAULT,
447                                    installed,
448                                    true, // stopped,
449                                    true, // notLaunched
450                                    null, null);
451                            writePackageRestrictionsLPr(user.id);
452                        }
453                    }
454                }
455                if (sharedUser != null) {
456                    p.appId = sharedUser.userId;
457                } else {
458                    // Clone the setting here for disabled system packages
459                    PackageSetting dis = mDisabledSysPackages.get(name);
460                    if (dis != null) {
//omit..
484                    } else {
485                        // Assign new user id
486                        p.appId = newUserIdLPw(p);//关键点
487                    }
488                }

继续查看newUserIdLPw

private int newUserIdLPw(Object obj) {
2360        // Let&#039;s be stupidly inefficient for now...
2361        final int N = mUserIds.size();
2362        for (int i = 0; i < N; i++) {
2363            if (mUserIds.get(i) == null) {//检查空位
2364                mUserIds.set(i, obj);
2365                return Process.FIRST_APPLICATION_UID + i;
2366            }
2367        }
2368
2369        // None left?
2370        if (N > (Process.LAST_APPLICATION_UID-Process.FIRST_APPLICATION_UID)) {
2371            return -1;
2372        }
2373
2374        mUserIds.add(obj);
2375        return Process.FIRST_APPLICATION_UID + N;
2376    }

mUserIds是一个PackageSettings的数组状结构,维护了当前的userid,并在安装时遍历进行分配。在第一次恶意的占位安装中,mUserIds这个array状结构已经被添加了一个PackageSettings进去,形成类似于[PackageSetting{(10001, bla)},…,PackageSetting{(10070, com.taobao.taobao)}]的结构,但在dexopt failed的时候最末尾一项没有被移除。随后再安装时,newUserIdLPw会遍历mUserIds,发现没有空位,就会在末尾重新添加一个,形成[PackageSetting{(10001, bla)},…,PackageSetting{(10070, com.taobao.taobao)},PackageSetting{(10071, com.taobao.taobao)}]的结构,导致两次安装分配的UID不同,触发INSTALL_FAILED_UID_CHANGED。
但值得注意的是,这时候mUserIds并没有被固化在packages.xml和packages.list中。
###进一步思考:
那么这样肯定会想到,如果杀掉system_server(软重启),让其重新扫描并建立mUserIds数组不就能修复这个问题了?
理论上来说,如果在重启前没有安装过其他应用的话,那么这还真是可行的。因为重启后重新建立的uid数组是[(10001, bla),…,(10069, haha)],那么重新安装的com.taobao.taobao刚好能占到10070的位置,皆大欢喜。
但如果在重启后又安装了其他应用,那么其就会占掉10070的位置,导致taobao再安装的时候以10071及之后的uid就拿不回原来应该属于它的/data/data/com.taobao.taobao了… what a pity.
以上在stock rom(Genymotion, SDK)和小米2、Nexus等上验证通过。
所以现在看来,原作者说只有root或者reset才能清除这个问题的说法似乎不准确,至少从给出的poc和google的diff来看实验结果某些情况下重启就能fix。总体来说,这是一个比较好玩的trick类漏洞,而且从issuelink来看,应该还有一些其他类型的同样效果的漏洞存在。
###Google对此的修复:
Google的diff主要是添加了SCAN_DELETE_DATA_ON_FAILURES的flag,在设置了该flag的时候安装失败时会删除遗留掉的文件。

@@ -4644,6 +4643,10 @@
         if ((scanMode&SCAN_NO_DEX) == 0) {
             if (performDexOptLI(pkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0, false)
                     == DEX_OPT_FAILED) {
+                if ((scanMode & SCAN_DELETE_DATA_ON_FAILURES) != 0) {
+                    removeDataDirsLI(pkg.packageName);
+                }
+
                 mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT;
                 return null;
             }
@@ -4721,6 +4724,10 @@
                     PackageParser.Package clientPkg = clientLibPkgs.get(i);
                     if (performDexOptLI(clientPkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0, false)
                             == DEX_OPT_FAILED) {
+                        if ((scanMode & SCAN_DELETE_DATA_ON_FAILURES) != 0) {
+                            removeDataDirsLI(pkg.packageName);
+                        }
+
                         mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT;
                         return null;
                     }

###如何fix某个占位攻击:
root下删除该数据目录即可,非root。。。那只能reset了。

一种Android沙盒中监控root命令执行的方法 (Enable command logging in Android)

author: hqdvista a.k.a flanker017
原创,转载请注明出处

0x01 Root下命令记录的情况

在Android应用中,有各种各样的应用都会去执行命令,很多灰色应用则是调用su去执行一些见不得人的勾当。一般来说执行root命令在framework层会这么做:

public static String execSuCommand(String cmd) throws IOException
    {
        Process process = Runtime.getRuntime().exec("su");
        DataOutputStream os = new DataOutputStream(process.getOutputStream());
        os.writeBytes(cmd+"n");
        os.flush();
        os.writeBytes("exitn");
        os.flush();
        BufferedReader reader = new BufferedReader(new InputStreamReader(
                process.getInputStream()));
        int read;
        char[] buffer = new char[4096];
        StringBuffer output = new StringBuffer();
        while ((read = reader.read(buffer)) > 0) {
            output.append(buffer, 0, read);
        }
        reader.close();
        os.close();
        return output.toString();
    }

当然,在native层直接调用su也可以。那沙盒监控中的需求就来了:如何监控到app执行了什么样的shell命令?
Continue reading

ACTF-misc300官方writeup

author: hqdvista a.k.a flanker017

0x01 数据包分析

将数据包 (链接:http://pan.baidu.com/s/1ntrzThB 密码:cbf2)下载下来,在wireshark中打开,看一下statistics和conversations,会看到一大坨http和personal-agent(5555)端口。http看过一遍,基本都是新浪新闻、google搜索ACTF这种,似乎没有什么有价值信息。image(X里的wireshark太难看了)
那么5555端口会是什么?大概follow stream一下, image 玩过android的人应该会意识到这是adb的协议。从数据流大小来看,普通的adb shell命令很难会产生这么多数据,那么要么是adb pull从设备中拖取了什么信息,要么是adb push了什么东西。
再往下翻: image,就会发现有意思的东西,安装流量,也就是说流量里是一个完整的安装APK的过程!
Continue reading

[Revert from backup]对Android最新fakesms漏洞的分析

注:原文发表于 2012-11-08并同时发表于freebuf,后来在博客地震中消失了,现在的是根据freebuf上的恢复而来的。
近期Android爆出SMS smishing vuln, 首先来源于http://www.csc.ncsu.edu/faculty/jiang/smishing.html, 然后github上给出了poc,具体来说是任意一个app没有write_sms权限下可以伪造任意发件人的任意短信。
20121108123649823
Continue reading