这里主要是看书的笔记。从基础开始。不断记录。直到啃完这本书(android软件安全权威指南:丰生强)

常见的android文件格式(2)

AndroidManifest.xml

存放了apk的大量配置信息,包括软件名称、图标、主题、包名、组件配置。

所有配置都属于manifest标签,与程序配置相关的属于android标签。

android:allowBackup=true允许系统在进行备份操作时,备份程序的应用数据,比如在终端执行adb backup命令。对安全敏感的情况要设置为false

android:supportsRtl=true让apk支持rtl(right-to-left)视图。targetSdkVersion必须在17及以上。

AXML文件格式

AndroidManifest.xml在编译成apk前是明文的。编译成apk的时候,会把这个文件编译成二进制格式的文件。解压了再打开这个文件,就是乱码的。这个文件就是AXML格式。

AXML文件修改

有些加固厂商利用android系统解析AXML的漏洞,在编译APK时构造畸形的AXML。导致apktool之类的工具不能正常工作,就需要修改AXML。有些现成的工具修复这种情况。AmBinaryEditor、AndroidManifestFix

resources.arsc

包含不同语言环境中res目录下所有资源的类型、名称与id所对应的信息。和AXML差不多,这是一个ARSC文件格式

ARSC文件的修改

和AXML一样,也是有些特殊的ARSC可以正常被系统加载,又能组织apktool之类的反编译。也有一些修改是为了汉化软件。

META-INF目录

存储apk签名有关的信息。

META-INF/CERT.RSA

存放了apk的开发者证书与签名信息。通过这个文件识别开发者身份以及判断apk是否被修改。可以使用openssl来解码查看证书内容。

openssl pkcs7 -inform DER -in CERT.RSA -noout -print_certs -text

输出结果如下

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
Certificate:
Data:
Version: 1 (0x0)
Serial Number: 1 (0x1)
Signature Algorithm: sha1WithRSAEncryption
Issuer: CN=Android Debug, O=Android, C=US
Validity
Not Before: Aug 12 14:05:31 2020 GMT
Not After : Aug 5 14:05:31 2050 GMT
Subject: CN=Android Debug, O=Android, C=US
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:9d:11:0b:8d:94:5f:28:34:1f:84:9a:1b:6a:1f:
4b:1d:c6:19:59:94:6d:1d:33:b3:3d:9f:16:7d:73:
ca:40:62:89:d7:39:75:15:67:c8:07:54:63:d0:42:
fb:a4:c6:a2:b4:41:b5:19:6f:7c:76:eb:ec:51:3b:
d1:fc:0c:f3:eb:db:4d:d8:ec:3c:6a:eb:46:3c:d2:
e6:47:fe:8f:f3:8a:07:67:5c:09:9f:de:00:97:60:
a7:7b:2b:f0:10:17:81:ec:e0:f7:a7:5a:55:79:89:
ed:6b:ba:ea:f8:0d:d6:c2:fe:6c:fc:67:f5:36:97:
8e:24:cd:97:41:7b:df:16:0d:63:b8:d6:ab:2b:fe:
6a:1e:94:bc:41:44:f4:de:26:c3:44:d0:c2:6e:79:
37:a1:3f:a6:a1:57:42:4f:5c:3e:e3:4b:ca:0b:eb:
ea:19:dd:6d:fe:c3:34:57:3d:a1:b7:38:44:5b:ca:
09:d9:da:70:3a:54:fb:4b:8d:c8:73:73:f7:38:0f:
6f:5d:5b:45:f9:0d:88:2d:2d:6e:2b:a5:c9:2a:29:
4a:8c:d0:87:74:47:4e:69:18:f8:93:0a:6d:a7:9b:
22:3d:0f:38:fb:8c:02:e9:1b:73:97:80:6d:06:31:
4f:82:75:dc:0a:76:30:aa:bb:cd:40:3d:3b:56:a6:
0d:77
Exponent: 65537 (0x10001)
Signature Algorithm: sha1WithRSAEncryption
90:52:91:03:0b:de:9b:69:da:47:7a:5d:c8:3f:31:e1:3e:6d:
55:54:e6:29:25:2a:14:25:00:27:21:d1:0d:70:56:ac:3a:bf:
17:95:c9:bd:a4:83:2a:ee:5e:c9:a6:ef:4c:f0:60:35:86:b3:
ee:86:9e:9a:94:7b:74:1f:99:86:65:23:a3:13:50:7f:41:ed:
53:72:7f:83:8b:6d:40:ca:54:c8:25:44:75:b3:49:c0:61:cd:
4f:11:28:dc:cb:65:76:ce:03:fa:c3:d9:1b:f6:67:af:34:9f:
25:ab:2f:94:d3:24:ae:be:50:97:76:af:d1:e5:8a:5d:25:4f:
8a:17:75:de:0f:8d:71:53:38:ed:90:e1:0d:25:20:07:6b:2d:
2d:37:18:a0:82:e1:54:71:63:7f:97:03:2a:2e:54:1e:d0:53:
55:85:83:f0:e8:14:46:9c:7f:50:6a:a8:ad:73:17:94:ac:4e:
8c:8b:56:c0:95:41:47:50:23:36:65:d1:c4:8c:53:85:8f:15:
8f:fe:94:13:b5:71:74:7e:55:30:78:20:42:50:f7:44:10:77:
87:92:2a:2a:2d:c8:f1:e8:24:1e:7f:4a:3c:55:f2:87:89:29:
98:c8:b8:79:37:41:c8:7c:35:c5:8f:1d:5c:15:e3:36:2e:83:
59:49:db:69

里面很多项都是用来鉴别apk是否被修改了。

META-INF/MANIFEST.MF

签名的清单文件,是个文本文件。大致的内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Manifest-Version: 1.0
Built-By: Generated-by-ADT
Created-By: Android Gradle 3.5.2

Name: AndroidManifest.xml
SHA-256-Digest: tQz8omUBWakxQm32m7lgSSAFdbP68612Iq4enf/gmxw=

Name: META-INF/androidx.appcompat_appcompat.version
SHA-256-Digest: n9KGQtOsoZHlx/wjg8/W+rsqrIdD8Cnau4mJrFhOMbw=

Name: META-INF/androidx.arch.core_core-runtime.version
SHA-256-Digest: wo/MpTY3vIjhJK8XJd8Ty5jGne3v1i+zzb4c22t2BiQ=

Name: META-INF/androidx.asynclayoutinflater_asynclayoutinflater.version
SHA-256-Digest: WYVJhIUxBN9cNT4vaBoV/HkkdC+aLkaMKa8kjc5FzgM=

例如我们校验一下AndroidManifest.xml的值

openssl sha256 AndroidManifest.xml 输出结果如下

1
SHA256(AndroidManifest.xml)= b50cfca2650159a931426df69bb96049200575b3faf3ad7622ae1e9dffe09b1c

然后我们把这个结果base64编码一下,输出结果如下,

1
tQz8omUBWakxQm32m7lgSSAFdbP68612Iq4enf/gmxw=

发现和清单里面记录的一样。如果我们修改了这个文件,和清单里面的结果就不一样了。这里保证进行apk签名验证时所有文件均未被修改

META-INF/CERT.SF

签名信息文件,也是文本文件。内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA-256-Digest-Manifest: CsbfwGGBNUMOUJt4R7LYm+LGmb72vPx3Knonej3SUY0=
X-Android-APK-Signed: 2

Name: AndroidManifest.xml
SHA-256-Digest: gx84XauTSsCx8N18oEcdgiOHMpjhObi7ywdIEy3OaZU=

Name: META-INF/androidx.appcompat_appcompat.version
SHA-256-Digest: ABbgKP0s08CVeuJ5ZMlIZx/AvJtb1QhNA0ffeXfCaHk=

Name: META-INF/androidx.arch.core_core-runtime.version
SHA-256-Digest: PjygIQMN5T6nIKT/hi5PFaxVcEB+W20fr4f0g2n7jrg=

和上面的文件非常类似,但是多了两个属性

SHA-256-Digest-Manifest:这个对应的就是对MANIFEST.MF这个文件的校验。

X-Android-APK-Signed:使用签名的版本,值为2时表示使用新版APK Signature Scheme v2进行签名。android studio在新版本的sdk构建工具中增加了签名工具apksigner,同时支持旧版的v1签名和android7.0引入的v2签名。

继续看这个文件的AndroidManifest.xml的值,发现和上面清单的值不一样的。因为这里实际是对上面清单每一项的验证。

Name: AndroidManifest.xml\r\nSHA-256-Digest: tQz8omUBWakxQm32m7lgSSAFdbP68612Iq4enf/gmxw=\r\n\r\n 对这个串进行sha256计算。

831f385dab934ac0b1f0dd7ca0471d8223873298e139b8bbcb0748132dce6995 上面的值sha256的结果。然后继续进行base64编码

gx84XauTSsCx8N18oEcdgiOHMpjhObi7ywdIEy3OaZU= 现在得到的值就和文件中的一致了。

ODEX

在android5.0之前主要使用dalvik虚拟机。为了提高dex文件的执行效率,会对dex文件进行一定程度的优化,具体做法就是解析并生成一个odex,然后保存在/data/dalvik-cache目录。以后运行的时候不会读取apk中的dex。而是直接加载优化过的odex。这样节省了运行程序在优化上耗费的时间。

OAT

OAT是优化过的,用于art虚拟机执行的dex文件。类似dalvik的odex

ART虚拟机

ART使用AOT编译技术,在apk第一次安装或者系统升级,重启时,通过dex2oat将apk中的dex文件静态编译成oat文件存放到设备的/data/dalvik-cache或者/data/app/package目录。我测试安卓10是在dalvik-cache的里面。

dex2oat更像是编译器,把dalvik字节码编译成native机器码。这样就提高了程序启动的速度。art虚拟机直接执行的oat文件,而不是dex。但是aot有个确定,就是静态编译操作影响apk的安装效率。但是android7.0中增加了jit编译。提高了安装的效率。

生成OAT文件

除了安装时会自动生成OAT文件。也可以手动的生成,使用dex2oat命令。–dex-file传入dex路径,–oat-file指定oat文件路径。连接到设备,执行下面的命令。

dex2oat --dex-file=./classes.dex --oat-file=./classes.oat 生成出了oat文件。然后把oat文件pull到电脑上。

file classes.oat 看一下文件的格式

1
classes.oat: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (GNU/Linux), dynamically linked, stripped

oat文件格式在设计之初就考虑到最终执行的程序是elf格式的,最终将oat文件格式直接融入android所特有的elf格式。

既然是一个ELF文件,那么可以看一下符号表。可以使用ndk中的readelf查看。我直接用ida打开查看的符号表。图如下

image-20210701220459221

一个oat文件必须包含oatdata、oatexec、oatlastword三个符号。

oatdata:指向的地址是oat所在elf的.rodata段。这里存放的是oat文件头OATHeader、oat的dex文件头,OATDexFile、原始dex文件DexFile、oat的dex类OatClass等信息。

oatexec:指向的地址是oat所在elf的.text段,这里存放的是编译生成的native指令代码。

oatlastword:指向的地址是oat文件结束处在elf中的文件偏移,通过它可以知道oat文件的内容在哪里结束。

OatHeader的结构,书中记录的位置并没有找到对应的struct,但是有个类似的class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class PACKED(4) OatHeader {
public:
static constexpr uint8_t kOatMagic[] = { 'o', 'a', 't', '\n' };
// Last oat version changed reason: Math.pow() intrinsic.
static constexpr uint8_t kOatVersion[] = { '1', '3', '8', '\0' };

static constexpr const char* kImageLocationKey = "image-location";
static constexpr const char* kDex2OatCmdLineKey = "dex2oat-cmdline";
static constexpr const char* kDex2OatHostKey = "dex2oat-host";
static constexpr const char* kPicKey = "pic";
static constexpr const char* kDebuggableKey = "debuggable";
static constexpr const char* kNativeDebuggableKey = "native-debuggable";
static constexpr const char* kCompilerFilter = "compiler-filter";
static constexpr const char* kClassPathKey = "classpath";
static constexpr const char* kBootClassPathKey = "bootclasspath";
static constexpr const char* kConcurrentCopying = "concurrent-copying";
static constexpr const char* kCompilationReasonKey = "compilation-reason";

static constexpr const char kTrueValue[] = "true";
static constexpr const char kFalseValue[] = "false";


static OatHeader* Create(InstructionSet instruction_set,
const InstructionSetFeatures* instruction_set_features,
uint32_t dex_file_count,
const SafeMap<std::string, std::string>* variable_data);

bool IsValid() const;
std::string GetValidationErrorMessage() const;
const char* GetMagic() const;
uint32_t GetChecksum() const;
...
private:
...
uint8_t magic_[4];
uint8_t version_[4];
uint32_t adler32_checksum_;

InstructionSet instruction_set_;
uint32_t instruction_set_features_bitmap_;
uint32_t dex_file_count_;
uint32_t oat_dex_files_offset_;
uint32_t executable_offset_;
uint32_t interpreter_to_interpreter_bridge_offset_;
uint32_t interpreter_to_compiled_code_bridge_offset_;
uint32_t jni_dlsym_lookup_offset_;
uint32_t quick_generic_jni_trampoline_offset_;
uint32_t quick_imt_conflict_trampoline_offset_;
uint32_t quick_resolution_trampoline_offset_;
uint32_t quick_to_interpreter_bridge_offset_;

// The amount that the image this oat is associated with has been patched.
int32_t image_patch_delta_;

uint32_t image_file_location_oat_checksum_;
uint32_t image_file_location_oat_data_begin_;

uint32_t key_value_store_size_;
uint8_t key_value_store_[0]; // note variable width data at end

DISALLOW_COPY_AND_ASSIGN(OatHeader);
};

从代码可以直接看到,有些值是固定的。

magic:文件的标识,固定是“oat\n”。表示oat的头部

version:oat的文件格式版本

Adler32_checksum:oat的adler32校验和

insetruction_set:oat文件使用的指令集。结构如下

1
2
3
4
5
6
7
8
9
10
11
enum class InstructionSet {
kNone,
kArm,
kArm64,
kThumb2,
kX86,
kX86_64,
kMips,
kMips64,
kLast = kMips64
};

dex_file_count:oat文件中包含的dex文件的个数。通常普通app都是1

executable_offset:oatexec符号所在段的开始位置与oatdata符号所在段的开始位置的偏移。该值通常等于oatdata符号所在段.rodata的大小

jni_dlsym_lookup_offset:执行过程,如果类方法要调用另外一个方法是jni函数,就需要通过这个字段指向一段代码来调用

OatHeader下面是OatDexFile结构。数量和dex_file_count这个字段一致。

然后这里发现OatDexFile的结构和书中的不大一致

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
class OatDexFile FINAL {
...
private:
OatDexFile(const OatFile* oat_file,
const std::string& dex_file_location,
const std::string& canonical_dex_file_location,
uint32_t dex_file_checksum,
const uint8_t* dex_file_pointer,
const uint8_t* lookup_table_data,
const IndexBssMapping* method_bss_mapping,
const IndexBssMapping* type_bss_mapping,
const IndexBssMapping* string_bss_mapping,
const uint32_t* oat_class_offsets_pointer,
const DexLayoutSections* dex_layout_sections);

static void AssertAotCompiler();

const OatFile* const oat_file_ = nullptr;
const std::string dex_file_location_;
const std::string canonical_dex_file_location_;
const uint32_t dex_file_location_checksum_ = 0u;
const uint8_t* const dex_file_pointer_ = nullptr;
const uint8_t* const lookup_table_data_ = nullptr;
const IndexBssMapping* const method_bss_mapping_ = nullptr;
const IndexBssMapping* const type_bss_mapping_ = nullptr;
const IndexBssMapping* const string_bss_mapping_ = nullptr;
const uint32_t* const oat_class_offsets_pointer_ = 0u;
mutable std::unique_ptr<TypeLookupTable> lookup_table_;
const DexLayoutSections* const dex_layout_sections_ = nullptr;

friend class OatFile;
friend class OatFileBase;
DISALLOW_COPY_AND_ASSIGN(OatDexFile);
};

OatDexFile数组的下面是DexFile结构体。里面存放的是一个个完整的dex。

OAT的格式图如下

image-20210701232116020

OAT文件转换成dex文件

上面看到OAT文件中包含完整的dex文件。所以只要定位到OAT文件中的DexFile结构体,完整导出即可。

android系统提供了oatdump来导出所有的dex到指定目录

oatdump --export-dex-to=/data/local/tmp --oat-file=./classes.oat 命令执行完毕后得到文件classes.dex_export.dex

或者是自己写工具,来解析OAT文件格式,取出dex。也有一种简单的方式,直接搜索文件头”dex\n035”。文件头的0x20字节处保存了大小。有了偏移和大小。直接搜索即可。