前言

为学习IOS知识并加深理解,故通过IOS漏洞靶场来直接体现IOS环境中的常见漏洞。

环境准备

iPhone X ios 13.4.1 已越狱

MacOS Big Sur 11.0 Bate 虚拟机

DVIA-v2 IOS 漏洞靶场app

grapefruit、objection

#pip3 install frida、pip3 install frida-tools、sudo npm install -g igf

注:工具和客户端的frida版本都是16.0.1

part1-Local Data Storage

任务:寻找存储在本地的敏感信息

Plist

PLIST 文件是一个特殊的文本文件,其中包含属性列表格式的数据。macOS、iOS 和 iPadOS 应用程序使用该文件以具有XML结构的键值格式存储设置和其他数据。

输入账号密码,数据会被保存到plist文件中

objection -g DVIA-v2 explore
env

使用objection定位app使用的目录,env 列出app当前环境(使用的目录),查看Documents目录下是否有plist文件

默认情况下, 每个沙盒含有1个应用程序包和三个文件夹: Documents, Library和tmp. 因为沙盒机制, 应用只能在这几个目录读写文件.

MyApp.app

应用程序包, 这里面存放的是应用程序文件, 包括资源文件和可执行文件.

Documents

应用程序在运行时生成的一些需要长久保存的数据(比如: 个人设置等信息), 通过iTunes, iCloud备份时,会备份这个目录下的数据, 此目录下保存相对重要的数据.

Library/Caches

从网络上下载的文件或数据(如: 音乐缓存, 图片缓存等) , 此目录下的数据某些情况下会自动删除, 需要程序员手动清除该目录下的数据. ITunes, iCloud备份时不会备份此目录下的数据.一般用于存储体积不大, 不需要备份的非重要资源数据.

tmp

保存应用运行时产生的一些临时数据,应用程序退出,系统磁盘空间不够,手机重启时,都会自动清除该目录的数据。无需程序员手动清除该目录中的数据.iTunes、iCloud备份时,不会备份次目录。

发现明文存储的账号密码

使用grapefruit查看

NSUserDefaults

NSUserDefaults 是一个单例,它可以用于数据的永久存储,适合存储轻量级的本地数据。

NSUserDefaults 存储的位置为应用的 /Library/Preferences/目录下

cd /var/mobile/Containers/Data/Application/733E2124-8A28-4F84-A7/Preferences
ios plist cat com.highaltitudehacks.DVIAswiftv2.plist

使用grapefruit查看

Keychain

Keychain Services 是 macOS 和 iOS 都提供一种安全的存储敏感信息的工具,比如,网络密码:用户访问服务器或者网站,通用密码:用来保存应用程序或者数据库密码.与此同时,用于认证的证书密钥和身份信息,也可以存储在Keychain中,Keychain Services 的安全机制保证了存储这些敏感信息不会被窃取。简单说来,Keychain 就是一个安全容器。

ios keychain dump

使用grapefruit查看,十六进制解密

Core Data

Core Data是苹果官方提供的管理数据层对象的框架,它提供了对象—关系映射(ORM)的功能,即能够将Objective-C对象转化成数据,保存在SQLite数据库文件中,也能够将保存在数据库中的数据还原成Objective-C对象。在此数据操作期间,不需要编写任何SQL语句。Core Data针对对象生命周期,以及持久化的对象图管理(object graph management)中的一些常见问题提供了解决方案。CoreData的主要任务是负责数据更改的管理、序列化到磁盘、最小化内存占用以及查询数据。CoreData通常能有效减少50%~70%模型层的代码量。

cd /var/mobile/Containers/Data/Application/9C3F7322-BB5B-4727-922C-51F238A4893F/Library/Application\ Support/
cp Model.sqlite ../
scp -P 1234 root@127.0.0.1:/var/mobile/Containers/Data/Application/9C3F7322-BB5B-4727-922C-51F238A4893F/Library/Model.sqlite .
sqlite3 Model.sqlite
.tables
select * from ZUSER;这里有个坑,到应用下复制sql文件到Mac上查看内容的时候是空的,而使用grapefruit工具查看的时候是有数据的

Webkit Caching

使用UrWebView加载网页的应用程序通常使用缓存来加快响应时间。在某些情况下,它们可以缓存包含机密信息的响应。您的任务是找到存储请求和响应的位置–应用程序沙箱。提示:在数据库中使用cf_url缓存数据端口的表。

在app目录中只发现了数据库文件,里面没有发现一些有用的东西。可能是开放式命题,在app内打开的链接会生成缓存存储在数据库中,如果缓存中包含敏感信息,则会造成信息泄露。

Realm

需要特定工具打开,先留着

Couchbase Lite

CouchBase是一款开源的,分布式的nosql数据库,主要用于分布式缓存和数据存储领域。能够通过manage cache提供快速的亚毫米级别的k-v存储操作,并且提供快速的查询和其功能强大的能够指定SQL-like查询的查询引擎。

但是这边不知道为什么写不进去,没有内容

YapDatabase

YapDatabase是以 键值对 的形式储存数据的,和NSUserDefault很像。但是Yap是数据库。
Yap里有 Collection,key。key对应value(可以储存任意对象)。而Collection可以理解为命名空间。

part2-Jaibreak Detection

任务:绕过越狱检测

Test 1

frida-trace -U -i "*jail*" DVIA-v2

使用frida查看相关的越狱函数,点击Test 1事件,触发 _T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest1TappedyypF() 方法,IDA反汇编查看相应代码,调用了几个函数,但是没什么内容,猜测应该是某个函数的返回值决定是否越狱

重新查看汇编代码,LDR指令中一般会在寄存器中存贮一些数据,可以通过配合BL指令来查看跳转函数

__text:0000000100192C10 SUB             SP, SP, #0x60
__text:0000000100192C14 STP             X20, X19, [SP,#0x50+var_10]
__text:0000000100192C18 STP             X29, X30, [SP,#0x50+var_s0]
__text:0000000100192C1C ADD             X29, SP, #0x50
__text:0000000100192C20 STUR            X0, [X29,#var_18]
__text:0000000100192C24 STUR            X20, [X29,#var_20]
__text:0000000100192C28 STR             X20, [SP,#0x50+var_28]
__text:0000000100192C2C STR             X0, [SP,#0x50+var_30]
__text:0000000100192C30 BL              __T07DVIA_v213DVIAUtilitiesCMa
__text:0000000100192C34 ADRP            X20, #_swift_isaMask_ptr@PAGE
__text:0000000100192C38 LDR             X20, [X20,#_swift_isaMask_ptr@PAGEOFF]
__text:0000000100192C3C LDR             X30, [X0,#0x58]
__text:0000000100192C40 LDR             X8, [SP,#0x50+var_28]
__text:0000000100192C44 LDR             X9, [X8]
__text:0000000100192C48 LDR             X20, [X20]
__text:0000000100192C4C AND             X9, X9, X20
__text:0000000100192C50 LDR             X9, [X9,#0x80]
__text:0000000100192C54 MOV             X20, X8
__text:0000000100192C58 STR             X0, [SP,#0x50+var_38]
__text:0000000100192C5C STR             X30, [SP,#0x50+var_40]
__text:0000000100192C60 BLR             X9
__text:0000000100192C64 LDR             X8, [SP,#0x50+var_28]    #将[SP,#0x50+var_28]的值保存在寄存器x8中,值为0x1,对应true
__text:0000000100192C68 STR             W0, [SP,#0x50+var_44]
__text:0000000100192C6C MOV             X0, X8
__text:0000000100192C70 BL              _objc_retain
__text:0000000100192C74 LDR             X8, [SP,#0x50+var_28]
__text:0000000100192C78 LDR             X9, [SP,#0x50+var_40]
__text:0000000100192C7C LDR             W10, [SP,#0x50+var_44]
__text:0000000100192C80 AND             W11, W10, #1
__text:0000000100192C84 STR             X0, [SP,#0x50+var_50]
__text:0000000100192C88 MOV             X0, X11
__text:0000000100192C8C MOV             X1, X8
__text:0000000100192C90 LDR             X20, [SP,#0x50+var_38]   #修改这个值也能绕过
__text:0000000100192C94 BLR             X9
__text:0000000100192C98 LDR             X0, [SP,#0x50+var_30]
__text:0000000100192C9C BL              ___swift_destroy_boxed_opaque_existential_0
__text:0000000100192CA0 LDP             X29, X30, [SP,#0x50+var_s0]
__text:0000000100192CA4 LDP             X20, X19, [SP,#0x50+var_10]
__text:0000000100192CA8 ADD             SP, SP, #0x6

hook 脚本

var targetModule = 'DVIA-v2';
var addr = ptr(0x192c64); #ptr(0x192c90);
var moduleBase = Module.getBaseAddress(targetModule);
var targetAddress = moduleBase.add(addr);Interceptor.attach(targetAddress, {onEnter: function(args) {console.log('At the address ' + addr + ' the value is currently ' + this.context.x0);},});var targetModule = 'DVIA-v2';
var addr = ptr(0x192c64); #ptr(0x192c90);
var moduleBase = Module.getBaseAddress(targetModule);
var targetAddress = moduleBase.add(addr);Interceptor.attach(targetAddress, {onEnter: function(args) {if(this.context.x0 == 0x01){     #替换返回值this.context.x0=0x00console.log("Bypass Test1");}},});

替换返回值之后成功绕过越狱检测

frida -U -l jailbreak.js DVIA-v2

Test 2

第二关用objection来测试比较方便

objection -g DVIA-v2 explore  #启动调试ios hooking search classes jailbreak  #寻找越狱的相关类
ios hooking list class_methods JailbreakDetection   #列出类中的相关方法
ios hooking list class_methods DVIA_v2.JailbreakDetectionViewController   #列出类中的相关方法

方法名称前的加号(+)和减号(-)表示方法的返回值。加号(+)表示这是一个返回BOOL类型的类方法。减号(-)表示这是一个实例方法。

只有一个返回BOOL值的类,即+ isJailbreak方法,极大是越狱的判定方法,可以通过改变布尔方法的返回值来越狱。

ios hooking watch method "+[JailbreakDetection isJailbroken]" --dump-args --dump-return   #hook该方法
ios hooking set return_value "+[JailbreakDetection isJailbroken]" false        #修改返回值为false

也可以通过frida hook返回值,脚本如下

var className = "JailbreakDetection";
var funcName = "+ isJailbroken";
var hook = eval('ObjC.classes.' + className + '["' + funcName + '"]');
var newretval = ptr("0x0");Interceptor.attach(hook.implementation, { onLeave: function(retval) { console.log("[*] Class Name: " + className); console.log("[*] Method Name: " + funcName); console.log("\t[-] Type of return value: " + typeof retval); console.log("\t[-] Original Return Value: " + retval);  retval.replace(newretval) console.log("\t[-] New Return Value: " + newretval) } });

Test 3

第三关调用了两个函数,重点看第二个方法中的代码

代码主要是检测越狱后的特征,比如安装软件、系统功能等,最终会写到一个jailbreak.txt文件中。

v2 = _T0S2SBp21_builtinStringLiteral_Bw17utf8CodeUnitCountBi1_7isASCIItcfC("/Applications/Cydia.app", 23LL, 1LL);
v4 = v3;
v5 = _T0SS10FoundationE19_bridgeToObjectiveCSo8NSStringCyF(v2);
swift_unknownRelease(v4);
v6 = (unsigned __int64)objc_msgSend(v1, "fileExistsAtPath:", v5);
v2 = _T0S2SBp21_builtinStringLiteral_Bw17utf8CodeUnitCountBi1_7isASCIItcfC("/Applications/Cydia.app", 23LL, 1LL);
v4 = v3;
v5 = _T0SS10FoundationE19_bridgeToObjectiveCSo8NSStringCyF(v2);
swift_unknownRelease(v4);
v6 = (unsigned __int64)objc_msgSend(v1, "fileExistsAtPath:", v5);
v23 = _T0S2SBp21_builtinStringLiteral_Bw17utf8CodeUnitCountBi1_7isASCIItcfC("/usr/sbin/sshd", 14LL, 1LL);
v25 = v24;
v26 = _T0SS10FoundationE19_bridgeToObjectiveCSo8NSStringCyF(v23);
swift_unknownRelease(v25);
v27 = (unsigned __int64)objc_msgSend(v22, "fileExistsAtPath:", v26);
v30 = _T0S2SBp21_builtinStringLiteral_Bw17utf8CodeUnitCountBi1_7isASCIItcfC("/etc/apt", 8LL, 1LL);
v32 = v31;
v33 = _T0SS10FoundationE19_bridgeToObjectiveCSo8NSStringCyF(v30);
swift_unknownRelease(v32);
v34 = (unsigned __int64)objc_msgSend(v29, "fileExistsAtPath:", v33);
v43 = _T0S2SBp21_builtinStringLiteral_Bw17utf8CodeUnitCountBi1_7isASCIItcfC("/private/jailbreak.txt", 22LL, 1LL);
v45 = v44;
v46 = _T0SS10FoundationE19_bridgeToObjectiveCSo8NSStringCyF(v43);
swift_unknownRelease(v45);
v104 = 0uLL;
v47 = (unsigned __int64)objc_msgSend(v42, "removeItemAtPath:error:", v46, &v104);

但是代码中有一个if逻辑决定是否越狱

  if ( v51 & 1 )v116 = 1;if ( v116 & 1 ){_T0So17UIAlertControllerCMa(v52);v53 = _T0S2SBp21_builtinStringLiteral_Bw17utf8CodeUnitCountBi1_7isASCIItcfC(&unk_100391E4C, 0LL, 1LL);v55 = v54;v57 = v56;v58 = _T0S2SBp21_builtinStringLiteral_Bw17utf8CodeUnitCountBi1_7isASCIItcfC("Device is Jailbroken, the application will now exit",51LL,1LL);_T0So13UIAlertActionCMa();}else{_T0So17UIAlertControllerCMa(v52);v66 = _T0S2SBp21_builtinStringLiteral_Bw17utf8CodeUnitCountBi1_7isASCIItcfC(&unk_100391E4C, 0LL, 1LL);v68 = v67;v70 = v69;v71 = _T0S2SBp21_builtinStringLiteral_Bw17utf8CodeUnitCountBi1_7isASCIItcfC("Device is Not Jailbroken", 24LL, 1LL);}

__text:00000001001959D4 loc_1001959D4
__text:00000001001959D4 LDURB           W8, [X29,#var_30]
__text:00000001001959D8 AND             W8, W8, #1
__text:00000001001959DC TBZ             W8, #0, loc_10

and指令中,它对寄存器 8 和值 0x1 执行 AND 操作。该tbz指令定义为Test bit and branch if zero。因此该tbz指令正在测试寄存器 8 是 0x0 还是 0x1。如果它是 0x0,那么它将跳转。这意味着可以让寄存器 8 的值为 0x0,那么它将跳转到成功函数。

查看目前寄存器中存的值,是0x1,对应的是检测到越狱的分支,修改为0x0则绕过越狱检测

var targetModule = 'DVIA-v2';
var addr = ptr(0x1959dc);
var moduleBase = Module.getBaseAddress(targetModule);
var targetAddress = moduleBase.add(addr);Interceptor.attach(targetAddress, {onEnter: function(args) {console.log('At the address ' + addr + ' the value is currently ' + this.context.x8);},});var targetModule = 'DVIA-v2';
var addr = ptr(0x1959dc);
var moduleBase = Module.getBaseAddress(targetModule);
var targetAddress = moduleBase.add(addr);Interceptor.attach(targetAddress, {onEnter: function(args) {if(this.context.x8 == 0x01){this.context.x8=0x00console.log("Bypass Test3");}},});

修改值之后

Test 4

操作同3

var targetModule = 'DVIA-v2';
var addr = ptr(0x1936E4);
var moduleBase = Module.getBaseAddress(targetModule);
var targetAddress = moduleBase.add(addr);Interceptor.attach(targetAddress, {onEnter: function(args) {console.log('At the address ' + addr + ' the value is currently ' + this.context.x8);},});var targetModule = 'DVIA-v2';
var addr = ptr(0x1936E4);
var moduleBase = Module.getBaseAddress(targetModule);
var targetAddress = moduleBase.add(addr);Interceptor.attach(targetAddress, {onEnter: function(args) {if(this.context.x8 == 0x01){this.context.x8=0x00console.log("Bypass Test4");}},});19702C

Test 5

同3

var targetModule = 'DVIA-v2';
var addr = ptr(0x19702C);
var moduleBase = Module.getBaseAddress(targetModule);
var targetAddress = moduleBase.add(addr);Interceptor.attach(targetAddress, {onEnter: function(args) {console.log('At the address ' + addr + ' the value is currently ' + this.context.x8);},});var targetModule = 'DVIA-v2';
var addr = ptr(0x19702C);
var moduleBase = Module.getBaseAddress(targetModule);
var targetAddress = moduleBase.add(addr);Interceptor.attach(targetAddress, {onEnter: function(args) {if(this.context.x8 == 0x01){this.context.x8=0x00console.log("Bypass Test5");}},});

part3-Runtime Manipulation

任务:绕过登录限制和修改url

Login Method 1

起初是想像越狱绕过一样hook TBZ指令的值来绕过登录,但是发现修改了也不行,所以改用objection

ios hooking search classes login
ios hooking list class_methods LoginValidate
ios hooking watch class LoginValidate
ios hooking watch method "+[LoginValidate isLoginValidated]" --dump-args --dump-return
ios hooking set return_value "+[LoginValidate isLoginValidated]" true

Login Method 2

定位登录方式2的方法,在代码中找到账号密码,登录成功

Read tutorial

还未弄清,暂留

Validate code

在登录验证类中就包含该方法,验证码写死了,值为8848

part4-Side Channel DataLeakage

Device Logs

提交信息就崩溃,暂留

App Screenshot

暂不清楚意义,暂留

Pasteboard

读取剪贴板的信息,但是另一个应用访问权限还不太清楚

ios pasteboard monitor

Cookies

ios cookies get

part5-Broken Cryptography

这一关环节使用 RNEncryptor 对数据进行加密存贮,使用的是AES加密,可以根据key和密文等信息解密出数据,但是试了一番发现自己对swift语言不太会,没办法编写对应解密工具,所以就先放一放。此题wp见:https://resources.infosecinstitute.com/topic/ios-part-29-insecure-broken-cryptography/

Challenge 1

Challenge 2

part6-Webview Issues

任务:向app注入一些代码以完成需求

MSG “Hello Word”

正常提交数据

向app注入代码,成功弹出数据

<script>alert(/Hello Word/)</script>

其他要求是通过注入代码达到开启其他app的效果,但是没找到相关资料没能复现成功

part7-Network Layer Security

任务:绕过证书检测

SslPinning

使用objection hook即可,和安卓端的思路一样

objection --gadget DVIA-v2 explore
ios sslpinning disable

手机配置burp的代理后发送请求,即可截获信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6AEi5uDa-1669259570834)(C:/Users/xinghe/AppData/Roaming/Typora/typora-user-images/image-20221107164149486.png)]

part8-Application Patching

任务:绕过一些特定的检测

Login Method 1

在ida中查看方法也能看到账号密码,但是任务要求我们要用hook的方式通关。定位到检测登录的代码,关键处理逻辑跟前面一样也是if判断,直接hook结果

frida-trace -U -i "*ApplicationPatching*" DVIA-v2  #定位触发函数

hook代码

var targetModule = 'DVIA-v2';
var addr = ptr(0x1AC1B4);
var moduleBase = Module.getBaseAddress(targetModule);
var targetAddress = moduleBase.add(addr);Interceptor.attach(targetAddress, {onEnter: function(args) {if(this.context.x8 == 0x00){this.context.x8=0x01console.log("Bypass OK");}},});

剩下的关卡也是和前面一样hook,就不做演示

总结

在练习这个DVIA-v2靶场的时候可借鉴的学习资料太少,IOS独有的漏洞还不太了解。因为swift和object-c语言自己平时也没接触,不熟悉app的开发语法,信息在app之前的传输、保存、读取等操作。但有些漏洞其实和安卓的大差不差,移动安全主要偏向于隐私保护,不能让其他未授权的app轻易读取敏感数据文件。后面的学习中可能会偏向于IOS的app开发,了解基础语法和权限申请、数据传递等知识。

IOS安全测试学习-DVIA-v2相关推荐

  1. IOS UI Automation 学习之常用类,方法和模拟手势

    为什么80%的码农都做不了架构师?>>>    IOS UI Automation 学习之常用类,方法和模拟手势 常用类结构图 作者不擅长作画,如果有好的画此类图形的工具,可以留言, ...

  2. KALI LINUX渗透测试学习笔记

    KALI LINUX渗透测试学习笔记 (苑房弘主讲) 第1章 课程介绍 任务1:Kali Linux渗透测试介绍.exe 安全问题的根源: 分层思想 只求功能实现 最大的威胁是人 渗透测试: 尝试挫败 ...

  3. iOS App Clips学习笔记

    一.什么是App Clip? App Clip是一个App的轻量版,用来提供一些用户所需功能,我们可以把它看成苹果的小程序.它不需要下载就直接能打开.这样即可达到不下载完整版APP便可体验APP的部分 ...

  4. GrowingIO 数据采集 iOS SDK 测试实践

    作者:吕雨强 GrowingIO iOS 测试工程师,主要负责 iOS SDK 功能测试.自动化测试 . GrowingIO 是基于用户行为数据的增长平台,精准采集用户行为数据是公司业务的基石,只有及 ...

  5. 《大话移动APP测试:Android与iOS应用测试指南》

    <大话移动app测试:android与ios应用测试指南> 基本信息 作者: 陈晔 出版社:清华大学出版社 ISBN:9787302368793 上架时间:2014-7-7 出版日期:20 ...

  6. go定时器 每天重复_通过测试学习Go:Hello, World

    点击上方蓝色"Go语言中文网"关注我们,设个星标,每天学习 Go 语言 你可以在这里查看本章的所有代码[1] 按照传统,我们学习新语言编写的第一个程序都是 Hello,world. ...

  7. python训练营微信公众号真实性_用python进行微信公众号开发(仅测试学习)

    今天看到篇教程,是用python开发微信公众号的,觉得有意思,就敲代码实现了一下,成功后更觉得好玩,故记录,方便开发深入时使用. 基础背景介绍: 首先得有个人微信号(没有自行注册),为方便测试学习: ...

  8. IOS应用管理学习,进阶,涉及字典转模型,工厂方法,面向对象思想,页面布局等

    IOS应用管理学习,进阶,涉及字典转模型,工厂方法,面向对象思想,页面布局等 前言:人为规定的参数 每一个 小view视图 宽度 80 高度 90 数据类型 CGFloat 定义, 3 列,数据类型 ...

  9. 电脑硬件检测_【学无止境】电脑硬件维修测试学习资料(附送各类PC检修资源)...

    大橙子资源驿站 『 电脑硬件维修测试学习资料』 多年的PC硬件检修工具和资料,都是本人一年年亲测积累起来的,安全无毒.打包上传分享给大家. -- 资源分享者语 关于『 电脑硬件维修学习资料 』 这是甛 ...

最新文章

  1. MAC安装MySQL
  2. bfs+状态压缩dp
  3. 算法:正则表达式匹配
  4. 90. Subsets II 1
  5. VTK:灯光之LightActor
  6. asp.net 按钮单击事件问题(自动弹出新窗口)
  7. 奇异值分解(Singular Value Decomposition,SVD)
  8. 笔记:awesome-chatops摘要
  9. Mysql 一条SQL语句实现批量更新数据,update结合case、when和then的使用案例
  10. 总结大学一些常见数据结构简单实现(cc++语言)
  11. 23. Perfer non-member non-friend functions to member functions
  12. Cglib 代码生成库使用快速入门
  13. 最有用的p d f 格式转换软件
  14. win10计算机打开一直读进度条,win10开机读条后黑屏怎么办_win10开机读条之后黑屏修复方法-win7之家...
  15. 正确使用pageX、pageY、offsetLeft、offsetTop
  16. 单片机自动调光C语言,基于51单片机的DS18B20水温PID控制调节系统设计C语言程序...
  17. 老有所依,得上点技术
  18. 人体存在感应雷达应用,飞睿科技雷达传感器,雷达感应模块技术
  19. PSP模拟器截图CG的高清化-Waifu2x
  20. 万卷书 - 自律就是自由 Discipline Equals Freedom

热门文章

  1. opencv python教程简书_OpenCV-Python教程:57.图像修复
  2. mysql 8.0.13解压版安装_mysql 8.0.13 解压版安装配置方法图文教程
  3. 2048游戏英雄榜java_2048游戏攻略 2048分数排行榜详解
  4. 二值形态学之击中击不中变换
  5. iOS iPhone、iPad、Mac等禁止系统摄像头功能
  6. c语言'\0','0',0 ,0之间的区别
  7. Eureka-使用教程
  8. 主板、内存条、硬盘、显卡认识和选购
  9. 驾考通-小型汽车考试
  10. 微信公众号授权登录之二(tp5)