Android逆向基础

1.Dalvik虚拟机

  1. 特点
  • 体积小, 占用内存空间少。
  • 专有的DEX ( Dalvik Exe c utable )可执行文件格式, 体积小, 执行速度快。
  • 常量池采用32位索引值, 对类方法名、 字段名、 常量的寻址速度快。
  • 基于寄存器架构, 同时拥有 一套完整的指令系统。
  • 提供了对象生命周期管理、 堆枝管理、 线程管理、 安全和异常管理及垃圾回收等重要功能。
  • 所有的Android程序都运行在Android系统进程中, 每个进程都与一个 Dalvik虚拟机实例对应。
  1. Dalvik虚拟机与Java虚拟机的区别
  • 运行的字节码不同

  • Dalvik可执行文件的体积更小。(dx工具:将Java字节码转换为Dalvik字节码)

    dx对所有Java类文件中的常量池进行了分解, 消除了其中的冗余信息,然后将它们重新组合形成一个常量池,并让所有类文件共享这个常量池。

  • 虚拟机架构不同

    java: 基于栈架构

    Dalvik: 基于寄存器架构

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x2slSsm6-1611245855983)(https://github.com/lvxinghang/lvxinghang.github.io/raw/main/assets/img/Android%E8%BD%AF%E4%BB%B6%E5%AE%89%E5%85%A8/JVM.png)]

    iload_1:i表示int类型,load表示将局部变量存入Java栈,1表示要操作的哪个局部变量。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wcJk22WZ-1611245855986)(https://github.com/lvxinghang/lvxinghang.github.io/raw/main/assets/img/Android%E8%BD%AF%E4%BB%B6%E5%AE%89%E5%85%A8/Dalvik%E8%99%9A%E6%8B%9F%E6%9C%BA%E8%BF%90%E8%A1%8C%E7%8A%B6%E6%80%81.png)]

​ add-int v0,v3,v4:将v3与v4相加,并将值存入v0。

  1. 虚拟机的执行方式
  • 即时编译(Just-in-time Compilation ,JIT)又称动态编译, 是一种通过在运行时将字节码翻译为机器码使得程序的执行速度加快的技术。
  • 主流的JIT包括两种字节码编译方式
  • method方式:以函数或方法为单位进行编译。
  • trace方式:以trace为单位进行编译。

2.Dalvik 语言基础

  1. Dalvik字节码

    • 类型

    • 基本类型

    • 引用类型

    • Dalvik字节码类型描述符

      语法ZBJL[
      含义booleanbytelongJava类类型数组
    • Lpackage/name/ObjectName;

      L 表示后面眼着一 个Java类,package/name/表示对象所在 的包,ObjectName表示对象的名称,分号表示对象名结束。

    • 方法

    • Lpackage/name/ObjectName;->MethodName(III)Z
      

      MethodName为具体的方法名,(III)Z是方法的签名部分, 括号内的III 为方法的参数(在此为三个整型参数),z表示方法的返回类型。

  2. Dalvik指令集

  • 指令类型

    • move-wide/from16 vAA, vBBBB
      
      • move为基础字节码;

      • -wide为名称后缀,表示指令操作 的数据宽度(64位);

      • from16为字节码后缀,表示源为一个16位的寄存器引用变量;

      • vAA为目的寄存器,取值范围为v0~v255;

      • vBBBB为源寄存器, 取值范围为v0~v65535;

    • 空操作指令

      • 助记符为nop,值为00;
    • 数据操作指令

      • move vA,vB将vB寄存器的值赋予vA寄存器,源寄存器与目的寄存器都为4位
        move/from16 vAA,vBBBB将vBBBB寄存器的值赋予vAA 寄存器,源寄存器为16位,目的寄存器为8位
        move/16 vAAAA,vBBBB将vBBBB寄存器的值赋予vAAAA寄存器, 源寄存器与目的寄存器都为16位。
        move-wide vA,vB为4位的寄存器对赋值, 源寄存器与目的寄存器都为4位
      • move-wide vA, vB
        

        指令用于为4位的寄存器对赋值, 源寄存器与目的寄存器都为4位。

      • move-result vAA
        

        指令用于将上 一 个invoke类型指令操作的单字非对象结果赋予vAA寄存器。

    • 返回指令

      • return vAA
        

        指令表示函数返回 一 个32位非对象类型的值, 返回值为8位寄存器vAA。

    • 数据定义指令

      • const/16 vAA, #+BBBB
        

        指令用于将数值符号扩展为32位后赋予寄存器vAA。

    • 锁指令

      • monitor-enter vAAmonitor-exit vAA
        为指定对象获取锁释放指定对象的锁
    • 实例操作指令

      • check-cast vAA, type@BBBB
        

        指令用于将vAA寄存器中的对象引用转换成指定的类型。

      • new-instance vAA, type@BBBB
        

        指令用于构造一个指定类型对象的新实例, 并将对象引用赋值给vAA寄存器 。 类型符type指定的类型不能是数组类。

      • instance-of vA, vB, type@CCCC
        

        指令用于判断vB寄存器中的对象引用是否可以转换成指定的类型,如果可以就为vA寄存器赋值1,否则为vA寄存器赋值为0。

    • 数组操作指令

      • array-length vA, vB
        

      指令用于获取给定vB寄存器中数组的长度, 并将值赋予vA寄存器。

    • arrayop vAA, vBB, vCC
      
      • 指令用于对vBB寄存器指定的数组元素进行取值与赋值;

        • vCC寄存器用于指定数组元素的索引;
        • vAA寄存器用于存放读取的或需要设置的数组元素的值;
        • 读取元素时使用aget类指令, 为元素赋值时使用aput类指令;
      • new-array vA, vB, type@CCCC
        

        指令用于构造指定类型(type@CCCC )和大小(vB)的数组,并将值赋予vA寄存器。

    • 跳转指令

    • 无条件跳转指令goto

      goto/16 +AAAA
      

      指令用于无条件跳转到指定偏移处, 偏移量 AAAA不能为0。

      • packed-switch vAA, +BBBBBBBB
        
        • 分支跳转指令,vAA寄存器为switch分支中需要判断的值,BBBBBBBB指向一个packed-switch -payload格式的偏移表, 表中的值是递增的偏移量。
    • 若packed改为sparse,则表中的值是无规律的偏移量。

      • if-test vA, v8, +CCC
        
      
      
  • if-test类型指令如下

    smaliif-eqif-neif-ltif-geif-gtif-le
    javaif(vA==vB)if(vA!=vB)if(vA<vB)if(vA>=vB)if(vA>vB)if(vA<=vB)
    • if-testz vAA, +BBBB
      
  • 指令将vAA寄存器的值与0进行比较,若比较结果满足或值为0,就跳转。

  • 比较指令

    • cmpl-floatcmpg-floatcmpl -doublecmpg-double
      若vBB>vCC,结果为-1; 若=,则结果为0;若<,则结果为1若vBB>vCC,结果为1.若vBB对>vCC对,结果为-1.若vBB对>vCC对,结果为1.
  • 字段操作指令

    • 普通字段的指令前缀为i。
    • 静态字段的指令前缀为s。
  • 数据运算指令

    binop vAA, vBB, vCC将vBB 寄存器与vCC 寄存器进行运算, 将运算结果保存到vAA寄存器中
    binop/2addr vA, vB将vA寄存器与 vB寄存器进行运算, 将运算结果保存到vA寄存器中
    rem-typevBB % vCC
    shl-typevBB << vCC

3.Android文件格式

  1. 库文件

  2. jar包

    • zip格式的压缩包文件,放着编译后Java代码的class文件的集合。
  3. aar包

    • 既包含了代码,又包含了开发中所有使用的资源数据,解决了手动复制图片,声音,布局等资源的繁琐。
  4. APK

  5. classes.dex

DEX文件使用的数据类型

自定义类型原类型含义
s1int8_t8位有符号整型
u1uint8_t8位无符号整型
s2int16_t16位有符号整型,小端字节序
u2uint16_t16位无符号整型, 小端字节序
s4int32_t32位有符号整型, 小端字节序
u4uint32_t32位无符号整型, 小端字节序
s8int64_t64位有符号整型, 小端字节序
u8uint64_t64位无符号整型, 小端字节序
sleb128有符号LEB128, 可变长度
ulebl28无符号LEB128,可变长度
ulebl28pl无符号LEB128加1,可变长度
  • ​ 每个LEB128由I~5字节组成,所有的字节组合在一起表示一个32位的数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gD0hHTSS-1611245855988)(https://github.com/lvxinghang/lvxinghang.github.io/raw/main/assets/img/Android%E8%BD%AF%E4%BB%B6%E5%AE%89%E5%85%A8/LEB128.png)]

  • ​ 每个字节只有7位为有效位, 如果第l个字节的最高位为1, 表示LEB128需要使用第2个字节, 如果第2个字节的最高位为l, 表示会使用第3个字节, 依此类推, 直到最后 一个字节的最高位为0为止。 当然,LEB128最多使用5字节, 如果读取5字节后下一个字节的最高位仍为1, 则表示该DEX文件无效 , Dalvik虚拟机在验证DEX文件时会失败井返回。

DEX文件的结构

  • | dex header | dex文件头部记录整个dex文件的相关属性 |
    | :--------------- | ------------------------------------------------------------ |
    | string_table | 字符串数据索引,记录了每个字符串在数据区的偏移量 |
    | type_table | 类似数据索引,记录了每个类型的字符串索引 |
    | proto_ids | 原型数据索引,记录了方法声明的字符串,返回类型字符串,参数列表 |
    | field_ids | 字段数据索引,记录了所属类,类型以及方法名 |
    | method_ids | 类方法索引,记录方法所属类名,方法声明以及方法名等信息 |
    | class_def | 类定义数据索引,记录指定类各类信息,包括接口,超类,类数据偏移量 |
    | data | 真实的数据 |
    | link_data | 静态链接数据区 |

  • magic字段:表示这是一个有效的DEX文件, 目前它的值固定为 “64 65 78 Oa 30 33 35 00”
  • checksum字段:DEX文件的校验和,可以判断DEX文件是否已经损坏或被篡改
  • signature字段:识别未经dexopt优化的DEX文件
  • filesize字段:记录了包括DexHeade在内的整个DEX文件的大小
  • headerSize字段:记录了DexHeader结构本身占用的字节数
  • endianTag字段:指定了DEX运行环境的CPU字节序, 预设值ENDIAN_CONSTANT等于0x12345678, 表示默认采用小端字节序
  • linkSize与linkOff字段:分别指定了链接段的大小与文件偏移, 在大多数情况下它们的值为0
  • mapOff字段:指定了DexMapList结构的文件偏移
  • 接下来的字段则分别表示 DexStringId、DexTypeId、DexProtoId、DexFieldId、DexMethodId、DexClassDef及数据段的大小与文件偏移

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jPMB7JmR-1611245855991)(https://github.com/lvxinghang/lvxinghang.github.io/raw/main/assets/img/Android%E8%BD%AF%E4%BB%B6%E5%AE%89%E5%85%A8/DexCode.png)]
  • registersSize字段:指定了方法中使用的寄存器的个数。(对应于smali语法中.registerd的值)
  • insSize字段:指定了方法的参数的个数(对应于.paramter)
  • outsSize字段:指定了方法在调用外部方法时使用的寄存器的个数
  • triesSize字段:指定了方法中try/catch语句的个数
  • insnsSize字段:指定了接下来的指令的个数
  • insns字段:真正的代码部分

4.AndroidManifest.xml

android:allowBackup= ''true''

​ android:allowBackup允许系统 在进行备份操作时备份程序的应用数据, 典型的操作是在终端执行 adb backup命令, 或者点击手机设置界面上的 “备份操作 ” 按钮。

android:supportsRtl= ''true''

​ 这个标签的作用是让APK支持 RTL ( Right-to-Left )视图。

AXML文件格式

​ Android Studio 在编译APK文件时, 会将AndroidManifest.xml处理后打包进去。打包进去的AndroidManifest.xml被编译成了二进制格式的文件,这个打包后的AndroidManifest.xml称为 “AXML”。

AXML文件结构

​ 文件头ResFileheader,字符串池ResStringPool,资源ID块ResIDs,XML数据内容块ResXMLTree四部分线性地组成。

ResFileheader表示文件的头部, 在这里用ResChunk_header表示,其定义如下:

struct ResChunk_header
{
	uint16_t type;
	uint16_t headerSize;
	uint32_t size;
}

type字段描述了chunk所属结构体的类型。header_size字段表示当前ResChunk_header结构的大小。file_size字段表示该chunk结构体数据的长度。


​ 字符串池ResStringPool ,它包含了AXML中使用的所有字符串。字符串池由字符串池头ResStringPool_header字符串列表样式列表三部分组成。

ResStringPool_header定义如下:

struct ResStringPool_header 
{ 
	struct ResChunk_header header;
	uint32_t stringCount; 
	uint32_t styleCount;  
	enum {  
		SORTED FLAG= 1<<0UTF8 FLAG = 1<<8 
	};
	uint32_t flags; 
	uint32_t stringsStart;
	uint32_t stylesStart;
};

​ stringCount与styleCount字段分别表示这个池中字符串的数目与样式的数目。flags字段用于标识字符串的类型是UTF-8还是 16 位编码 。 stringsStart与stylesStart字段分别表示字符串列表与样式列表在文件中的偏移量。


字符串数据是 一个字符串索引列表, 每条索引都使用ResStringPool_string结构体来表示:

struct ResStringPool_string
{
	uint32_t index;
};

​ index字段指向字符串在文件中的具体偏移量,其指向的内容可能是一个UTF-8字符串,也可能是一个16位编码字符串。


资源ID块ResIDs,该部分主要用于 存放AndroidManifest.xml使用的系统属性值所对应的资源ID, 结构定义如下:

typedef struct { 
	ResChunk_header header;
	int count; 
	uint ids[count]; 
} ResIDs;

​ header字段的 type 在这里是 0x180, 即RES_XML_RESOURCE_MAP_TYPE, 表示这是资源表。count字段表示资源ID的个数。ids字段中存放的是一个个资源ID。

​ 每个资源ID都是一个32位的整型值,由三部分组成:

  • Package ID :相当于一个命名空间, 用于限定资源的来源。
  • Type ID:表示资源的类型ID。
  • Entry ID:指明了每一个资源在其所属的资源类型中的索引位置。

ResXMLTree,它用于 表示XML文件的具体内容。它是一个线性的XML节点数据集合,由多个XML节点数据组成, 由多个XML节点数据组成, 每个XML节点数据由基本结构体ResXMLT陀飞 node和扩展结构体组成。 ResXMLTree_node 的定义如下:

struct ResXMLTree node
{
	struct ResChunk_header header;
	uint32_t lineNumber; 
	structResStringPool_ref comment;
};

​ 对第1个节点来说,header的type字段必须是RES_XML_START_NAMESPACE_TYPE, 表示这是一个namespace开始节点。与此对应的是ResxXMLTree 部分的最后一个 ResXMLTr、民_node, 它的header的type字段必须是RES_XML_END_NA问ESPACE_TYPE, 表示 namespace 节点的结束。lineNumber字段表示节点数据在 AndroidManifest 文件中的行号, 占用4字节。


AXML文件的修改

目前,部分APK保护工具及一些厂商的加固方案利用了Android系统解析AXML的漏洞,在编译APK时构造畸形的AXML, 使系统能正常安装APK,但无法运行ApkTool 这类反编译工具 。 在这种情况下 ,需要对AXML进行修改,最直接的修改方式是:配合使用。10Editor及AXML模板查看文件格式,找到异常部分后进行修改。 对一些已经出现的AXML加固方案,可以使用现成的工具来修改, 具体如下。

  • AmBinaryEditor,下载地址为https://github.com/ele7enxxh/AmBinaryEditor。

  • AndroidManifestFix,下载地址为https://github.com/zylc369/AroidManifestFix。


5.resources.arsc

ARSC文件格式

​ 一个 ARSC 从整体结构上看, 由文件头ResTableHeader、资源项值字符串池ResStringPool、Package 数据内容块 ResTablePackage 三部分线性地组成。

文件头ResTableHeader:

struct ResTable_header
{
	struct ResChunk_header header;
	uint32_t packageCount;
};

​ header字段的类型是ResChunk_header,header的type字段指向的类型为RES_TABLE_TYPE,表示这是一个ARSC文件。packageCount字段指明该ARSC中包含多少个Package的资源信息,它对应ARSC文件中Package数据内容块ResTablePackage的个数。


​ Package数据内容块ResTablePackage,它由数据内容块头ResTable_package、资源类型字符串池 TypeStrings、资源项名称字符串池 KeyStrings、资源表规范ResTable_typeSpec、资源表类型配置ResTable_type五部分组成。ResTable_package的定义如下:

struct ResTable_package
{
	struct ResChunk_header header;
	uint32_t id;
	uint16_t name[128];
	uint32_t typeStrings;
	uint32_t lastPublicType;
	uint32_t keyStrings;
	uint32_t lastPublicKey;
	uint32_t typeIdOffset;
};

​ header的 type字段的类型是RES_TABLE_PACKAGE_TYPE。id字段指定了Package的ID, 对用户编译的 APK 来说,它的取值是Ox7F。name字段指定了Package的名称 ,该名称通常就是APK的包名。typeStrings字段是一个偏移量,指的是资源类型字符串池typeStrings在文件中相对ResTable_package结构体的偏移量。lastPublicType字段指的是导出的Public类型的字符串在资源类型字符串池中的索引。keyStrings字段是一个偏移量,指的是资源项名称字符串池 KeyStrings在文件中相对ResTable_package结构体的偏移量。lastPublicKey字段指的是导出的Public资源项名称字符串在资源项名称字符串池中的索引。typeidOffset 字段指的是类型ID的偏移量。


​ 在KeyStrings下面就是ResTable_typeSpec与ResTable_type,它们在文件中可能交叉出现。 ResTable_typeSpec的定义如下:

struct ResTable_typeSpec
{
	struct ResChunk_header header;
	uint8_t id;
	uint8_t res0;
	uint16_t res1;
	uint32_t entryCount;
};

​ header的type字段在这里是RES_TABLE_TYPE_SPEC_TYPE。id字段指明了类型规范资源的Type ID。内s0与resl字段的值目前必须是0。 entryCount字段指明了接下来的flags的个数(每个flag都是32位整型的)。


6.META-INF目录

CERT.RSA存放了 APK 的开发者证书与签名信息
MANIFEST.MF签名的清单文件, 它是一个文本文件
CERT.SF签名信息文件, 它也是一个文本文件

7.ODEX

ODEX与DEX相比, 多出了如下内容:

DexOptHeaderODEX文件头, 描述了ODEX文件的基本信息
Dependences依赖库列表 , 描述了ODEX文件加载时可能使用的依赖库
ClassLookups优化数据块的类索引列表信息, 用于提高类搜索速度
RegisterMaps优化数据块的寄存器图( Register Map )信息, 主要用于帮助Dalvik虚拟机进行精确的垃圾回收( Garbage Collection )

ODEX文件转换成DEX文件

先将ODEX文件反编译成smali文件, 再将smali文件编译成DEX文件,我们将这个过程称为 “deodex” 。

8.OAT

OAT是优化过的, 用于ART虚拟机执行的DEX文件,类似于Dalvik的ODEX文件。

ART虚拟机

ART使用AOT ( Ahead-of-Time )编译技术,在APK 第一次安装或系统升级、 重启时, 通过调用dex2oat命令将APK 中的DEX文件 静态编译成OAT文件并存放到 Android设备的/data/dalvik-cache 或/data/app/package目录下。AOT的静态编译操作会影响APK的安装效率,于是新版本的Android使用的是基于JIT on AOT的编译技术,可以明显加快安装速度。

OAT文件格式

OAT文件格式完全融入Android所特有的ELF格式,一个OAT文件必须包含 oatdata、oatexec、oatlastword三个符号。

  • oatdata符号指向的地址是 OAT 所在 ELF的.rodata 段,这里存放的是 OAi:文件头OATHeader、OAT 的 DEX文件头,OATDexFile、原始的DEX文件 DexFile、OAT的DEX类OatClass等信息。
  • oatexec符号指向的地址是OAT所在ELF的.text段, 这里存放的是编译生成的Native指令代码。
  • oatlastwo时符号指向的地址是OAT文件结束处在ELF中的文件偏移, 通过它可以确定OAT文件的内容在哪里结束。

将OAT文件转换成DEX文件

​ 定位OAT文件中的DexFile结构体, 将它的完整数据导出。Android系统提供了 oatdump命令, 使用该命令的–export-dex-to参数可以将OAT中的所有DEX文件导出, 放到指定的目录下。

adb shell 'oatdump --oat-file=/data/dalvik-cache/arm/system@framework@boot.oat --export-dex-to=/data/local/tmp'

​ 如果当前环境中没有合适的Andrid设备,要导出DEX的OAT文件就在本地, 可以尝试自行编写导出工具。传统的方法是解析OAT文件格式, 定位DexFile结构体后导出DEX 文件。 一种更简单的方法是:由于DEX文件有固定的文件头magic''dex\n035'' , 只需要在OAT文件中搜索它, 就可以快速定位DEX文件的开头(在DEX文件头的0x20字节处保存了DEX文件的完整大小, 这样一来, 要导出的偏移量与文件大小就都有了。 接下来, 只需向下搜索, 即可定位所有的DEX文件)。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
网传资源,如有侵权请联系/留言,资源过大,上传乃是下载链接,失效请留言,下面上大纲: 01.Android环境配置与常用工具介绍 02.Android smali 与 java 代码介绍1 : d% y( z) X- o& ~, e0 _; c1 I 03.Android smali 与 java 代码介绍2 c+ K& I/ q( b 04.Android smali 与 java 代码介绍3 % ]7 Z+ f! I! [5 S. O. N 05.Android smali 与 java 代码介绍4 7 A9 G6 c k; B 06.Android smali 与 java 代码介绍5 ; [. D3 O0 ~9 _0 ]3 W 07.常用Android快速定位关键点方法介绍 " v+ h0 Z5 x& }1 o4 c/ L 08.从0开始打造自己的破解代码库 09.Android 结构基础讲解 10.快速Hook代码搭建之 Cydia Substrate 11.快速Hook代码搭建之 Xposed 12.安装部署Android源码编译环境 13.Android源码目录结构与修改引导 / |3 T: f, f8 [2 @+ p 14.Android源码修改与刷机介绍 & D- q# v- o) o) ?/ u( A 15.Android Jni 编程 & Y6 ^/ J* G3 ] 16.arm 汇编代码讲解1 . J) E# f# h! Q4 x2 P+ K 17.arm 汇编代码讲解2 18.arm 汇编代码讲解3 19.arm 汇编代码讲解4 20.arm 汇编代码讲解5 ' B! y1 m7 _% U8 r2 G! R% h& L! a4 J0 B 21.class.dex文件格式讲解 22.Android 动态代码自修改原理 23.Android 动态代码自修改实现1 . F; Z5 @* D* r 24.Android 动态代码自修改实现2 25.Android dvm 脱壳1 26.elf结构详解1, d9 H, S" s2 }8 j' B6 v 27.elf结构详解2 8 A9 q+ O" `- v 28.elf文件变形与保护 1 g, b1 q, P( P& W, k3 F7 U 29.elf文件修复分析 9 K p" k/ `- s, w/ r: R( X 30.so加壳文件修复 31.常用调试检测方法与过检测方法 * G( L. J' P1 \+ }: N; r 32.Android源码定制添加反反调试机制 ' v/ q6 K1 {6 ] 33.Android dvm 脱壳2 34.Android dvm 脱壳3 H2 X- A# M4 s+ A6 K- b 35.Dalvik dex处理分析 ) x+ l1 l1 J R2 N) T" R) ^2 o 36.IDA脱壳脚本编写1) O7 `% E" Q. @1 X! o ~ 37.Odex修复方法 38.IDAOdex修复脚本编写 " X' w1 h: w3 N" u8 P5 z 39.Android 加壳原理 40.Android 加壳保护工具编写1 1 x4 k0 P/ V' C9 a( O 41.Android 加壳保护工具编写2 42.Android 加壳保护工具编写3
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿航的博客

我比你有钱,请不要打赏!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值