Apple高级调试与逆向工程

(七)你好,Mach-O

2020-03-08  本文已影响0人  收纳箱

1 你好,Mach-O

Mach-O是在任何苹果操作系统上运行的编译程序所使用的文件格式。格式知识对于调试和逆向工程都很重要。因为Mach-O的布局决定了如何将可执行文件存储在磁盘上,以及如何将可执行文件加载到内存中。

了解指令所引用的内存区域在逆向工程方面很有用,但在探索Mach-O时,在调试方面有许多有用的隐藏功能。例如:

1.1 专业用语

在查看所有不同的C结构之前,我们先看看Mach-O的布局。

这是每个编译可执行文件的布局。每个主程序,每个框架,每个内核扩展,所有在苹果平台上编译的东西都是这样。

Mach-O布局

在每个编译过的苹果程序的开头都有一个Mach-O头,它给出了这个程序可以什么样的CPU上运行,它是什么类型的可执行文件(一个框架?一个独立的程序?)以及后面有多少加载命令

加载命令是关于如何加载程序的指令。由C结构组成,其大小取决于加载命令的类型。

一些加载命令提供了有关如何加载segment的说明。将segment视为具有特定类型内存保护的内存区域。例如,可执行代码应该只有读取和执行权限;它不需要写入权限。

程序的其他部分,如全局变量或单例,需要读写权限,但不需要可执行权限。这意味着可执行代码和全局变量的地址将位于不同的segment中。

segment可以有0个或多个子组件,称为section。这些是由父segment提供的相同内存保护绑定的更细粒度区域。再看看上面的图表。segment命令1,指向可执行文件中包含四个segment命令的偏移量。segment命令2,指向包含0个段命令的偏移量。最后,segment命令3没有指向可执行文件中的任何偏移量。

开发人员和逆向工程人员正是对这些部分有着浓厚的兴趣。因为它们对程序都有着独特的用途。例如,有一个特定的section存储硬编码的UTF-8字符串,有一个特定的section存储对静态定义的变量的引用,等等。

1.2 Mach-O header

在每个已编译的Apple可执行文件的开头都有一个特殊的struct,表明它是否是Mach-O可执行文件。这个struct可以在mach-o/loader.h中找到。这个struct有两种变体:一种用于32位操作系统(mach_header),另一种用于64位操作系统(mach_header_64)。我们默认情况下讨论的都是64位系统。

让我们看看struct mach_header_64的布局。

struct mach_header_64 {
    uint32_t magic; /* mach magic number identifier */ 
    cpu_type_t cputype; /* cpu specifier */
    cpu_subtype_t cpusubtype; /* machine specifier */
    uint32_t filetype; /* type of file */
    uint32_t ncmds; /* number of load commands */
    uint32_t sizeofcmds; /* the size of all the load commands */ 
    uint32_t flags; /* flags */
    uint32_t reserved; /* reserved */
};

第一个成员,magic是一个硬编码的32位无符号整数,表示这是Mach-O头的开头。这个神奇的数字是多少?在mach-o/loader.h头文件中再往下一点,我们会发现:

/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /*the 64-bit mach magic number*/ #define MH_CIGAM_64 0xcffaedfe /*NXSwapInt(MH_MAGIC_64)*/

这意味着,如果字节顺序被交换,每个64位Mach-O可执行文件都将以0xfeedfacf0xcffaedfe开头。在32位系统上,magic0xfeedface,如果字节交换,则为0xcefaedfe。这个值可以让我们快速确定文件是Mach-O可执行文件,是32位还是64位架构编译的。

magic之后是cputypecpusubtype,这表示该Mach-O可执行文件允许在哪种类型的cpu上运行。filetype告诉我们正在处理哪种类型的可执行文件。同样,查询mach-o/loader.h会向我们展示以下定义。

#define MH_OBJECT 0x1 /* relocatable object file */ 
#define MH_EXECUTE 0x2 /* demand paged executable file */ 
#define MH_FVMLIB 0x3 /* fixed VM shared library file */
#define MH_CORE 0x4 /* core file */
...

因此,对于主可执行文件(不是framework),文件类型将是MH_EXECUTE。在filetype之后,header的下一个最意思的地方是ncmdssizeofcmds。加载命令表明了属性以及如何将可执行文件加载到内存中。

grep命令的Mach-O header

打开终端窗口,输入

xxd -l 32 /usr/bin/grep

这个命令将输出grep可执行文件的前32个原始字节。为什么是32字节?因为在struct mach header_64声明中,有8个变量,每个变量4字节长。

00000000: cffa edfe 0700 0001 0300 0080 0200 0000  ................
00000010: 1400 0000 9807 0000 8500 2000 0000 0000  .......... .....

x86_64 Intel系统使用一个小端架构。这意味着字节是反向的。

尽管x86_64 Intel架构是小端的,但苹果可以以大端小端的格式存储Mach-O信息。这部分是由于历史原因,可以追溯到PPC体系结构。但是iOS不会这样做,所以每个iOS文件的Mach-O头在磁盘和内存中都是小端的。相比之下,macOS上的Mach-O header顺序可以在任何一种格式中找到,但在内存中都是小端的。后面,我们将看到macOS的CoreFoundation模块,其Mach-O头以大端格式存储。

所以前4比特

//原始
cffa edfe
//拆分为比特
cf fa ed fe
//小端读取结果
fe ed fa cf

MH_MAGIC_64,也就是0xfeedfacf magic变量,这表明它是64位系统编译的。
幸运的是,xxd命令对于小端架构有一个特殊的选项-e。将-e选项添加到上一个终端命令中。

00000000: feedfacf 01000007 80000003 00000002  ................
00000010: 00000014 00000798 00200085 00000000  .......... .....

让我们将所有这些值放入struct mach_header_64

struct mach_header_64 {
    uint32_t        magic       = 0xfeedfacf
    cpu_type_t      cputype     = 0x01000007
    cpu_subtype_t   cpusubtype  = 0x80000003
    uint32_t        filetype    = 0x00000002
    uint32_t        ncmds       = 0x00000014
    uint32_t        sizeofcmds  = 0x00000798
    uint32_t        flags       = 0x00200085
    uint32_t        reserved    = 0x00000000
};

可以清楚地看到magic0xfeedfacf。在0xfeedfacf之后,有一个0x01000007。要计算出这个值,必须查看mach/machine.h

#define CPU_ARCH_ABI64 0x01000000 /* 64 bit ABI */
...
#define CPU_TYPE_X86 ((cpu_type_t) 7)

机器类型是CPU_ARCH_ABI64CPU_TYPE_X86一起产生十六进制的0x01000007

cpusubtype0x80000003由同一头文件中的CPU_SUBTYPE_LIB64CPU_SUBTYPE_X86_64_ALL确定。

filetype的值为0x00000002,即MH_EXECUTE。有20个加载命令(0x00000014为十六进制,大小为0x00200085)。

filetype0x00200085包含一系列的选项,在mach-o/loader.h中。

#define MH_NOUNDEFS 0x1     /* the object file has no undefined references */
#define MH_INCRLINK 0x2     /* the object file is the output of an incremental link against a base file and can't be link edited again */
#define MH_TWOLEVEL 0x80    /* the image is using two-level name space bindings */
#define MH_PIE 0x200000     /* When this bit is set, the OS will load the main executable at a random address.  Only used in MH_EXECUTE filetypes. */

最后,还有一个保留值,在这里没有任何意义。

胖header

有些可执行文件实际上是一组由一个或多个可执行文件“粘合”在一起的文件。例如,许多应用程序同时编译32位和64位可执行文件,并将它们放入“胖”可执行文件中。

多个可执行文件的“粘合在一起”由一个胖header表示,它还有一个独特的magic值,将其与Mach-O头区分开来。

紧跟在胖header之后的是structs,它表示CPU类型和文件中胖header存储位置的偏移量。

Fat Header

查看mach-o/fat.h可以得到以下结构:

#define FAT_MAGIC 0xcafebabe
#define FAT_CIGAM 0xbebafeca /* NXSwapLong(FAT_MAGIC) */
struct fat_header {
    uint32_t magic; /* FAT_MAGIC or FAT_MAGIC_64 */
    uint32_t nfat_arch; /* number of structs that follow */
};
...
#define FAT_MAGIC_64 0xcafebabf
#define FAT_CIGAM_64 0xbfbafeca /* NXSwapLong(FAT_MAGIC_64) */

尽管有64位的胖header,32位仍然广泛应用于64位系统。只有当可执行片的偏移量大于4MB时,才真正使用64位的胖headernfat_arch表明了有多少个胖结构的struct

下面是64位和32位版本的fat架构:

// 64
struct fat_arch_64 {
    cpu_type_t cputype; /* cpu specifier (int) */
    cpu_subtype_t cpusubtype; /* machine specifier (int) */
    uint64_t offset; /* file offset to this object file */
    uint64_t size; /* size of this object file */
    uint32_t align; /* alignment as a power of 2 */
    uint32_t reserved; /* reserved */
};

// 32
struct fat_arch {
    cpu_type_t cputype; /* cpu specifier (int) */
    cpu_subtype_t cpusubtype; /* machine specifier (int) */
    uint32_t offset; /* file offset to this object file */
    uint32_t size; /* size of this object file */
    uint32_t align; /* alignment as a power of 2 */
};

magic值表明了使用32位或64位结构中的哪一个。我们来看看macOS的CoreFoundation框架。

~> file /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamically linked shared library x86_64] [x86_64h]
/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation (for architecture x86_64):   Mach-O 64-bit dynamically linked shared library x86_64
/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation (for architecture x86_64h):  Mach-O 64-bit dynamically linked shared library x86_64h

这说明CoreFoundation由2个被分割的架构粘合在一起组成:x86_64x86_64hx86_64h代表Haswell,2013年10月引入Macbook Pro的x86_64变体。可以通过在加载核心基础模块的程序上使用LLDB来查看加载了哪个体系结构。

 ~> lldb /usr/bin/plutil
(lldb) target create "/usr/bin/plutil"
Current executable set to '/usr/bin/plutil' (x86_64).
(lldb) run
Process 29834 launched: '/usr/bin/plutil' (x86_64)
No files specified.
Process 29834 exited with status = 1 (0x00000001)
(lldb) image list -h CoreFoundation
[  0] 0x00007fff2ea5b000
(lldb) x/8wx 0x00007fff2ea5b000
0x7fff2ea5b000: 0xfeedfacf 0x01000007 0x00000008 0x00000006
0x7fff2ea5b010: 0x00000017 0x00001278 0x02000085 0x00000000

我们可以看到cpusubtype0x00000008对应mach/machine.h文件中的

#define CPU_SUBTYPE_X86_64_H            ((cpu_subtype_t)8)      /* Haswell feature subset */

所以CoreFoundationHaswell/x86_64h架构已加载到我的进程中。

我们接着看CoreFoundation

 ~>  xxd -l 48 -e /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
00000000: bebafeca 02000000 07000001 03000000  ................
00000010: 00100000 20997300 0c000000 07000001  .....s. ........
00000020: 08000000 00b07300 60987300 0c000000  .....s...s.`....

0xbebafeca(FAT_CIGAM)是字节交换的(大端)格式。这意味着不需要-e。为什么一个长度要用48个字节?我们来计算一下。

-e替换为字节大小参数-g,即按4字节分组显示输出,会更容易阅读。

 ~> xxd -l 48 -g 4 /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
00000000: cafebabe 00000002 01000007 00000003  ................
00000010: 00001000 00739920 0000000c 01000007  .....s. ........
00000020: 00000008 0073b000 00739860 0000000c  .....s...s.`....

注意:如果胖header是64位的,-g 4效果就不那么好了。因为struct fat_arch_64中有两个8字节的变量与4字节的变量混合在一起。

前两个值(0xcafebabe0x00000002)是struct fat_header,其余字节属于2个struct fat_arch。检查第一个struct fat_arch,我们可以看到它是针对x86_64的,因为前面看到的cputype 0x01000007cpusubtype 0x00000003。到x86_64扇区开始的偏移量为0x00001000(4096),其大小为0x00739920

要证明x86_64扇区在文件开头的偏移4096处,请使用xxd-s选项。

 ~>  xxd -l 32 -e -s 4096 /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
00001000: feedfacf 01000007 00000003 00000006  ................
00001010: 00000017 00001278 02000085 00000000  ....x...........

1.3 加载命令

紧跟在Mach-O header之后的是加载命令,指明了如何将可执行文件加载到内存中的指令,以及其他细节。每个加载命令都由一系列struct组成,每个struct的大小和参数都不同。

但对于每个加载命令struct,前两个变量总是一致的,即cmdcmdsizecmd将指示加载命令的类型,cmdsize将为您提供结构的大小。这使您可以迭代加载命令,然后按适当的cmdsize跳转。

Mach-O的作者预见到了这种情况,并提供了一个名为struct load_command的通用加载命令结构。

struct load_command {
    uint32_t cmd; /* type of load command */
    uint32_t cmdsize; /* total size of command in bytes */
};

这使我们可以将每个加载命令用这个通用结构加载命令启动。一旦知道了cmd值,就可以将内存地址转换成适当的struct。那么cmd有哪些值呢?我们可以在mach-o/loader.h中查看。

...
#define LC_SEGMENT_64   0x19    /* 64-bit segment of this file to be mapped */
#define LC_ROUTINES_64  0x1a    /* 64-bit image routines */
#define LC_UUID     0x1b    /* the uuid */
...

如果看到一个以LC开头的常量,那么这就是一个加载命令。有64位和32位等效加载命令,因此请确保使用适当的命令。64位加载命令将以名称中的64结尾。也就是说,64位系统仍然可以使用32位加载命令。例如,LC_UUID命令在名称中不包含64,但包含在所有可执行文件中。

LC_UUID是一个简单的加载命令。LC_UUID提供通用唯一标识符来标识可执行文件的特定版本。这个加载命令不提供任何特定的segment信息,因为它都包含在LC_UUID struct中。

实际上,LC_UUID加载命令的结构是mach-o/loader.h中的struct uuid_command

/*
 * The uuid load command contains a single 128-bit unique random number that
 * identifies an object produced by the static link editor.
 */
struct uuid_command {
    uint32_t    cmd;        /* LC_UUID */
    uint32_t    cmdsize;    /* sizeof(struct uuid_command) */
    uint8_t uuid[16];   /* the 128-bit uuid */
};

我们用otool -l来看看grep命令的UUID

 ~> otool -l /usr/bin/grep | grep LC_UUID -A2
     cmd LC_UUID
 cmdsize 24
    uuid 8093FC14-ACCC-343F-B925-CBCD7C5FBC8C

otool已将cmd0x1b转换为LC_UUID,将cmdsize显示为sizeof(struct uuid_command)(24字节),并以一种漂亮的格式显示UUID值。

1.4 Segments段

LC_UUID是一个简单的加载命令。因为它是自包含的,并且不向可执行文件的segment/section提供偏移量。下面我们来看看段segment

segment是一组具有特定权限的内存。一个段可以有0个或多个名为section的子组件。

在进入为segment提供指令的加载命令结构之前,让我们先讨论一些通常在程序中找到的segment

让我们通过一个真实的例子来分析这些信息。使用LLDB并附加到模拟器的SpringBoard为例。

 ~> lldb -n SpringBoard
(lldb) process attach --name "SpringBoard"
Process 16988 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
    frame #0: 0x00007fff523b625a libsystem_kernel.dylib`mach_msg_trap + 10
libsystem_kernel.dylib`mach_msg_trap:
->  0x7fff523b625a <+10>: retq
    0x7fff523b625b <+11>: nop

libsystem_kernel.dylib`mach_msg_overwrite_trap:
    0x7fff523b625c <+0>:  movq   %rcx, %r10
    0x7fff523b625f <+3>:  movl   $0x1000020, %eax          ; imm = 0x1000020
Target 0: (SpringBoard) stopped.

Executable module set to "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/CoreServices/SpringBoard.app/SpringBoard".
Architecture set to: x86_64h-apple-ios-.

(lldb) image dump sections SpringBoard
Sections for '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/CoreServices/SpringBoard.app/SpringBoard' (x86_64):
  SectID     Type             Load Address                             Perm File Off.  File Size  Flags      Section Name
  ---------- ---------------- ---------------------------------------  ---- ---------- ---------- ---------- ----------------------------
  0x00000100 container        [0x0000000000000000-0x0000000100000000)* ---  0x00000000 0x00000000 0x00000000 SpringBoard.__PAGEZERO
  0x00000200 container        [0x000000010e442000-0x000000010e448000)  r-x  0x00000000 0x00006000 0x00000000 SpringBoard.__TEXT
  0x00000001 code             [0x000000010e442d40-0x000000010e442d4a)  r-x  0x00000d40 0x0000000a 0x80000400 SpringBoard.__TEXT.__text
...

1.5 以编程方式查找segments和sections

我们创建一个命令行工具工程。打开Xcode,选择macOSCommand Line Tool,名字叫MachOSegments,语言选择Swift

import Foundation
import MachO // 1
for i in 0..<_dyld_image_count() { // 2
  let imagePath = String(validatingUTF8: _dyld_get_image_name(i))!// 3
  let imageName = (imagePath as NSString).lastPathComponent
  let header = _dyld_get_image_header(i)! // 4
  print("\(i) \(imageName) \(header)")
}
CFRunLoopRun() // 5
  1. 尽管Foundation会间接导入MachO模块,但为了安全和代码清晰起见,我们显式导入MachO模块。我们马上将使用mach-o/loader.h中找到的几个结构。
  2. _dyld_image_count函数将返回进程中所有加载模块的数量。
  3. _dyld_get_image_name函数将返回图像的完整路径。
  4. _dyld_get_image_header将返回该当前模块的Mach-O header(mach_headermach_header_64)的加载地址。
  5. CFRunLoopRun将阻止应用程序退出。因为输出完成后,我们想继续使用LLDB检查进程。

运行程序。我们将看到一个模块及其加载地址的列表显示在控制台上。这些加载地址是特定Mach-O header在该模块内存中的位置。这几乎与在LLDB中执行image list -b -h完全相同。例如:

7 CoreFoundation 0x00007fff2ea5b000
(lldb) x/8wx 0x00007fff2ea5b000
0x7fff2ea5b000: 0xfeedfacf 0x01000007 0x00000008 0x00000006
0x7fff2ea5b010: 0x00000015 0x00001258 0xc2000085 0x00000000

for循环的最后加入以下代码

var curLoadCommandIterator = Int(bitPattern: header) + MemoryLayout<mach_header_64>.size // 1
  for _ in 0..<header.pointee.ncmds {
    let loadCommand = UnsafePointer<load_command>(bitPattern: curLoadCommandIterator)!.pointee // 2
    if loadCommand.cmd == LC_SEGMENT_64 {
      let segmentCommand = UnsafePointer<segment_command_64>(bitPattern: curLoadCommandIterator)!.pointee // 3
      print("\t\(segmentCommand.segname)")
    }
    curLoadCommandIterator =  curLoadCommandIterator + Int(loadCommand.cmdsize) // 4
  }
  1. Mach-O header之后是加载命令。因此header地址和mach_header_64的大小相加,以确定加载命令的开始位置。我们这里使用的是64位设备,所以没有检测是否是32位。
  2. 使用Swift的UnsafePointer将加载命令强制转换为前面看到的“通用”load_command结构。如果此结构包含正确的cmd值,则将此内存地址强制转换为适当的segment_command_64 struct
  3. 这里我们知道load_command struct实际上应该是segment_command_64 struct,所以我们再次使用Swift的UnsafePointer对象进行转换。
  4. 在每个循环结束时,我们需要将curLoadCommandIterator变量增加当前loadCommand的大小。该大小由其cmdsize变量决定。

注意:当看到值LC_segment_64时,我们是如何知道要如何转换segment_command_64 struct的?在mach-o/loader.h头中,搜索对LC_SEGMENT_64的所有项目。有一个segment_command_64 struct,它的cmd对应的是LC_SEGMENT_64。查找对加载命令的所有参考将为我们提供对应的C结构体。

运行一下。

0 MachOSegments 0x0000000100000000
    (95, 95, 80, 65, 71, 69, 90, 69, 82, 79, 0, 0, 0, 0, 0, 0)
    (95, 95, 84, 69, 88, 84, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
    (95, 95, 68, 65, 84, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
    (95, 95, 76, 73, 78, 75, 69, 68, 73, 84, 0, 0, 0, 0, 0, 0)

这是因为Swift在使用C. segmentCommand.segname时非常糟糕。它被声明为Int8的Swift元组,这意味着我们需要构建一个helper函数来将这些值转换为实际可读的Swift字符串。

func convertIntTupleToString(name : Any) -> String {
  var returnString = ""
  let mirror = Mirror(reflecting: name)
  for child in mirror.children {
    guard let val = child.value as? Int8,
      val != 0 else { break }
    returnString.append(Character(UnicodeScalar(UInt8(val))))
  }
  return returnString
}

利用Mirror对象,我们可以接受任意大小的元组并对其进行迭代。它比用16个Int8对元祖类型的参数进行硬编码要好得多。

替换print("\t\(segmentCommand.segname)")

let segName = convertIntTupleToString(name: segmentCommand.segname)
print("\t\(segName)")

运行一下。这下好多了。

0 MachOSegments 0x0000000100000000
    __PAGEZERO
    __TEXT
    __DATA
    __LINKEDIT
1 libBacktraceRecording.dylib 0x0000000100112000
    __TEXT
    __DATA
    __LINKEDIT
2 libMainThreadChecker.dylib 0x0000000100124000
    __TEXT
    __DATA
    __LINKEDIT
...

在刚刚的print代码下马加入以下代码。

let sectionOffset = curLoadCommandIterator + MemoryLayout<segment_command_64>.size // 1
for j in 0..<segmentCommand.nsects { // 2
  let offset = MemoryLayout<section_64>.size * Int(j) // 3
  let sectionCommand = UnsafePointer<section_64>(bitPattern: sectionOffset + offset)!.pointee
  let sectionName = convertIntTupleToString(name: sectionCommand.sectname) // 4
  print("\t\t\(sectionName)")
}
  1. 获取内存中第一个struct section_64的基址。
  2. 在每个struct segment_command_64中,都有一个成员指明了跟在它后面的section_64命令的数量。我们用它进行遍历。
  3. for循环中,用j乘以的struct section_64的大小得到偏移量。计算sectionOffset + offset就可以得到正确的section_64地址。
  4. struct section_64还有一个sectname变量,它是Int8组成的另一个元组。我们将使用之前创建的同一个函数,从中获取字符串。

运行一下。

0 MachOSegments 0x0000000100000000
    __PAGEZERO
    __TEXT
        __text
        __stubs
        __stub_helper
        __cstring
        __objc_methname
        __const
        __swift5_typeref
        __swift5_builtin
        __swift5_reflstr
        __swift5_fieldmd
        __swift5_types
        __unwind_info
        __eh_frame
    __DATA
        __nl_symbol_ptr
        __got
        __la_symbol_ptr
        __mod_init_func
        __const
        __objc_imageinfo
        __objc_selrefs
        __data
        __swift_hooks
        __bss
    __LINKEDIT
1 libBacktraceRecording.dylib 0x0000000100114000
    __TEXT
        __text
        __stubs
        __stub_helper
        __cstring
        __const
        __info_plist
        __unwind_info
    __DATA
        __nl_symbol_ptr
        __got
        __la_symbol_ptr
...

如我们所看到的,只有主可执行文件才有__PAGEZERO segment,它有0个部分。里面有很多section包含swift5。因为Swift在苹果平台上没有OC就无法生存,所以在数据段中有很多OC相关的部分。

上一篇下一篇

猜你喜欢

热点阅读