从XXTEA到魔改AES:安卓逆向中复合加密算法的完整分析实战

1. 项目概述与核心思路拆解

最近在ISCC-2025的Mobile赛道上,遇到了一道融合了经典加密算法与安卓逆向的综合题目,它从基础的XXTEA算法入手,逐步过渡到经过深度魔改的AES加密,对逆向分析者的算法识别、代码还原和动态调试能力提出了不小的挑战。这道题的核心价值在于,它模拟了真实移动安全评估中常见的场景:开发者为了保护核心业务逻辑或关键数据,往往会采用自定义或修改过的加密算法来增加逆向难度。对于从事安卓安全研究、应用审计或CTF比赛的同行来说,掌握这类复合型加密算法的分析流程,是一项非常实用的技能。

这道题适合有一定安卓逆向基础,熟悉Jadx、Frida等基础工具,并对常见加密算法(如TEA系列、AES)有初步了解的开发者或安全爱好者。通过完整的解题过程,你不仅能巩固静态分析与动态调试的技巧,更能深入理解如何从零开始,一步步还原一个被混淆和修改过的加密黑盒。整个过程就像在解一个复杂的密码锁,你需要先找到锁眼(算法入口),识别锁芯的结构(算法类型),最后才能配出正确的钥匙(解密密钥)。

2. 环境准备与工具链选择

工欲善其事,必先利其器。面对一个混合了多种加密的安卓APK,一套高效、稳定的工具链是成功的一半。以下是我在实际解题和日常审计中反复验证过的组合,兼顾了效率与深度。

2.1 核心静态分析工具

静态分析是我们的“地图”,用于快速了解应用结构和定位关键代码。

  • Jadx-GUI :这是我们的主力反编译器。它的优势在于能将Dex文件快速、可读地反编译成Java代码,对于分析算法逻辑和程序流程至关重要。我通常使用其“搜索”功能全局查找关键词,如“encrypt”、“decrypt”、“AES”、“TEA”等。
  • Android Killer / APKTool :用于APK的解包和重打包。当我们需要修改Smali代码进行插桩、绕过验证或者简单Patch时,就离不开它们。Android Killer集成了APKTool、签名等工具,对新手更友好。
  • IDA Pro / Ghidra :当关键逻辑被下沉到Native层(.so文件)时,就必须请出这些强大的反汇编工具。对于魔改的AES算法,其核心的S盒、列混合等操作很可能在C/C++层实现,需要用它们进行逆向。

2.2 核心动态调试工具

动态调试是我们的“探针”,用于在运行时观察数据流和验证猜想。

  • Frida :动态插桩的瑞士军刀。我们可以编写JavaScript脚本,在目标应用运行时挂钩(Hook)关键函数,打印出函数的输入参数、返回值以及内部变量的值。这对于快速验证加密函数的输入输出、获取中间计算值(如轮密钥)有无可替代的优势。
  • Objection :基于Frida的命令行工具,可以快速完成内存搜索、绕过SSL Pinning等常见任务,作为Frida的补充非常高效。
  • 一台Root过的安卓真机或模拟器 :这是动态调试的基础。我强烈推荐使用真机,稳定性远胜模拟器。如果使用模拟器,推荐 夜神模拟器(Android 7/9) ,其对Frida的支持相对较好。 注意 :题目本身或日常分析中,绝对不应涉及任何关于绕过网络限制的工具或方法,所有分析均在合法授权的设备或模拟环境中进行。

2.3 辅助与效率工具

  • Charles / Burp Suite :用于抓取应用网络流量,有时加密后的数据会直接通过网络传输,抓包可以为我们提供密文样本。
  • Python + 相关密码学库(pycryptodome) :用于快速验证算法。当我们推测出算法和密钥后,可以立刻写一个Python脚本进行加解密验证,这比在手机或Java环境中测试要快得多。
  • 文本编辑器/IDE :如VS Code,用于编写和修改Frida脚本、Python脚本。

提示:工具版本尽量保持较新,但不必追求最新。一个稳定、熟悉的工具组合比频繁更换新工具更能提升效率。建议提前配置好Frida服务端与客户端的连接,避免在分析时手忙脚乱。

3. 初步侦察与入口点定位

拿到APK后,不要急于深入细节。先进行一遍快速的“体检”,了解其整体情况。

3.1 基础信息收集

首先使用 apktool d 或直接使用Jadx打开APK。关注以下几点:

  1. AndroidManifest.xml :查看主Activity(程序入口)、使用的权限、是否有Native库声明( android:hasCode="true" 或特定的 <library> 标签)。
  2. 资源文件 :有时密钥或关键字符串会硬编码在 strings.xml res 目录下的其他文件中。
  3. Lib目录 :查看 lib/ 下有哪些架构的 .so 文件,这能提示我们加密逻辑是否在Native层。

3.2 搜索加密相关关键词

在Jadx中打开全局搜索(通常快捷键是Ctrl+Shift+F)。我们根据题目标题和常见模式,优先搜索以下关键词:

  • 算法名 TEA , XXTEA , AES , DES , RSA , MD5 , SHA
  • 功能名 encrypt , decrypt , cipher , Crypto , SecretKey , KeyGenerator
  • 常量特征 0x9E3779B9 (TEA系列算法的黄金比例常数), AES/ECB/PKCS5Padding (AES算法模式字符串)。
  • 类名 :查看是否有包含 Crypto , Encrypt , Decrypt , Security , Cipher 等字眼的类。

在这个题目中,搜索“XXTEA”或“0x9E3779B9”很可能直接定位到第一层加密的逻辑。而“AES”相关的搜索可能会指向另一个类或方法,这提示我们加密是分阶段的。

3.3 定位用户交互点

找到程序的主Activity,特别是用户输入(如Flag)和点击“验证”按钮的事件处理方法。这个方法的末尾,通常会调用核心的校验或加密函数。这是我们分析逻辑流的起点。例如,找到 onClick 监听器,跟踪其调用的 checkFlag() verify() 方法。

4. 第一层:XXTEA算法识别与逆向

假设我们通过搜索,找到了一个名为 XXTEAUtils.encrypt 的方法。XXTEA是TEA系列算法的一个变种,比原始TEA更安全。

4.1 XXTEA算法原理快速回顾

理解算法才能逆向它。XXTEA的核心是对一个32位整数数组进行多轮迭代运算。其加密和解密过程使用相同的轮函数,但顺序相反。几个关键特征:

  • 数据分组 :将明文填充并分割成多个32位无符号整数( uint32_t )。
  • 密钥 :是一个128位的密钥,同样表示为4个32位整数数组。
  • 魔数 0x9E3779B9 ,这是一个由黄金分割率推导出的常数,在每轮运算中都会使用。
  • 轮函数 :包含移位、异或、加法等操作,将数据与密钥进行多轮混合。

在Java代码中,你可能会看到类似这样的结构:

public static byte[] encrypt(byte[] data, byte[] key) {
    // 1. 将data字节数组转换为int数组v
    int[] v = bytesToInts(data, true); // true表示小端序
    // 2. 将key字节数组转换为int数组k
    int[] k = bytesToInts(key, true);
    // 3. 核心加密循环
    int n = v.length - 1;
    int rounds = 6 + 52 / (n + 1);
    long sum = 0;
    int z = v[n];
    int y = v[0];
    int delta = 0x9E3779B9; // 魔数
    for (int i = 0; i < rounds; i++) {
        sum += delta;
        int e = (int)((sum >> 2) & 3);
        for (int p = 0; p < n; p++) {
            y = v[p + 1];
            // MX宏定义的运算,包含移位、异或、加法
            v[p] += ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum ^ y) + (k[(p & 3) ^ e] ^ z));
            z = v[p];
        }
        y = v[0];
        // 最后一轮的MX运算
        v[n] += ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum ^ y) + (k[(n & 3) ^ e] ^ z));
        z = v[n];
    }
    // 4. 将int数组v转换回字节数组
    return intsToBytes(v, true);
}

看到这样的结构,结合常数 0x9E3779B9 ,基本可以确定是XXTEA。

4.2 静态分析与密钥获取

我们的目标是找到解密逻辑。通常,题目会提供加密函数,我们需要逆向写出解密函数,或者更简单——直接获取加密时使用的密钥。

  1. 查找密钥 :在 encrypt 方法附近,查找密钥的来源。它可能是:
    • 硬编码在代码中的字节数组。
    • 从资源文件(如 strings.xml )中读取。
    • 通过某个简单变换(如字符串反转、Base64解码)生成。
    • 在Jadx中,可以右键点击密钥变量,选择“查找用例”,追踪其赋值源头。
  2. 分析数据流 :找到调用 XXTEAUtils.encrypt 的地方,看它的输入(明文)是什么,输出(密文)又传递到哪里去了。输出很可能作为下一层加密(魔改AES)的输入。

4.3 使用Frida进行动态验证

静态分析可能因为混淆而不够清晰。此时,Frida就派上用场了。我们可以写一个脚本,Hook这个加密函数。

Java.perform(function() {
    var XXTEAUtils = Java.use('com.example.challenge.XXTEAUtils');
    XXTEAUtils.encrypt.overload('[B', '[B').implementation = function(data, key) {
        console.log(\"[*] XXTEA.encrypt called!\");
        console.log(\"Data (hex): \" + bytesToHex(data));
        console.log(\"Key (hex): \" + bytesToHex(key));
        var result = this.encrypt(data, key);
        console.log(\"Result (hex): \" + bytesToHex(result));
        return result;
    };
    // 辅助函数:字节数组转十六进制字符串
    function bytesToHex(bytes) {
        var arr = [];
        for (var i = 0; i < bytes.length; i++) {
            arr.push(('0' + (bytes[i] & 0xFF).toString(16)).slice(-2));
        }
        return arr.join('');
    }
});

运行这个脚本,当应用执行加密时,我们就能在控制台看到实时的输入、输出和密钥。这能100%确认我们的静态分析结果,并拿到确切的密钥值。

5. 第二层:魔改AES的深度剖析

通过了XXTEA层,我们得到的输出成为了魔改AES的输入。这是本题的难点所在。“魔改”意味着标准AES的某些组成部分被修改了,常见的魔改点包括:

5.1 AES标准流程与常见魔改点

标准AES-128加密流程(以ECB模式为例)包括:

  1. 密钥扩展 :将初始密钥扩展成11组轮密钥(Round Key)。
  2. 初始轮 :明文与第0轮轮密钥进行异或(AddRoundKey)。
  3. 重复9轮标准轮函数 ,每轮包含:
    • 字节替换(SubBytes) :通过S盒进行非线性替换。
    • 行移位(ShiftRows) :对状态矩阵的行进行循环移位。
    • 列混合(MixColumns) :对状态矩阵的列进行线性变换。
    • 轮密钥加(AddRoundKey) :与当前轮密钥异或。
  4. 最终轮 :执行字节替换、行移位、轮密钥加( 不进行列混合 )。

魔改通常发生在:

  • 自定义S盒 :开发者替换了标准的AES S盒,这是最隐蔽的魔改。逆向时必须找到这个新的替换表。
  • 修改列混合矩阵 :改变了 MixColumns 所用的固定矩阵。
  • 修改密钥扩展算法 :让轮密钥的生成方式变得非标准。
  • 增加/减少加密轮数
  • 改变操作顺序

5.2 定位与逆向魔改AES逻辑

  1. 寻找入口 :在Jadx中跟踪XXTEA加密结果的流向,找到下一个处理它的方法,类名可能包含 AES MyCipher CustomCrypto 等。
  2. 区分Java与Native :如果这个类的方法体非常简单,只是用 native 关键字声明,或者直接调用了 System.loadLibrary 加载的库函数,那么核心逻辑就在 .so 文件里。反之,则可能在Java层。
  3. 静态分析Java层魔改 :如果在Java层,我们需要仔细阅读代码,对比标准AES实现。重点关注:
    • S盒数组 :查找名为 SBOX sBox 的静态字节数组。用Python打印出其值,与标准AES S盒对比。如果不一致,就是魔改点。
    • 列混合矩阵 :查找名为 MIX_COLUMNS_MATRIX 的矩阵。
    • 密钥扩展函数 :查看 KeySchedule expandKey 方法。
    • 轮函数 :查看 encryptBlock cipher 方法。
  4. 静态分析Native层魔改 :如果在Native层( .so ),就需要用IDA Pro或Ghidra打开对应的库文件。
    • 导出函数 :在导出函数表中查找 Java_ 开头的函数(JNI函数),或者直接搜索 AES encrypt 等字符串。
    • 定位S盒 :在IDA中,S盒通常是一个大小为256的静态字节数组。可以通过查找交叉引用来定位使用它的函数。魔改的S盒值会直接硬编码在数据段。
    • 分析算法逻辑 :对照标准AES的伪代码,分析反汇编出来的函数流程。寻找 SubBytes ShiftRows MixColumns AddRoundKey 对应的代码块。注意识别魔改部分。

5.3 动态提取关键数据(S盒、密钥)

对于魔改AES,尤其是S盒被改的情况,动态提取往往比静态分析更直接。我们可以用Frida Hook Native层函数。 假设我们通过分析,找到了Native层的加密函数 native_encrypt 。我们可以编写Frida脚本读取其内存中的S盒和轮密钥。

// 假设我们已知S盒在内存中的地址是 0xcf2d4000(通过IDA静态分析得到)
var sbox_addr = ptr(\"0xcf2d4000\");
// 读取256字节的S盒
var sbox = Memory.readByteArray(sbox_addr, 256);
console.log(\"[*] Dumping Custom S-Box:\");
console.log(hexdump(sbox, { offset: 0, length: 256, header: true, ansi: false }));

// Hook native加密函数,打印输入输出
Interceptor.attach(Module.findExportByName(\"libnative-lib.so\", \"native_encrypt\"), {
    onEnter: function(args) {
        // args[0]: JNIEnv, args[1]: jclass, args[2]: jbyteArray input, args[3]: jbyteArray key...
        console.log(\"[*] native_encrypt called\");
        // 这里需要根据函数签名,正确读取Java传入的字节数组
        // 通常使用JNI API如 GetByteArrayElements
    },
    onLeave: function(retval) {
        console.log(\"[*] native_encrypt returned\");
    }
});

通过动态Hook,我们可以直接拿到运行时确切的S盒内容、输入的明文/密文以及密钥,为后续编写解密脚本提供准确数据。

6. 完整解密流程串联与脚本编写

现在,我们手头应该有了以下信息:

  1. XXTEA的密钥(Key_xxtea)。
  2. 魔改AES的密钥(Key_aes)和自定义S盒(Custom_Sbox)。
  3. 整个加密流程: Flag -> XXTEA加密 -> 中间数据 -> 魔改AES加密 -> 最终密文

题目通常会给出一个“最终密文”,我们需要逆向这个过程得到Flag。

6.1 解密步骤设计

解密流程是加密的逆序:

  1. 逆向魔改AES :使用我们提取出的 Custom_Sbox Key_aes ,编写一个解密函数。这需要根据魔改的具体内容来实现:
    • 如果只改了S盒,那么解密时需要对应的逆S盒(Inv_Sbox)。可以通过遍历 Custom_Sbox 计算出来: Inv_Sbox[Custom_Sbox[i]] = i
    • 如果改了列混合矩阵,则需要求出该矩阵的逆矩阵,用于解密时的逆列混合。
    • 密钥扩展算法如果被改,解密时也需要对应的逆密钥扩展,或者直接使用加密时生成的轮密钥(顺序反向使用)。
  2. 逆向XXTEA :XXTEA的解密算法是其加密算法的逆过程。标准XXTEA解密代码网上很多,我们需要确保使用的 delta 常数( 0x9E3779B9 )和轮数与加密端一致。

6.2 Python解密脚本示例

以下是一个高度简化的示例框架,展示了如何串联两个解密过程:

import struct

# 假设我们通过分析得到的常量
DELTA = 0x9E3779B9
ROUNDS = 32 # XXTEA轮数,根据实际分析确定

# 1. 自定义的AES解密函数 (这里仅为框架,需填充具体魔改逻辑)
def custom_aes_decrypt(ciphertext, key, custom_sbox):
    """
    魔改AES解密
    :param ciphertext: 字节数组,最终密文
    :param key: 字节数组,AES密钥
    :param custom_sbox: 字节数组,自定义的256字节S盒
    :return: 字节数组,AES解密后的中间数据
    """
    # TODO: 实现基于custom_sbox和key的AES解密逻辑
    # 包括:生成逆S盒、逆列混合、轮密钥逆序使用等
    # 这里需要你根据静态/动态分析出的具体魔改方式编写
    intermediate_data = b'' # 假设这是解密结果
    print(f\"[+] AES Decrypted: {intermediate_data.hex()}\")
    return intermediate_data

# 2. 标准XXTEA解密函数
def xxtea_decrypt(data, key):
    """
    XXTEA解密
    :param data: 字节数组
    :param key: 字节数组(16字节)
    :return: 字节数组
    """
    def _to_ints(v):
        n = len(v)
        ints = []
        for i in range(0, n, 4):
            ints.append(struct.unpack(\"<I\", v[i:i+4])[0]) # 小端序
        return ints
    def _to_bytes(ints):
        data = b''
        for i in ints:
            data += struct.pack(\"<I\", i & 0xffffffff) # 小端序
        return data
    v = _to_ints(data)
    k = _to_ints(key)
    n = len(v)
    rounds = 6 + 52 // n
    sum_ = (DELTA * rounds) & 0xffffffff
    y = v[0]
    for _ in range(rounds):
        e = (sum_ >> 2) & 3
        for p in range(n-1, 0, -1):
            z = v[p-1]
            v[p] -= ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum_ ^ y) + (k[(p & 3) ^ e] ^ z))
            v[p] &= 0xffffffff
            y = v[p]
        z = v[n-1]
        v[0] -= ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4)) ^ ((sum_ ^ y) + (k[(0 & 3) ^ e] ^ z))
        v[0] &= 0xffffffff
        y = v[0]
        sum_ = (sum_ - DELTA) & 0xffffffff
    plain_bytes = _to_bytes(v)
    # 去除可能的填充(根据实际情况,可能是PKCS5/7等)
    # padding_len = plain_bytes[-1]
    # plain_bytes = plain_bytes[:-padding_len]
    return plain_bytes

# 3. 主解密流程
def main():
    # 这些值需要从题目或你的分析中获取
    final_ciphertext = bytes.fromhex(\"这里是题目给的最终密文十六进制字符串\")
    aes_key = bytes.fromhex(\"你分析出的AES密钥\")
    xxtea_key = bytes.fromhex(\"你分析出的XXTEA密钥\")
    custom_sbox = bytes.fromhex(\"你dump出的256字节自定义S盒十六进制字符串\")
    
    print(\"[*] Starting decryption...\")
    # 第一步:魔改AES解密
    intermediate_data = custom_aes_decrypt(final_ciphertext, aes_key, custom_sbox)
    # 第二步:XXTEA解密
    flag_bytes = xxtea_decrypt(intermediate_data, xxtea_key)
    # 第三步:输出Flag
    try:
        flag = flag_bytes.decode('utf-8')
        print(f\"[+] Flag found: {flag}\")
    except UnicodeDecodeError:
        print(f\"[+] Raw flag bytes (hex): {flag_bytes.hex()}\")
        print(\"Flag might not be a simple string, check the bytes.\")

if __name__ == \"__main__\":
    main()

7. 常见问题与实战调试技巧

在实际操作中,你几乎一定会遇到各种问题。下面是我踩过坑后总结的一些经验。

7.1 静态分析中的障碍与应对

  • 问题:代码混淆严重,类名方法名无意义。
    • 技巧 :不要纠结于名称。关注方法体的逻辑和常量。搜索特征常量(如 0x9E3779B9 , 0x63 (标准S盒第一个值))来定位关键函数。使用“字符串搜索”功能,查找可能泄露信息的日志字符串。
  • 问题:算法逻辑被分散到多个类或方法中。
    • 技巧 :在Jadx中,对关键变量或参数使用“查找用例”功能,追踪其在整个项目中的传递路径,绘制出简化的调用图。

7.2 动态调试中的陷阱与解决

  • 问题:Frida脚本注入失败或应用崩溃。
    • 检查点
      1. Frida-server版本与桌面客户端版本是否匹配。
      2. 应用是否有反调试或反注入检测?可以尝试使用 objection android disable 命令禁用一些常见检测,或者使用 frida -U -f com.package.name --no-pause 在应用启动前注入。
      3. Hook的函数签名是否完全正确?重载(overload)是否选对?使用 Java.choose 或枚举类的方法来确认。
  • 问题:Native层Hook时,函数地址找不到。
    • 技巧 :不要只依赖导出函数名。使用 Module.enumerateImports() Module.enumerateExports() 列出所有导入导出函数。对于未导出的函数,可以通过特征码(字节序列)在内存中搜索。或者,先Hook一个已知的JNI函数(如 Java_com_example_MainActivity_encrypt ),然后在其内部回溯调用栈,找到真正的算法函数。

7.3 算法还原与验证阶段的难题

  • 问题:自己实现的解密脚本输出乱码,无法验证。
    • 排查步骤
      1. 字节序 :这是最常见的问题!Java/Android默认是 大端序(Big-Endian) ,而很多C语言实现和标准算法描述使用 小端序(Little-Endian) 。在 bytesToInts intsToBytes 转换时,必须确认与目标代码保持一致。用Frida Hook打印出中间整数数组的值,与你脚本中转换后的值对比。
      2. 填充模式 :AES是块加密,需要对明文进行填充。常见的PKCS5/PKCS7填充在解密后需要去除。确认题目使用的填充方式,并在解密后正确去除填充。
      3. 魔改点遗漏 :是否只改了S盒,但忽略了密钥扩展?是否轮数不对?用Frida在Native层加密函数的入口和出口打印状态矩阵(State)的中间值,与你本地算法实现的每一步输出进行比对,定位第一个出现差异的步骤。
      4. 密钥错误 :双重确认获取的密钥是否正确。是否在传递过程中经过了编码(如Base64)或二次转换?

7.4 效率提升心得

  • 先动后静 :对于复杂算法,有时直接动态Hook获取输入输出和中间值,比完全静态分析更快。用已知的明文去触发加密,观察输出,可以快速验证对算法结构的猜想。
  • 单元测试思维 :将大问题分解。先单独验证XXTEA的解密是否正确(用题目可能提供的中间数据或自己加密一个测试数据)。再单独验证魔改AES的解密。最后再串联。
  • 善用搜索引擎和社区 :标准算法(如XXTEA)的实现代码网上很多。但 切勿直接复制 ,一定要理解其代码逻辑,并适配题目可能存在的细微改动(如轮数、字节序)。

逆向工程就像侦探破案,需要耐心、细致的观察和逻辑推理。这道从XXTEA到魔改AES的题目,完美地串联了移动逆向的多个核心技能点。当你成功提取出Flag的那一刻,所有的调试和排查都会变得值得。最重要的是,通过这个过程积累的经验,能让你在面对真实世界中更加复杂的保护方案时,拥有清晰的破解思路和扎实的动手能力。

代码下载链接: https://pan.quark.cn/s/a4b39357ea24 第 一 章 概述 1-1 简述计算机程序设计语言的发展阶段。 解: 自从计算机诞生以来,程序设计语言经历了从机器语言、汇编语言到高级语言的演变过程,C++语言作为一种面向对象的编程语言,也属于高级语言范畴。 1-2 面向对象的编程语言具备哪些特性? 解: 面向对象的编程语言与传统的编程语言有着本质的区别,其设计初衷是为了更直观地模拟现实世界中存在的事物及其相互关系。这类编程语言将客观事物视为具有属性和行为的对象,通过抽象方法提取出同一类对象的共同属性(静态特征)和行为(动态特征),从而构建类。借助类的继承与多态机制,能够便捷地实现代码复用,显著缩短软件开发周期,并确保软件风格的一致性。因此,面向对象的编程语言使得程序能够较为准确地反映问题域的本质,软件开发人员可以运用人类惯用的思维模式进行开发工作。C++语言是目前应用最为广泛的面向对象编程语言。 1-3 结构化程序设计方法是什么?这种方法有哪些优势和不足? 解: 结构化程序设计的核心思想是自顶向下、逐步求精;其程序结构按照功能划分为多个基本模块;各模块之间的关联尽可能简化,在功能上保持相对独立性;每个模块内部均由顺序、选择和循环三种基本结构构成;模块化实现的具体途径是利用子程序。结构化程序设计由于采用模块分解与功能抽象,自顶向下、分而治之的策略,从而有效地将一个较为复杂的程序系统设计任务分解成许多易于管理和处理的子任务,便于开发与维护。 尽管结构化程序设计方法具备诸多优点,但它本质上仍是一种面向过程的程序设计方法,将数据与处理数据的操作分离为相互独立的实体。当数据结构发生变化时,所有相关的处理过程都需要进行相应的调整,每一种...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值