Art中的c++对象内存布局
zsk Lv4

ART由C++11实现的,C++11中的类所占内存的大小主要是由成员变量(静态变量除外)决定的,成员函数(虚函数除外)是不计算在内的。
成员函数的存储是以一般函数的模式进行存储。a.fun()是通过fun(a.this)来调用的,这时候this指针会做为隐藏的第一个参数传入成员函数,this指针的地址就是对象的地址。所谓成员函数只是名义上是在类里的。而成员函数的大小并不在类的对象里面,即同一个类的多个对象共享函数代码,因此可以简单将C++中的类当成C中的结构体即可。对象和成员函数的联系是靠:this指针,也是连接对象与其成员函数的唯一桥梁。

简单总结:
空的类是会占用内存空间的,而且大小是1,原因是c++要求每个实例在内存中都有独一无二的地址。

  1. 类内部的成员变量:
    普通的变量:是要占用内存的,但是要注意对齐原则(这点和struct类型很相似)。
    static修饰的静态变量:不占用内存,原因是编辑器将其放在全局变量区。
  2. 类内部的成员函数:
    普通函数:不占用内存
    虚函数:要占用4个以上字节,用来指定虚函数的虚函数表的入口地址。所以一个类的虚函数所占用的大小是不变的,和虚函数的个数是没有关系的。 32位下占4字节,64位占8字节。
  3. 多重继承,函数函数覆盖
    考虑下面情况,三个父类虚函数表中的f()的位置被替换成子类的函数指针。这样就可以任一父类对象指针来指向子类,并调用子类的f()了。也会导致虚函数表的指针增加,继承几个就会增加几个。
    假设,基类和派生类又如下关系:派生类i中覆盖了基类的虚函数f

image

art中的内存布局

将/art/runtime/dex_file.h下的DexFile类拷贝出来,除去属性外其余都删掉
在脱壳过程中拿到DexFile对象后,只需要知道它的begin和size的偏移
删除后的DexFile精简代码如下

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
class DexFile {
public:
// First Dex format version supporting default methods.
static const uint32_t kDefaultMethodsVersion = 37;
// First Dex format version enforcing class definition ordering rules.
static const uint32_t kClassDefinitionOrderEnforcedVersion = 37;

static const uint8_t kDexMagic[];
static constexpr size_t kNumDexVersions = 2;
static constexpr size_t kDexVersionLen = 4;
static const uint8_t kDexMagicVersions[kNumDexVersions][kDexVersionLen];

static constexpr size_t kSha1DigestSize = 20;
static constexpr uint32_t kDexEndianConstant = 0x12345678;

// name of the DexFile entry within a zip archive
static const char* kClassesDex;

// The value of an invalid index.
static const uint32_t kDexNoIndex = 0xFFFFFFFF;

// The value of an invalid index.
static const uint16_t kDexNoIndex16 = 0xFFFF;

// The separator character in MultiDex locations.
static constexpr char kMultiDexSeparator = ':';

// Closes a .dex file.
virtual ~DexFile() {};

public:
// The base address of the memory mapping.
uint8_t* begin_;
// The size of the underlying memory allocation in bytes.
size_t size_;
std::string location_;
uint32_t location_checksum_;
};

进行调用,分别以32和64位运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_cpp6_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {

DexFile dexFile;
void * beginAddress = (void *) &(dexFile.begin_);
void * sizeAddress = (void *) &(dexFile.size_);
unsigned long dexFileAddress = reinterpret_cast<unsigned long> (&dexFile); // 拿到DexFile对象的地址
unsigned long beginOffset = reinterpret_cast<unsigned long> (beginAddress)-dexFileAddress; // 拿到begin_的偏移量
unsigned long sizeOffset = reinterpret_cast<unsigned long> (sizeAddress)-dexFileAddress; // 拿到size_的偏移量
__android_log_print(4, "cpp11", "beginOffset: %lu, sizeOffset: %lu", beginOffset, sizeOffset);

std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}

输出日志:
arm32 com.example.cpp6 I/cpp11: beginOffset: 4, sizeOffset: 8
arm64 com.example.cpp6 I/cpp11: beginOffset: 8, sizeOffset: 16
去不去掉前面的static属性,都对内存布局没影响,对于脱壳而言,内存布局从前完后,而我们之关注begin_,size_。可以再把上面的DexFile类精简为结构体

1
2
3
4
5
struct DexFileStruct{
void* vptr; // 虚函数表指针
void* begin_;
uint32_t size_;
};

先测试一下,在libart.so中随便找一个函数参数有DexFile的,
以 art::ClassLinker::LoadMethod(art::DexFile const&,
art::ClassDataItemIterator const&, art::Handleart::mirror::Class, art::ArtMethod *) 为例,使用frida进行hook

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
function hookart(){
var libart_module = Process.getModuleByName("libart.so");
var LoadMethodaddr = null;
libart_module.enumerateExports().forEach(function(symbol){
// _ZN3art11ClassLinker10LoadMethodERKNS_7DexFileERKNS_21ClassDataItemIteratorENS_6HandleINS_6mirror5ClassEEEPNS_9ArtMethodE
// 在安卓7和8中LoadMethod的参数发生了变化,所以这里直接找函数的特征
if (symbol.name.indexOf("ClassLinker") >= 0 &&
symbol.name.indexOf("LoadMethod") >= 0 &&
symbol.name.indexOf("DexFile") >= 0 &&
symbol.name.indexOf("1ClassDataItemIterator") >= 0 &&
symbol.name.indexOf("ArtMethod") >= 0){
console.log(symbol.name, symbol.address);
LoadMethodaddr = symbol.address;
}
})

// 在安卓8.1下 art::ClassLinker::LoadMethod(art::DexFile const&, art::ClassDataItemIterator const&, art::Handle<art::mirror::Class>, art::ArtMethod *)
if (LoadMethodaddr != null) {
console.log("start hook LoadMethodaddr");
Interceptor.attach(LoadMethodaddr, {
onEnter: function (args) {
var dexfileptr = args[1];
var dexfilebegin = ptr(dexfileptr).add(Process.pointerSize * 1).readPointer();
var dexfilesize = ptr(dexfileptr).add(Process.pointerSize * 2).readU32();
console.warn("get a dex:size:" + dexfilesize + "---" + hexdump(dexfilebegin, {
length: 16
}))
console.log("go into LoadMethodaddr->" + hexdump(dexfileptr, {
length: 32
}));
}, onLeave: function () {
}
})
}
}

function main(){
hookart()
}

setImmediate(main);
1
2
3
4
5
get a dex:size:730868---             0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
7893d1a01c 64 65 78 0a 30 33 35 00 7f 66 c3 d6 bb 9f a5 71 dex.035..f.....q
go into LoadMethodaddr-> 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
78c4c40060 20 5e 11 cd 78 00 00 00 1c a0 d1 93 78 00 00 00 ^..x.......x...
78c4c40070 f4 26 0b 00 00 00 00 00 81 00 00 00 00 00 00 00 .&..............

可以看到dex的模值dex 037,也验证了dex的内存布局
在函数粒度的修复与脱壳中,还需要关注ArtMethod
将/art/runtime/art_method.h下的ArtMethod类拷贝出来,除去属性外其余都删掉
删除后的ArtMethod精简代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ArtMethod {
public:
// GcRoot <mirror::Class> declaring_class_; ==> uint32_t reference_;
uint32_t reference_;
uint32_t access_flags_;

// Offset to the CodeItem.
uint32_t dex_code_item_offset_;

// Index into method_ids of the dex file associated with this method.
uint32_t dex_method_index_;

uint16_t method_index_;
uint16_t hotness_count_;
};

抽取壳的过程主要关注两个字段,dex_code_item_offset_和dex_method_index_
脱壳过程中的应用在脱壳过程中,重点是从内存中提取出被保护或加密的DEX文件,或从已经加载到内存中的DEX文件中提取出方法字节码。这时,dex_code_item_offset_和dex_method_index_就起到了关键作用:

  1. 使用dex_code_item_offset_加上DEX文件基址,可以计算出方法字节码在内存中的实际地址。
  2. 通过dex_method_index_,可以找到该方法在DEX文件中的定义,从而获得方法的名称、所属类及其签名等信息。

    ArtMethod是没有虚函数的,所以在32位或64位下dex_code_item_offset_和dex_method_index_的偏移都是8, 12
    修改frida代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    if (LoadMethodaddr != null) {
    console.log("start hook LoadMethodaddr");
    Interceptor.attach(LoadMethodaddr, {
    onEnter: function (args) {
    var dexfileptr = args[1];
    this.artmethodptr = args[4];
    this.dexfilebegin = ptr(dexfileptr).add(Process.pointerSize * 1).readPointer();
    this.dexfilesize = ptr(dexfileptr).add(Process.pointerSize * 2).readU32();
    // console.warn("get a dex:size:" + dexfilesize + "---" + hexdump(dexfilebegin, {
    // length: 16
    // }))
    // console.log("go into LoadMethodaddr->" + hexdump(dexfileptr, {
    // length: 32
    // }));
    }, onLeave: function () {
    var dex_code_item_offset = ptr(this.artmethodptr).add(8).readU32(); // 在dex文件中函数的偏移, dexfilebegin+偏移就是函数的绝对地址
    var dex_method_index = ptr(this.artmethodptr).add(12).readU32(); // 在dex文件中的方法索引
    console.log(this.dexfilesize + "-->LoadMethodaddr index:" + dex_method_index, "-->" + this.dexfilebegin.add(dex_code_item_offset));
    }
    })
    }
    日志:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    _ZN3art11ClassLinker10LoadMethodERKNS_7DexFileERKNS_21ClassDataItemIteratorENS_6HandleINS_6mirror5ClassEEEPNS_9ArtMethodE 0x78ccc2868c
    start hook LoadMethodaddr
    3926232-->LoadMethodaddr index:29945 -->0x78984d1264
    3926232-->LoadMethodaddr index:29946 -->0x7898686a3c
    3926232-->LoadMethodaddr index:29949 -->0x7898686a54
    3926232-->LoadMethodaddr index:29950 -->0x7898686bd0
    3926232-->LoadMethodaddr index:29952 -->0x7898686e10
    3926232-->LoadMethodaddr index:29947 -->0x7898687000
    3926232-->LoadMethodaddr index:29948 -->0x789868711c
    ...
    对app解压,也能看到dex的大小也是3926232,把dex放进010editor查看函数的执行顺序
    1
    2
    3
    4
    struct method_id_item method_id[29945]	void s.h.e.l.l.S.<clinit>()	8A5E0h	8h	Fg: Bg:0x008080	Method ID
    struct method_id_item method_id[29946] void s.h.e.l.l.S.<init>() 8A5E8h 8h Fg: Bg:0x008080 Method ID
    struct method_id_item method_id[29949] void s.h.e.l.l.S.c(java.util.zip.ZipFile, java.util.zip.ZipEntry, java.io.File) 8A600h 8h Fg: Bg:0x008080 Method ID
    struct method_id_item method_id[29950] long s.h.e.l.l.S.g(java.io.File) 8A608h 8h Fg: Bg:0x008080 Method ID

打开apk的AndroidManifest.xml,跟artmethod打印的顺序是一致的,”s.h.e.l.l.S”是程序的入口

1
<application android:theme="0x7f090002" android:label="0x7f080000" android:icon="0x7f020029" android:name="s.h.e.l.l.S" android:debuggable="true" android:allowBackup="true" android:largeHeap="true">

最先执行的两个函数分别是初始化类和对象,

当类加载时,JVM会解析类的字节码,并为每个方法生成对应的 ArtMethod 实例。这个过程确保了Java函数和 ArtMethod 是一一对应的关系。每个Java方法都有一个与之对应的 ArtMethod 对象,它负责存储和管理该方法的信息。

可以说Java函数和 ArtMethod 是一一对应的关系,它们之间的对应关系是固定的,且在类加载和初始化过程中被建立

 评论