当前位置: 首页 > news >正文

Android应用逆向实战:从抓包到so层算法还原全解析

1. 项目概述与核心价值

最近在分析一个主流房产信息平台“某居客”的移动端应用,目标很明确:完成从APP端网络请求抓包到关键接口逆向,最终深入其核心的so层(共享库)进行算法解析的全过程。这听起来像是一个标准的移动安全分析流程,但实际操作中,你会发现从抓包开始就布满了“雷区”,而深入到so层后,更是对逆向工程基本功和耐心的一次全面考验。这个项目对于从事移动应用安全研究、风控策略对抗、数据合规审计,甚至是希望理解大型APP如何构建其客户端安全体系的开发者而言,都具有很高的参考价值。它不仅仅是一次技术演练,更是一次对现代APP混合防护策略(HTTPS证书绑定、代码混淆、Native层加密)的实战拆解。

简单来说,我们要做三件事:第一,成功捕获到APP发出的所有网络请求,特别是那些包含核心业务数据(如房源列表、详情、用户信息)的接口;第二,逆向分析这些接口的调用逻辑、参数构造和签名算法,理解其通信协议;第三,也是最硬核的部分,定位并逆向实现那些被转移到Android so库(C/C++层)中的关键加密或校验函数。整个过程,你会遇到证书锁定(SSL Pinning)、各种代码混淆、反调试,以及so层的控制流平坦化等保护手段。接下来,我就以一个实战者的视角,带你一步步拆解这个“某居客”,分享其中踩过的坑和总结出的有效技巧。

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

工欲善其事,必先利其器。一个稳定、高效的工具链是成功的一半。对于移动端抓包与逆向,工具的选择需要兼顾易用性、功能强大和对抗检测的能力。

2.1 抓包环境搭建

抓包是整个分析的入口。对于HTTPS流量,传统的方法(如设置系统代理)在遇到证书绑定时会立刻失效。“某居客”这类大型APP几乎必然使用了证书绑定。

我的方案是:使用一部已Root的Android真机 + 模块化注入工具。

  • 设备选择:我选择了一台闲置的Android手机,刷入了Magisk获取Root权限。模拟器(如Genymotion、夜神)在对抗高级反调试和检测虚拟环境时容易出问题,真机更稳定。
  • 核心工具FridaJustTrustMe模块(或类似功能的Xposed模块,如TrustMeAlready)。Frida是一个动态插桩框架,我们可以编写脚本,在运行时Hook住APP的证书验证逻辑,使其接受我们抓包工具(如Burp Suite或Charles)签发的证书。
  • 抓包代理Burp Suite Professional。它的Repeater、Intruder、Decoder等功能在后续参数分析和爆破时非常有用。社区版功能受限,但基础抓包足够。

具体搭建步骤:

  1. 在电脑上安装Burp Suite,配置好代理(如127.0.0.1:8080),并导出Burp的CA证书。
  2. 在Root后的手机上,安装Magisk模块管理器,然后安装MagiskFrida模块(或手动部署Frida-server到手机并运行)。
  3. 安装Xposed框架(如LSPosed),并安装JustTrustMe模块。这个模块的作用是禁用或绕过大量APP的证书绑定逻辑。
  4. 将Burp的CA证书安装到手机的系统证书目录(/system/etc/security/cacerts/),这需要Root权限。这一步确保了手机系统信任Burp。
  5. 配置手机Wi-Fi代理,指向运行Burp的电脑IP和端口。

注意:仅仅安装用户证书是不够的,Android 7.0以上,APP默认不信任用户安装的证书,必须将证书放入系统证书目录。这是第一个常见的坑。

2.2 逆向分析环境搭建

成功抓到包后,我们需要静态和动态分析APP本身。

  • 反编译与静态分析

    • Jadx-GUI:这是我首选的Java反编译工具。它可以直接打开APK文件,将Dex代码反编译成可读性相当高的Java代码。用于快速浏览APP结构、查找关键类和方法。
    • Apktool:用于反编译APK资源文件(如图片、布局XML)和AndroidManifest.xml,对于理解APP权限和组件结构很重要。
    • JEBGhidra:更强大的反编译器。当Jadx遇到复杂混淆处理不佳时,JEB的反编译效果往往更好。Ghidra则是免费的强大逆向工程框架,对Native层(so)的分析支持极佳。
  • 动态调试与Hook

    • Frida:不仅是绕过SSL的利器,更是动态分析的瑞士军刀。我们可以编写Python脚本,主动调用APP的方法、修改函数返回值、打印调用栈和参数,这对于快速定位加密函数入口至关重要。
    • Objection:基于Frida的命令行工具,可以快速实现一些常见操作,如禁用SSL绑定、内存搜索、列出类和方法等,适合快速侦查。
  • So层分析

    • Ghidra/IDA Pro:静态分析so文件的不二之选。Ghidra免费且功能强大,支持反编译C/C++代码。IDA Pro是行业标准,交互和插件生态更成熟。
    • Frida:同样可以用于Hook so层导出的Native函数,动态获取函数的输入输出,辅助静态分析。
    • radare2:命令行下的强大逆向工具,适合喜欢终端操作的研究者。

这套组合拳覆盖了从网络流量捕获、Java层逻辑分析到Native层算法还原的全链路需求。

3. 抓包实战与证书绑定绕过

环境准备好后,启动“某居客”APP,开始抓包。不出所料,最初的尝试可能一无所获,APP提示网络错误或直接闪退。这说明它检测到了代理或证书异常。

3.1 初步对抗与问题定位

首先,检查Burp Suite是否能看到任何HTTP/HTTPS请求。如果完全看不到,可能是APP使用了透明代理检测或证书绑定(SSL Pinning)。我们之前安装的JustTrustMe模块就是为了通用性地绕过证书绑定。如果安装了该模块后仍然抓不到包,可能有以下原因:

  1. APP使用了自定义的HTTP库或Socket通信:有些APP会使用OkHttp的自定义TrustManager,或者直接使用Socket进行非HTTP协议通信。JustTrustMe主要针对系统证书验证逻辑,对高度自定义的实现可能失效。
  2. APP具备代理检测功能:APP会读取系统代理设置,如果发现设置了代理,则拒绝发送请求或走其他通道。
  3. So层实现的绑定:最棘手的情况,证书验证逻辑被编译到了so文件中,Java层只是一个壳子。

应对策略:

  • 使用Frida脚本进行精准Hook:写一个Frida脚本,Hook常见的证书验证类,如OkHttpClient.BuildersslSocketFactoryhostnameVerifier,或者TrustManagercheckClientTrusted/checkServerTrusted方法。通过打印堆栈,可以找到APP具体在哪里进行了证书验证。
    // 示例Frida脚本:Hook OkHttp的证书验证 Java.perform(function() { var OkHttpClient_Builder = Java.use('okhttp3.OkHttpClient$Builder'); OkHttpClient_Builder.sslSocketFactory.implementation = function(sslSocketFactory, trustManager) { console.log(\"[*] OkHttpClient.Builder.sslSocketFactory() called!\"); console.log(Java.use(\"android.util.Log\").getStackTraceString(Java.use(\"java.lang.Exception\").$new())); // 返回一个接受所有证书的Factory // ... 这里可以替换成自定义的信任所有证书的Factory return this.sslSocketFactory(sslSocketFactory, trustManager); }; });
  • 使用ProxyDroid等工具进行全局代理:这类工具可以在VPN层面实现代理,绕过APP层的代理检测。但可能会与抓包工具的证书安装产生冲突,需要仔细配置。
  • 针对So层绑定:这需要进入下一步的逆向分析。可以先通过抓包工具看到TCP连接建立但SSL握手失败,或者APP直接崩溃,来间接判断。

3.2 成功捕获流量

经过Frida脚本的辅助Hook和JustTrustMe模块的作用,我最终成功在Burp Suite中看到了“某居客”的HTTPS流量。关键接口通常以.json或纯路径形式出现,例如/house/list/detail/info等。此时,需要重点关注:

  1. 请求头(Headers):特别是AuthorizationX-TokenUser-Agent(可能被自定义)、X-SignX-Timestamp等字段,这些往往是身份验证和请求签名的关键。
  2. 请求参数(Params/Body):查看GET参数或POST的JSON/form-data数据。注意是否有看似随机的字符串参数,如signnonceencryptedData等。
  3. 响应数据(Response):观察数据是明文的JSON,还是经过编码或加密的。如果是加密的,那么解密逻辑很可能在so层。

实操心得:不要一上来就分析所有接口。先进行常规的APP操作(如浏览首页、搜索房源、查看详情),在Burp中过滤出与这些操作相关的、数据量较大的接口。通常,列表接口和详情接口是分析签名和加密算法的突破口,因为它们参数相对固定,易于对比分析。

4. Java层接口逻辑逆向

抓到关键接口(例如/api/v1/house/search)后,下一步就是找到APP中构造这个请求的代码位置。

4.1 定位关键代码

常用的定位方法有几种:

  1. 关键词搜索法:在Jadx中,直接搜索接口路径的一部分,如/house/search。但现代APP通常会将接口域名和路径拼接,可能搜索不到。可以搜索域名,或者搜索参数名,如cityIdkeyword
  2. 调用栈分析法:这是更高效的方法。在Frida脚本中,Hook住网络请求库的发送函数,例如okhttp3.Call.execute()HttpURLConnection.connect(),然后在调用时打印堆栈信息(Log.getStackTraceString(new Exception()))。堆栈会清晰地告诉你这个请求是从哪个类、哪个方法发起的。
    Java.perform(function() { var OkHttpClient = Java.use('okhttp3.OkHttpClient'); OkHttpClient.newCall.implementation = function(request) { var url = request.url().toString(); if (url.indexOf(\"ke.com\") != -1) { // 过滤特定域名 console.log(\"[*] Intercepting request to: \" + url); console.log(\"Stack Trace:\\n\" + Java.use(\"android.util.Log\").getStackTraceString(Java.use(\"java.lang.Exception\").$new())); } return this.newCall(request); }; });
  3. 参数回溯法:如果某个请求有一个特征明显的参数值(比如一个特定的时间戳或ID),可以在Jadx中搜索这个值的字符串或数字,可能直接定位到生成它的代码附近。

通过上述方法,我定位到了“某居客”请求构造的核心类,通常命名为XXXApiServiceXXXHttpClientXXXNetworkManager

4.2 分析参数构造与签名逻辑

找到发起请求的类和方法后,在Jadx中查看其反编译的代码。你需要关注:

  • BaseUrl和路径拼接:接口的完整URL是如何生成的。
  • 公共参数添加:哪些参数(如设备IDdeviceId、版本号appVersion、渠道channel)是在拦截器(Interceptor)或方法开头统一添加的。
  • 签名(Sign)生成:这是重中之重。寻找名为getSigngenerateSignaturecalculateSign的方法。签名算法通常是对所有请求参数(有时包括请求体、时间戳、一个固定密钥secret)按照一定规则(如字典序排序)拼接后,再进行某种哈希(如MD5、SHA256)或HMAC运算。
    • 关键线索:在代码中搜索MD5SHA-256HmacMessageDigestMac等关键词。
    • 动态验证:用Frida Hook你怀疑的签名方法,打印其输入(参数Map)和输出(sign字符串)。然后在Burp Repeater中,用同样的参数手动计算一次签名,看是否匹配。这是验证你分析是否正确的最直接方法。

踩坑记录:我最初发现一个generateSign方法,它接收一个Map<String, String>参数,返回一个字符串。Hook后发现它内部只是做了一次简单的MD5。但用这个算法计算出的签名和服务端验证不通过。后来通过更细致的堆栈分析发现,在调用generateSign之前,参数Map已经被另一个方法处理过,添加了一个从so层获取的dynamicKey。这就是典型的Java层与Native层协作:Java层负责收集和组装参数,核心的密钥或部分计算逻辑放在so层以增加逆向难度。

5. So层逆向分析与算法还原

当发现关键参数(如签名所需的密钥secret、加密数据的IVkey)是通过System.loadLibrary加载的so库中的Native方法获取时,战斗才真正开始。

5.1 定位与提取So文件

  1. 定位Native方法:在Jadx中,搜索native关键字,或者查找调用System.loadLibrary(\"xxx\")的代码。通常会在一个static块中加载。找到声明Native方法的类,如NativeSignHelper
  2. 提取So文件:APK本质是ZIP包,解压后在其lib/目录下(可能有armeabi-v7aarm64-v8a等子目录)找到对应的libxxx.so文件。将目标架构(通常选arm64-v8a)的so文件复制出来。

5.2 静态分析入口点

使用Ghidra或IDA Pro加载so文件。首先寻找导出函数(Exported Functions)。JNI(Java Native Interface)函数的命名有固定格式:Java_包名_类名_方法名。例如,Java_com_ke_app_util_NativeSignHelper_getEncryptKey

  • 在Ghidra中,可以在Symbol Tree窗口的Functions里过滤Java_来快速定位。
  • 找到目标函数后,开始分析其汇编或反编译后的C代码。

初始挑战:so文件很可能经过了混淆。函数名可能被抹去(只剩下地址),控制流可能被平坦化(Control Flow Flattening),并穿插了大量无用指令(垃圾代码)。这会让反编译出来的代码逻辑支离破碎,难以阅读。

5.3 动态辅助与算法追踪

静态分析受阻时,动态调试是破局关键。

  1. Frida Hook Native函数:我们可以直接Hook so层导出的JNI函数。

    // Hook Native方法,打印输入输出 Interceptor.attach(Module.findExportByName(\"libnative-sign.so\", \"Java_com_ke_app_util_NativeSignHelper_getSign\"), { onEnter: function(args) { // args[0]是JNIEnv*, args[1]是jclass/jobject, args[2]...是Java传入的参数 this.param1 = args[2]; // 假设第一个参数是jstring var param1Str = Java.vm.getEnv().getStringUtfChars(this.param1, null).readCString(); console.log(\"[Native] getSign called with param: \" + param1Str); }, onLeave: function(retval) { // retval是返回值,可能是jstring var retStr = Java.vm.getEnv().getStringUtfChars(retval, null).readCString(); console.log(\"[Native] getSign returned: \" + retStr); } });

    通过Hook,我们可以确认这个Native函数的输入输出是什么,验证它是否是我们寻找的签名或密钥生成函数。

  2. 使用Frida Stalker追踪指令:对于内部函数,可以使用Frida的Stalker功能追踪CPU指令执行流,虽然数据量大,但有时能发现关键调用或循环。

  3. 结合静态分析:用动态获取到的具体输入输出值(例如,输入字符串“test”,输出“a1b2c3d4”),在静态反编译的代码中设置“断点”或进行数据流跟踪。在Ghidra中,你可以搜索这些常量字符串或数值,可能定位到关键的比较或计算代码块。

5.4 还原算法逻辑

在“某居客”的案例中,我追踪到一个关键的Native函数,它接收时间戳和设备ID的一部分,经过一系列位运算、查表(S-Box)和循环后,生成一个16字节的dynamicKey。这个dynamicKey会与Java层拼接好的参数字符串组合,再进行一次HMAC-SHA256运算,最终得到提交的sign

还原步骤:

  1. 理解函数原型:明确输入参数的数量和类型(int, string, byte array),以及返回值类型。
  2. 梳理主流程:忽略混淆的垃圾代码和虚假分支,聚焦在那些对输入参数进行实际操作的指令上。寻找常见的加密算法特征,如:
    • AESAES_encrypt/AES_decrypt函数调用,或者明显的SubBytesShiftRowsMixColumnsAddRoundKey操作序列(可通过查找固定的轮常数Rcon来辅助识别)。
    • MD5/SHA:初始化魔数(如MD5的0x67452301等),以及固定的循环位移操作。
    • Base64:编码表(ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/)和=填充。
    • TEA/XXTEA:简单的位运算、加法和异或,配合一个魔数(如0x9E3779B9)。
  3. 模拟实现:用Python或C语言,按照分析出的逻辑重写算法。这是一个反复验证的过程:用相同的输入,你的代码输出必须与Hook抓取的输出完全一致。
  4. 验证:将还原的算法用于构造新的请求参数,用Burp Repeater发送,看服务端是否正常响应。这是最终的验收标准。

注意事项:So层分析极其耗时,需要耐心。有时算法是标准算法的轻微变种(修改了S盒、初始向量IV,或者增加了额外的置换步骤)。可以尝试将so文件中的常量数组提取出来,与标准算法(如AES的S盒)进行对比,能快速识别算法类型。

6. 完整请求构造与复现

在成功还原了Java层的参数组装逻辑和so层的核心加密/签名算法后,就可以脱离APP,用任何编程语言(如Python)来模拟发送请求了。

6.1 构建请求流程

一个完整的自动化请求脚本通常包含以下步骤:

  1. 设备信息模拟:生成或固定deviceIdimei(可随机生成符合格式的)、androidId等设备标识。
  2. 获取动态密钥:调用还原的Native算法函数(已用Python实现),传入必要的种子(如时间戳),计算出本次请求使用的dynamicKeysecret
  3. 组装业务参数:根据要查询的内容(如城市、页码、筛选条件)构造参数字典。
  4. 生成签名
    • 将业务参数、公共参数、动态密钥按照APP的规则(如按Key字典序排序后拼接成k1=v1&k2=v2...的格式)拼接成待签名字符串。
    • 使用还原的签名算法(如HMAC-SHA256)计算签名,并将签名值加入请求参数。
  5. 发送请求:设置好必要的请求头(如User-AgentContent-TypeAuthorization(如果有Token)),使用requests库发送HTTP请求。
  6. 处理响应:解析返回的JSON数据。如果数据被加密,还需调用还原的解密算法进行解密。

6.2 Python实现示例片段

import hashlib import hmac import time import requests # 1. 还原的So层算法:生成dynamic_key def native_get_dynamic_key(timestamp): # 这里是逆向还原的算法,可能包含位运算、查表等 seed = str(timestamp)[-6:] # ... 复杂的计算过程 ... dynamic_key = \"a1b2c3d4e5f67890\" # 示例输出 return dynamic_key # 2. 还原的签名算法 def generate_sign(params_dict, dynamic_key): # 步骤1: 参数排序并拼接 sorted_params = \'&\'.join([f\"{k}={params_dict[k]}\" for k in sorted(params_dict.keys())]) # 步骤2: 拼接动态密钥 string_to_sign = sorted_params + \"&key=\" + dynamic_key # 步骤3: 计算HMAC-SHA256 (假设这是还原出的算法) sign = hmac.new(dynamic_key.encode(\'utf-8\'), string_to_sign.encode(\'utf-8\'), hashlib.sha256).hexdigest() return sign # 3. 构造请求 def fetch_house_list(city_id, page): timestamp = int(time.time() * 1000) dynamic_key = native_get_dynamic_key(timestamp) base_params = { \"cityId\": city_id, \"page\": str(page), \"pageSize\": \"20\", \"timestamp\": str(timestamp), \"deviceId\": \"模拟的设备ID\", \"appVersion\": \"9.0.0\" } sign = generate_sign(base_params, dynamic_key) base_params[\"sign\"] = sign headers = { \"User-Agent\": \"Dalvik/2.1.0 (Linux; U; Android 10; MI 8 Build/QQ3A.200805.001)\", \"Content-Type\": \"application/x-www-form-urlencoded\" } response = requests.post(\"https://app.api.ke.com/api/v1/house/list\", data=base_params, headers=headers) return response.json() # 使用 if __name__ == \"__main__\": data = fetch_house_list(\"110000\", 1) print(data)

7. 常见问题排查与对抗升级

在整个过程中,你会遇到各种问题。以下是一些常见问题及解决思路的速查表:

问题现象可能原因排查与解决思路
抓不到任何包1. 代理未正确设置或未生效。
2. APP检测并禁用了代理。
3. 使用了纯Socket或自定义协议。
1. 检查手机代理设置和Burp监听端口。
2. 使用Frida HookSystem.getProperty(\"http.proxyHost\")等检测方法并返回空。
3. 使用tcpdump或Wireshark在手机上抓取原始网络包分析。
HTTPS请求显示为Tunnel to证书绑定(SSL Pinning)生效。1. 确认Burp CA证书已安装到系统证书目录。
2. 使用JustTrustMe等Xposed模块。
3. 编写Frida脚本Hook证书验证逻辑。
APP启动后闪退1. 检测到Root环境。
2. 检测到调试器或注入工具(如Frida)。
3. So层有反调试。
1. 使用Magisk Hide隐藏Root。
2. 使用Frida的-f参数以spawn方式启动APP,而非attach
3. 使用objectionandroid root disableandroid anti-root disable命令。
4. 分析so中的ptracefork等反调试函数并用Frida绕过。
签名一直无效1. 签名算法分析错误。
2. 遗漏了某个参数(如请求体、Cookie)。
3. 密钥或盐值是动态的,未正确获取。
1. 用Frida Hook多个可能的签名函数,对比输入输出。
2. 对比多个成功请求的参数差异,找出规律。
3. 检查网络请求拦截器,看是否有全局添加的参数。
4. 确认从so层获取的动态值是否正确。
So层函数无法Hook1. 函数符号被剥离(stripped)。
2. 函数是静态的,未导出。
3. So文件有完整性校验。
1. 使用函数地址而非符号名进行Hook:Module.findBaseAddress(\"libxxx.so\").add(0x1234)
2. 在JNI调用的Java层方法上进行Hook,间接追踪。
3. 使用内存扫描找到函数特征码。
反编译代码逻辑混乱代码被混淆(ProGuard等)。1. 结合动态调试,关注实际执行的代码流。
2. 根据字符串常量、资源ID等线索推测类和方法名。
3. 尝试使用更强大的反编译器(如JEB)。

对抗升级的思考:作为分析方,我们也要意识到,我们的技术手段也在推动防护技术的升级。例如,越来越多的APP开始使用白盒加密(将密钥与算法深度融合)、虚拟机保护(VMP,将关键代码转换为自定义指令集在虚拟机中执行)、服务器协同(关键参数由服务器下发,每次不同)等更高级的方案。面对这些,逆向的难度和成本会指数级上升,往往需要结合静态分析、动态调试、硬件调试乃至侧信道分析等多种手段。这也意味着,在合规的前提下进行安全研究,需要持续学习和更新知识库。

http://www.jsqmd.com/news/1045521/

相关文章:

  • DDrawCompat完全指南:如何让Windows 11上的老游戏流畅运行
  • 大连购宠避雷实录:实测 10 家猫犬舍,3000㎡合规基地终结星期宠​ - 同城宠物优选基地
  • 2026最新测评:16款降AIGC软件测评,论文安全过关就靠它!
  • 深入解析MC68HC908GR8/GR4 SIM模块:复位管理与低功耗模式实战
  • 深圳购宠避雷实录:实测 10 家猫犬舍,6 区连锁合规基地终结星期宠​ - 同城宠物优选基地
  • 2026山福镇空调回收口碑推荐榜单 - 品牌排行榜
  • 产品设计误区:功能越多越好?聚焦核心才是关键!
  • 深入解析MC9S12 BDM硬件调试模块:原理、命令与实战应用
  • 洛雪音乐终极音源指南:一站式获取全网无损音乐的完整解决方案
  • 深入解析恩智浦MR2001V:W波段四通道VCO芯片的设计与应用
  • Python自动化抢票实战:大麦网高效抢票脚本深度解析
  • 2026长江路街道专业的空调维修服务商口碑推荐榜 - 品牌排行榜
  • NXP 12XS6D4智能高边开关:SPI控制、PWM调光与多重保护机制详解
  • 2026年双碳业务认证机构有哪些?行业权威盘点 - 品牌排行榜
  • 无惧潮湿盐雾,长期高频使用不锈钢防火门省心之选
  • 终极指南:如何使用 nunif iw3 将普通2D视频转换为沉浸式VR 3D体验
  • Display Driver Uninstaller深度清理方案:显卡驱动残留问题的终极解决方案(2024版)
  • 上海正规靠谱空调维修公司推荐,全城优选上海迪迅通制冷设备 - 星际AI
  • Rust Trait 对象与泛型的性能比较
  • SPI协议深度解析:从时钟相位到错误处理,以MC68HC908GR8为例
  • 太原沙发翻新哪家靠谱?2026本地正规翻新门店 - 我叫一
  • 终极指南:用 dayspan-vuetify 快速构建智能日历应用
  • 嵌入式系统热设计与功耗分析:从LPC435x数据手册到可靠硬件设计
  • 泰州十家猫犬舍实测调查:3000㎡合规基地成行业标杆,警惕这些星期宠重灾区​ - 同城宠物优选基地
  • python: Fan-In Pattern Fan-In
  • ComfyUI-LTXVideo进阶攻略:从入门到精通的AI视频创作工具箱
  • 2026深圳漏水检测维修精选优质服务商TOP5推荐!卫生间漏水/厨房漏水/屋顶天花板漏水/阳台漏水/地下室漏水防水补漏检测维修-正规防水补漏公司优选口碑榜测评推荐 - 即刻修防水
  • 地理坐标转换实战:将全球经度数据从0-360映射到-180-180
  • Adobe-GenP:释放创意生产力的智能激活解决方案
  • ATE测试—新手入门学习(二)【6-10】