首页
社区
课程
招聘
[原创]Unidbg-Linker部分源码分析(上)
2021-10-29 18:05 24842

[原创]Unidbg-Linker部分源码分析(上)

2021-10-29 18:05
24842

Unidbg-Linker部分源码分析(上)

概要

我们有了Android Linker源码部分的解析,还学习了Unicorn的详细使用。如果没有看过之前文章的同学可以看下之前的文章哦。今天我们就来看下Unidbg是如何将一个So加载且跑起来的

 

在Unidbg中,我们想要加载一个So一般都是通过

1
DalvikModule dalvikModule = vm.loadLibrary(new File("so_path"), true);

来加载一个So文件,通过这个方法,我们最终会发现Unidbg调用了AndroidElfLoader类的loadInternal方法。AndroidElfLoader类也就充当了Linker的角色,所以我们今天就来分析一下这个类

init

我们先来分析一下AndroidElfLoader的构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public AndroidElfLoader(Emulator<AndroidFileIO> emulator, UnixSyscallHandler<AndroidFileIO> syscallHandler) {
     // 调用父类构造方法,初始化emulator和syscallHandler字段
    super(emulator, syscallHandler);
 
    // 初始化SP
    stackSize = STACK_SIZE_OF_PAGE * emulator.getPageAlign();
 
    // 将栈空间mem_map映射,因为在Backend(Unicorn)中,所有需要用的内存都需要先进行映射才能够进行使用,大小就是STACK_SIZE_OF_PAGE * emulator.getPageAlign(),可读可写权限
    backend.mem_map(STACK_BASE - stackSize, stackSize, UnicornConst.UC_PROT_READ | UnicornConst.UC_PROT_WRITE);
 
    // 设置SP寄存器
    setStackPoint(STACK_BASE);
    // 初始化TLS(线程局部存储相关),在libc一些系统库中是有线程局部变量的,如errno等。这里就做了相关的协处理器的初始化操作
    this.environ = initializeTLS(new String[] {
            "ANDROID_DATA=/data",
            "ANDROID_ROOT=/system"
    });
    this.setErrno(0);
}

上面的注释也写的很清楚了,再总结一下AndroidElfLoader初始化做了什么

  • 在类中保存了emulator和syscallHandler字段,其中emulator就是一个模拟器对象。syscallHandler是linux系统调用相关的处理对象
  • 初始化了栈空间和设置SP寄存器
  • 初始化TLS操作

对TLS感兴趣可以阅读以下源码

http://androidxref.com/4.4.4_r1/xref/bionic/libc/bionic/libc_init_common.cpp#111
http://androidxref.com/4.4.4_r1/xref/bionic/libc/bionic/pthread_internals.cpp#66
http://androidxref.com/4.4.4_r1/xref/bionic/libc/private/bionic_tls.h#90

load

那么当我们调用了loadLibrary方法后,在Android中最终调用了AndroidElfLoader类下的loadInternal方法,我们来分析这个方法,该类的初始化我们已经分析过了

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
protected final LinuxModule loadInternal(LibraryFile libraryFile, boolean forceCallInit) {
    // File对象被封装为了LibraryFile对象
    try {
        // 接着调用了loadInternal(重载)方法,继续加载流程
        LinuxModule module = loadInternal(libraryFile);
        // 处理符号(关于重定位)
        resolveSymbols(!forceCallInit);
        // callInitFunction默认true
        if (callInitFunction || forceCallInit) {
            // 调用初始化函数
            for (LinuxModule m : modules.values().toArray(new LinuxModule[0])) {
                boolean forceCall = (forceCallInit && m == module) || m.isForceCallInit();
                if (callInitFunction) {
                    m.callInitFunction(emulator, forceCall);
                } else if (forceCall) {
                    m.callInitFunction(emulator, true);
                }
                m.initFunctionList.clear();
            }
        }
        // 添加引用计数
        module.addReferenceCount();
        return module;
    } catch (IOException e) {
        throw new IllegalStateException(e);
    }
}

上面的函数接着调用了重载loadInternal方法继续加载,加载完成会返回一个LinuxModule对象,该对象就保存了该So文件加载后的信息,接着处理了符号和调用初始化函数

 

我们接着来看重载loadInternal方法,代码比较长

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
private LinuxModule loadInternal(LibraryFile libraryFile) throws IOException {
    // 将我们的So文件让ElfFile类去解析,这个ElfFile是jelf库经过凯神改装过的,可以帮助解析Elf文件
    final ElfFile elfFile = ElfFile.fromBytes(libraryFile.mapBuffer());
 
    // ... 判断文件是否合法,32位模拟器是否加载了64位So等待
 
    long start = System.currentTimeMillis();
 
    // 获取当前So的最大虚拟地址和页对齐align参数
    long bound_high = 0;
    long align = 0;
    for (int i = 0; i < elfFile.num_ph; i++) {
        ElfSegment ph = elfFile.getProgramHeader(i);
        if (ph.type == ElfSegment.PT_LOAD && ph.mem_size > 0) {
            // 遍历所有mem_size>0的PT_LOAD段
            long high = ph.virtual_address + ph.mem_size;
            // 寻找bound_high最大值
            if (bound_high < high) {
                bound_high = high;
            }
            // 寻找alignment最大值
            if (ph.alignment > align) {
                align = ph.alignment;
            }
        }
    }
 
    ElfDynamicStructure dynamicStructure = null;
 
    // 从获取到的So指定的alignment和默认的PageAlign取一个最大值,一般拿到的就是4K大小
    final long baseAlign = Math.max(emulator.getPageAlign(), align);
 
    // 根据baseAlign来计算该So的加载地址。初始地址0x40000000L
    final long load_base = ((mmapBaseAddress - 1) / baseAlign + 1) * baseAlign;
 
    // 这个就相当于Linker在计算load_size,但Unidbg中将所有So的最小虚拟地址默认为0
    // 这里有改进空间对吧,因为Linker中为了防止内存浪费,出现了一个load_bias_字段
    // 但是出于目的不用,Unidbg的目的是让二进制文件跑起来
    long size = ARM.align(0, bound_high, baseAlign).size;
 
    // 设置加载下个So的mmapBaseAddress
    setMMapBaseAddress(load_base + size);
 
    // MemRegion存储了哪块内存对应了哪个So文件
    final List<MemRegion> regions = new ArrayList<>(5);
    MemoizedObject<ArmExIdx> armExIdx = null;
    MemoizedObject<GnuEhFrameHeader> ehFrameHeader = null;
    Alignment lastAlignment = null;
 
    // 再次遍历所有段
    for (int i = 0; i < elfFile.num_ph; i++) {
        ElfSegment ph = elfFile.getProgramHeader(i);
        switch (ph.type) {
            case ElfSegment.PT_LOAD:
                // PT_LOAD段
                // 获取该段在内存中对应的操作权限,如该段未指定,设置满权限(一般不会出现这种情况)
                int prot = get_segment_protection(ph.flags);
                if (prot == UnicornConst.UC_PROT_NONE) {
                    prot = UnicornConst.UC_PROT_ALL;
                }
                // 该段在内存中的起始地址
                final long begin = load_base + ph.virtual_address;
 
                // 计算该段在内存中的位置和大小
                Alignment check = ARM.align(begin, ph.mem_size, Math.max(emulator.getPageAlign(), ph.alignment));
 
                // 获取上一个内存快
                final int regionSize = regions.size();
                MemRegion last = regionSize <= 0 ? null : regions.get(regionSize - 1);
 
                // 处理两个段之间重叠部分?未找到案例
                MemRegion overall = null;
                if (last != null && check.address >= last.begin && check.address < last.end) {
                    overall = last;
                }
 
                if (overall != null) {
                    // 处理重叠段,应该为特殊情况,正常都会走下面else分支
                    long overallSize = overall.end - check.address;
                    backend.mem_protect(check.address, overallSize, overall.perms | prot);
                    if (ph.mem_size > overallSize) {
                        Alignment alignment = this.mem_map(begin + overallSize, ph.mem_size - overallSize, prot, libraryFile.getName(), Math.max(emulator.getPageAlign(), ph.alignment));
                        regions.add(new MemRegion(alignment.address, alignment.address + alignment.size, prot, libraryFile, ph.virtual_address));
                        if (lastAlignment != null) {
                            throw new UnsupportedOperationException();
                        }
                        lastAlignment = alignment;
                    }
                } else {
                    // 将该PT_LOAD段指示的内存大小进行映射
                    Alignment alignment = this.mem_map(begin, ph.mem_size, prot, libraryFile.getName(), Math.max(emulator.getPageAlign(), ph.alignment));
 
                    // 添加一块MemRegion
                    regions.add(new MemRegion(alignment.address, alignment.address + alignment.size, prot, libraryFile, ph.virtual_address));
                    if (lastAlignment != null) {
                        // 处理该段与上一个段之间的空隙,置0
                        long base = lastAlignment.address + lastAlignment.size;
                        long off = alignment.address - base;
                        if (off < 0) {
                            throw new IllegalStateException();
                        }
                        if (off > 0) {
                            backend.mem_map(base, off, UnicornConst.UC_PROT_NONE);
                            if (memoryMap.put(base, new MemoryMap(base, (int) off, UnicornConst.UC_PROT_NONE)) != null) {
                                log.warn("mem_map replace exists memory map base=" + Long.toHexString(base));
                            }
                        }
                    }
                    lastAlignment = alignment;
                }
 
                // 将该段对应的数据写入进已经映射好的内存
                ph.getPtLoadData().writeTo(pointer(begin));
                break;
            case ElfSegment.PT_DYNAMIC:
                // DYNAMIC段
                dynamicStructure = ph.getDynamicStructure();
                break;
            case ElfSegment.PT_INTERP:
                // INTERP段指定了解释器位置,在So中没用
                if (log.isDebugEnabled()) {
                    log.debug("[" + libraryFile.getName() + "]interp=" + ph.getInterpreter());
                }
                break;
            case ElfSegment.PT_GNU_EH_FRAME:
                // 没分析过,未知TODO
                ehFrameHeader = ph.getEhFrameHeader();
                break;
            case ElfSegment.PT_ARM_EXIDX:
                // 异常相关的段
                armExIdx = ph.getARMExIdxData();
                break;
            default:
                if (log.isDebugEnabled()) {
                    log.debug("[" + libraryFile.getName() + "]segment type=0x" + Integer.toHexString(ph.type) + ", offset=0x" + Long.toHexString(ph.offset));
                }
                break;
        }
    }
 
 
    // 此时,该So中的有用的段信息已经处理完毕
    // 该加载到内存的已经加载到内存
    // 该置空的内存也已置空
    // 动态段、异常段、PT_GNU_EH_FRAME段的信息已经保存下来,继续看接下来的处理
 
    // 动态段是必须有的
    if (dynamicStructure == null) {
        throw new IllegalStateException("dynamicStructure is empty.");
    }
    // 此SoName是动态段中的tag为SO_NAME指定的内容,而且Unidbg中的Log也是基于这个SoName打印的
    // 如果该内容为空,才会使用文件名。这也就是有的同学会问为什么我加载的是libxxxxx.so,而日志输出libyyyyyy.so呢
    final String soName = dynamicStructure.getSOName(libraryFile.getName());
 
    // 下面处理So中的依赖库
    Map<String, Module> neededLibraries = new HashMap<>();
 
    // dynamicStructure.getNeededLibraries(),这个方法是Unidbg改写jelf库加上的方法,会获取到所有的依赖库的名字
    for (String neededLibrary : dynamicStructure.getNeededLibraries()) {
        if (log.isDebugEnabled()) {
            log.debug(soName + " need dependency " + neededLibrary);
        }
 
        // modules字段保存了所有已经加载过的库,这里就是在寻找是否该So已经被加载过
        LinuxModule loaded = modules.get(neededLibrary);
        if (loaded != null) {
            // 如果加载过了,添加引用计数、放到neededLibraries变量
            loaded.addReferenceCount();
            neededLibraries.put(FilenameUtils.getBaseName(loaded.name), loaded);
            continue;
        }
        // 如果依赖还没有被加载过,就开始寻找这个依赖文件在哪,先在当前So的路径下找
        LibraryFile neededLibraryFile = libraryFile.resolveLibrary(emulator, neededLibrary);
 
        // 如果当前路径下没有找到,就去找library解析器去找
        if (libraryResolver != null && neededLibraryFile == null) {
            neededLibraryFile = libraryResolver.resolveLibrary(emulator, neededLibrary);
        }
 
        if (neededLibraryFile != null) {
            // 大吉大利,So找到啦,就会在这里加载
            LinuxModule needed = loadInternal(neededLibraryFile);
            needed.addReferenceCount();
            neededLibraries.put(FilenameUtils.getBaseName(needed.name), needed);
        } else {
            log.info(soName + " load dependency " + neededLibrary + " failed");
        }
    }
 
    // 到这里,该So所依赖的So也被加载进来了
 
    // 下面这个循环会处理未解决(符号为0特殊情况)的重定位,进行二次重定位,极少数能成功,如果确定没用可以注释掉
    for (LinuxModule module : modules.values()) {
        for (Iterator<ModuleSymbol> iterator = module.getUnresolvedSymbol().iterator(); iterator.hasNext(); ) {
            ModuleSymbol moduleSymbol = iterator.next();
            ModuleSymbol resolved = moduleSymbol.resolve(module.getNeededLibraries(), false, hookListeners, emulator.getSvcMemory());
            if (resolved != null) {
                if (log.isDebugEnabled()) {
                    log.debug("[" + moduleSymbol.soName + "]" + moduleSymbol.symbol.getName() + " symbol resolved to " + resolved.toSoName);
                }
                resolved.relocation(emulator);
                iterator.remove();
            }
        }
    }
 
 
    // 下面开始处理重定位
    List<ModuleSymbol> list = new ArrayList<>();
    for (MemoizedObject<ElfRelocation> object : dynamicStructure.getRelocations()) {
        // 遍历So中所有的重定位信息
        ElfRelocation relocation = object.getValue();
 
        // 拿到重定位类型
        final int type = relocation.type();
        if (type == 0) {
            log.warn("Unhandled relocation type " + type);
            continue;
        }
 
        // 拿到重定位项指定的符号信息
        ElfSymbol symbol = relocation.sym() == 0 ? null : relocation.symbol();
        long sym_value = symbol != null ? symbol.value : 0;
 
        // 计算需要重定位的位置
        Pointer relocationAddr = UnidbgPointer.pointer(emulator, load_base + relocation.offset());
        assert relocationAddr != null;
 
        Log log = LogFactory.getLog("com.github.unidbg.linux." + soName);
        if (log.isDebugEnabled()) {
            log.debug("symbol=" + symbol + ", type=" + type + ", relocationAddr=" + relocationAddr + ", offset=0x" + Long.toHexString(relocation.offset()) + ", addend=" + relocation.addend() + ", sym=" + relocation.sym() + ", android=" + relocation.isAndroid());
        }
 
        ModuleSymbol moduleSymbol;
        // 根据重定位类型进行不同的处理,下面包含了32/64位下的重定位处理
        switch (type) {
            case ARMEmulator.R_ARM_ABS32: {
                int offset = relocationAddr.getInt(0);
                moduleSymbol = resolveSymbol(load_base, symbol, relocationAddr, soName, neededLibraries.values(), offset);
                if (moduleSymbol == null) {
                    // 不能当即处理的,添加到list,后面再处理
                    list.add(new ModuleSymbol(soName, load_base, symbol, relocationAddr, null, offset));
                } else {
                    moduleSymbol.relocation(emulator);
                }
                break;
            }
            case ARMEmulator.R_AARCH64_ABS64: {
                long offset = relocationAddr.getLong(0) + relocation.addend();
                moduleSymbol = resolveSymbol(load_base, symbol, relocationAddr, soName, neededLibraries.values(), offset);
                if (moduleSymbol == null) {
                    list.add(new ModuleSymbol(soName, load_base, symbol, relocationAddr, null, offset));
                } else {
                    moduleSymbol.relocation(emulator);
                }
                break;
            }
            case ARMEmulator.R_ARM_RELATIVE: {
                int offset = relocationAddr.getInt(0);
                if (sym_value == 0) {
                    relocationAddr.setInt(0, (int) load_base + offset);
                } else {
                    throw new IllegalStateException("sym_value=0x" + Long.toHexString(sym_value));
                }
                break;
            }
            case ARMEmulator.R_AARCH64_RELATIVE:
                if (sym_value == 0) {
                    relocationAddr.setLong(0, load_base + relocation.addend());
                } else {
                    throw new IllegalStateException("sym_value=0x" + Long.toHexString(sym_value));
                }
                break;
            case ARMEmulator.R_ARM_GLOB_DAT:
            case ARMEmulator.R_ARM_JUMP_SLOT:
                moduleSymbol = resolveSymbol(load_base, symbol, relocationAddr, soName, neededLibraries.values(), 0);
                if (moduleSymbol == null) {
                    list.add(new ModuleSymbol(soName, load_base, symbol, relocationAddr, null, 0));
                } else {
                    moduleSymbol.relocation(emulator);
                }
                break;
            case ARMEmulator.R_AARCH64_GLOB_DAT:
            case ARMEmulator.R_AARCH64_JUMP_SLOT:
                moduleSymbol = resolveSymbol(load_base, symbol, relocationAddr, soName, neededLibraries.values(), relocation.addend());
                if (moduleSymbol == null) {
                    list.add(new ModuleSymbol(soName, load_base, symbol, relocationAddr, null, relocation.addend()));
                } else {
                    moduleSymbol.relocation(emulator);
                }
                break;
            case ARMEmulator.R_ARM_COPY:
                throw new IllegalStateException("R_ARM_COPY relocations are not supported");
            case ARMEmulator.R_AARCH64_COPY:
                throw new IllegalStateException("R_AARCH64_COPY relocations are not supported");
            case ARMEmulator.R_AARCH64_ABS32:
            case ARMEmulator.R_AARCH64_ABS16:
            case ARMEmulator.R_AARCH64_PREL64:
            case ARMEmulator.R_AARCH64_PREL32:
            case ARMEmulator.R_AARCH64_PREL16:
            case ARMEmulator.R_AARCH64_IRELATIVE:
            case ARMEmulator.R_AARCH64_TLS_TPREL64:
            case ARMEmulator.R_AARCH64_TLS_DTPREL32:
            case ARMEmulator.R_ARM_IRELATIVE:
            case ARMEmulator.R_ARM_REL32:
            default:
                log.warn("[" + soName + "]Unhandled relocation type " + type + ", symbol=" + symbol + ", relocationAddr=" + relocationAddr + ", offset=0x" + Long.toHexString(relocation.offset()) + ", addend=" + relocation.addend() + ", android=" + relocation.isAndroid());
                break;
        }
    }
 
    // 重定位完成后,开始执行初始化函数
    List<InitFunction> initFunctionList = new ArrayList<>();
    if (elfFile.file_type == ElfFile.FT_EXEC) {
        // 处理可执行文件相关,我们分析So的,忽略就可以
        int preInitArraySize = dynamicStructure.getPreInitArraySize();
        int count = preInitArraySize / emulator.getPointerSize();
        if (count > 0) {
            Pointer pointer = UnidbgPointer.pointer(emulator, load_base + dynamicStructure.getPreInitArrayOffset());
            if (pointer == null) {
                throw new IllegalStateException("DT_PREINIT_ARRAY is null");
            }
            for (int i = 0; i < count; i++) {
                Pointer func = pointer.getPointer((long) i * emulator.getPointerSize());
                if (func != null) {
                    initFunctionList.add(new AbsoluteInitFunction(load_base, soName, ((UnidbgPointer) func).peer));
                }
            }
        }
    }
    if (elfFile.file_type == ElfFile.FT_DYN) {
        // 处理So的初始化函数
        //下面的处理内容在新版有修复,我们之前Linker的文章也讲过,他们的顺序不应该是平级的,需要Init函数先执行
        int init = dynamicStructure.getInit();
        if (init != 0) {
            initFunctionList.add(new LinuxInitFunction(load_base, soName, init));
        }
 
        // 处理 init.array
        int initArraySize = dynamicStructure.getInitArraySize();
        int count = initArraySize / emulator.getPointerSize();
        if (count > 0) {
            Pointer pointer = UnidbgPointer.pointer(emulator, load_base + dynamicStructure.getInitArrayOffset());
            if (pointer == null) {
                throw new IllegalStateException("DT_INIT_ARRAY is null");
            }
            for (int i = 0; i < count; i++) {
                // 当作数组来处理每一个init函数
                Pointer func = pointer.getPointer((long) i * emulator.getPointerSize());
                if (func != null) {
                    // 将他们添加到initFunction列表中
                    initFunctionList.add(new AbsoluteInitFunction(load_base, soName, ((UnidbgPointer) func).peer));
                }
            }
        }
    }
 
    // 至此,依赖So加载了,重定位可以处理的也处理了(不能处理的还会有二次处理)
    // 初始化函数也被添加到列表中了,但是还没有调用(注意)
 
    SymbolLocator dynsym = dynamicStructure.getSymbolStructure();
    if (dynsym == null) {
        throw new IllegalStateException("dynsym is null");
    }
    ElfSection symbolTableSection = null;
    try {
        symbolTableSection = elfFile.getSymbolTableSection();
    } catch(Throwable ignored) {}
 
    // 将加载好的So封装位LinuxModule对象
    LinuxModule module = new LinuxModule(load_base, size, soName, dynsym, list, initFunctionList, neededLibraries, regions,
            armExIdx, ehFrameHeader, symbolTableSection, elfFile, dynamicStructure);
    // ...
 
    // 放入已加载的So列表中
    modules.put(soName, module);
    if (maxSoName == null || soName.length() > maxSoName.length()) {
        maxSoName = soName;
    }
    if (bound_high > maxSizeOfSo) {
        maxSizeOfSo = bound_high;
    }
    // 设置可执行Elf的入口点
    module.setEntryPoint(elfFile.entry_point);
    log.debug("Load library " + soName + " offset=" + (System.currentTimeMillis() - start) + "ms" + ", entry_point=0x" + Long.toHexString(elfFile.entry_point));
    // 通知监听器,So已加载完毕
    notifyModuleLoaded(module);
    return module;
}

总结

我们分为上下两篇文章。我们看到上面这个loadInternal方法就是加载一个So主要的内容,大致内容看到这里其实已经够用了,如果想了解更多的细节就看下篇。其中的很多方法我们没有展开来讲解,接下来我们就来处理剩余的细枝末节。如果文章有错误的地方还请指正,可以加个VX一起交流:roy5ue或直接在文章下方进行评论哦


[培训]《安卓高级研修班(网课)》月薪三万计划

最后于 2021-10-29 18:07 被r0ysue编辑 ,原因:
收藏
点赞6
打赏
分享
最新回复 (2)
雪    币: 1
活跃值: (822)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
贪吃虫 2021-11-2 17:55
2
0
wink
雪    币: 175
活跃值: (141)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Superoot 2021-11-2 19:52
3
0
mark
游客
登录 | 注册 方可回帖
返回