Fairplay DRM与混淆实现的研究 plain混淆方式
yuyutoo 2024-12-19 17:34 5 浏览 0 评论
研究Fairplay DRM(Digital Rights Management,即数字版权保护)最关键的两点是授权和加密。但长久以来,关于App DRM的研究却很少,而就是在这样的前提下,Fairplay DRM又为iOS App的安全研究叠加了一层“阻碍”。我们通过分析混淆系统的设计和实现过程中的问题,克服调试跟踪的障碍,设计了多种静态和动态的对抗方案;同时通过大量的逆向工程,填补了安全研究人员对macOS系统机制中,关于Fairplay这一部分的认知空白。
什么是DRM?
DRM全称Digital Rights Management,即数字版权保护。苹果为了保护App Store分发的音乐/视频/书籍/App免于盗版,开发了Fairplay DRM技术,并申请了很多相关的专利,比较有代表性的如:
- US8934624B2: Decoupling rights in a digital content unit from download
- US8165286B2: Combination white box/black box cryptographic processes and apparatus
- ES2373131T3: Safe distribution of content using descifrado keys
长久以来,关于App DRM的研究很少,而DRM的关键是授权和加密。破解Fairplay DRM加密的方式俗称“砸壳”,这是进行iOS App安全研究的必要前提。自从2013年苹果引入App DRM机制以后,诞生了如Cluth、Bagbak、Flexdecrypt这样的经典“砸壳工具”,而此类“砸壳工具”通常需要越狱设备的支持,因此具有一定的局限性。
2020年发布的M1 Mac将Fairplay DRM机制引入了MacOS,由于Mac设备的权限没有iOS严格,因此我们得以在MacOS上探索更多Fairplay DRM的原理,最终目标是使解密流程不受Apple平台的限制。下面,我们先来聊聊Apple中是如何实现的?
Apple上DRM的实现:Fairplay DRM
LC_ENCRYPTION_INFO中的标记
加密的MachO含有LC_ENCRYPTION_INFO字段,其中cryptoff标识了加密部分在文件中的起始偏移,cryptsize标识了加密部分的尺寸,cryptid则表明了加密的方法。Fairplay DRM保护下的App,其加密尺寸为4096的倍数,加密方式标识为1。
而负责解密Mach-O的组件主要包括:内核态的FairplayIOKit和用户态的fairplayd。
Fairplay的Open
MacOS的XNU Kernel中有text_crypter_create_hook这个导出符号,IOTextEncryptionFamily驱动则注册了这个Hook,并作为桥梁,将调用转发给了FairplayIOKit内核驱动。
最终负责处理的函数是:
com_apple_driver_FairPlayIOKit::xhU6d1(
char const* executable_path,
long long cpu_type,
long long cpu_subtype,
rp6S0jzg** out_handle
)
此后,内核中的FairplayIOKit开始初始化,通过host_get_special_port中的unfreed port发送MIG调用到用户态的fairplayd,fairplayd开始处理SC_Info目录下的sinf和supp文件,并将处理的数据返回给内核中的FairplayIOKit。
注:用户态的fairplayd具体工作流程不在本文讨论范围内。
其中MIG调用的结构如下:
struct FPRequest{
mach_msg_header_t header;
mach_msg_body_t body;
mach_msg_ool_descriptor_t ool;
NDR_record_t ndr;
uint32_t size;
uint64_t cpu_type;
uint64_t cpu_subtype;
};
struct FPResponse{
mach_msg_header_t header;
mach_msg_body_t body;
mach_msg_ool_descriptor_t ool1; //supf文件映射
mach_msg_ool_descriptor_t ool2; //unk,正比与加密内容的尺寸
uint64_t unk1;
uint8_t unk2[136];
uint8_t unk3[84];
uint32_t size1;
uint32_t size2;
uint64_t unk5;
};
完成所有调用后,返回的结构rp6S0jzg*实际是一个uint32_t类型的handle,接下来则可以用这个handle来完成解密操作。
Fairplay的Decrypt Page
前面提到的Fairplay Open操作最终返回了一个pager_crypt_info的结构体,其中page_decrypt的Hook由IOTextEncryptionFamily驱动接管,并最终转发给FairplayIOKit。
最后,FairplayIOKit中负责解密的函数定义如下:
com_apple_driver_FairPlayIOKit::bvqhJ(
rp6S0jzg *hanlde,
unsigned long long offset,
unsigned char const* src,
unsigned char * dst
)
至此,Fairplay的解密逻辑完成调用。值得注意的是,在Fairplay DRM中,page的概念为4096bytes。
那么,用户态fairplayd处理的sinf和supp文件又是什么样子的呢?
SINF和SUPF文件
结构
用户态的fairplayd会读取随IPA携带的两个重要文件:SINF和SUPF,存储在App的SC_Info目录下。
其中SUPF文件和IPA一起分发,每个用户的IPA和SUPF文件都是一致的,其中SUPF文件中保存了加密Mach-O的密钥,但是密钥本身被另外的机制加密。而SINF文件则作为每个用户的DRM许可,记录了购买用户的标识符和姓名,以及解密SUPF需要的信息,因此在Sandbox策略下,App无法读取自身的SINF文件,以防止其被作为唯一ID追踪用户。
SINF
SINF文件是一个LTV+KV结构的文件,它的字段如下所示:
sinf.frma: game
sinf.schm: itun
sinf.schi.user: 0xdeadbeef
sinf.schi.key : 0x00000005
sinf.schi.iviv: 0x12345678901234567890123456789012
sinf.schi.righ.veID: 0x000007d3
sinf.schi.righ.plat: 0x00000000
sinf.schi.righ.aver: 0x01010100
sinf.schi.righ.tran: 0xdc64f80c
sinf.schi.righ.sing: 0x00000000
sinf.schi.righ.song: 0x59a73c58
sinf.schi.righ.tool: P550
sinf.schi.righ.medi: 0x00000080
sinf.schi.righ.mode: 0x00002000
sinf.schi.righ.hi32: 0x00000004
sinf.schi.name: User Name
sinf.schi.priv: (432 Bytes Private Key)
sinf.sign: (128 Bytes Private)
SUPF
SUPF文件主要分为三个部分,我们将其命名为Key Segments、Fairplay Certificate、RSA Signature,其中Key Segments可以含有多个子Segment,用来保存多个架构的解密信息。
KeyPair Segments:
Segment 0x0: arm64, Keys: 0x36c/4k, sha1sum = e369546960d805dd1188d42e3350430c7e3a0025
Fairplay Certificate:
Data:
Version: 3 (0x2)
Serial Number:
33:33:af:08:07:08:af:00:01:af:00:00:10
Signature Algorithm: sha1WithRSAEncryption
Issuer: C=US, O=Apple Inc., OU=Apple Certification Authority, CN=Apple FairPlay Certification Authority
Validity
Not Before: Jul 8 00:48:29 2008 GMT
Not After : Jul 7 00:48:29 2013 GMT
Subject: C=US, O=Apple Inc., OU=Apple FairPlay, CN=AP.3333AF080708AF0001AF000010
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (1024 bit)
Modulus:
00:b0:01:16:4b:62:b2:37:8d:60:12:4f:02:15:15:
a0:32:1b:e8:ed:44:ed:e9:17:5b:ec:9e:5d:11:24:
5a:66:2f:dc:a3:25:aa:52:70:e1:09:22:09:4b:65:
0f:67:f5:82:dc:af:78:9b:4c:45:f3:b4:f4:77:aa:
fc:a3:b2:84:c3:8b:09:c6:2e:55:f5:14:85:07:ac:
ae:0d:ff:ff:ca:41:3b:44:cb:52:b6:28:60:55:23:
35:8d:26:71:c6:12:a5:e0:72:58:09:3c:4a:9e:b6:
63:df:2a:91:94:27:eb:65:0a:b2:36:45:11:c1:91:
43:58:12:d9:e5:18:a1:ad:db
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment, Data Encipherment, Key Agreement
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Subject Key Identifier:
7B:07:34:81:A5:75:D0:F6:11:BB:D2:36:3F:79:93:4B:A1:70:EB:CF
X509v3 Authority Key Identifier:
keyid:FA:0D:D4:11:91:1B:E6:B2:4E:1E:06:49:94:11:DD:63:62:07:59:64
Signature Algorithm: sha1WithRSAEncryption
06:11:4e:87:ed:b1:08:70:c2:0d:e4:d2:94:bb:7f:ee:50:18:
c0:2a:21:34:0e:99:1f:bf:60:a2:58:d0:0c:28:3d:03:5b:ab:
4e:72:69:ba:41:52:45:b2:29:27:4a:c8:ba:7f:b5:9b:63:78:
b1:68:41:40:59:3f:05:8a:57:74:c5:63:30:cc:f3:20:41:c0:
3c:65:d4:0d:22:47:f3:97:76:e6:d6:3c:eb:e7:20:78:10:59:
fd:96:09:82:c3:41:f0:5f:d0:3e:91:44:6d:77:3f:a5:d9:da:
f0:f7:53:ad:94:61:28:1c:4c:40:3b:17:2b:dd:e3:00:df:77:
71:22
RSA Signature: 6aeb00124d62f75f5761f7c26ec866a061f0776be7e84bfad4b6a1941dbddfdb3bd1afdcc5ef305877fa5bee41caa37b1a9d4ce763cf7d2cb89efa60660a49dd5ddff0f46eee7cd916d382f727d912e82b6e0a62e8110c195e298481aa8c8162faac066ef017c6c2c508700d7adb57e0c988af437621e698946da1b09adf89e9
下面,我们来聊聊Fairplay DRM的混淆原理和实现。
混淆原理和一些实现
LLVM Pass
LLVM是一个优良的编译器框架,其中,我们可以将其大略的分为前端、中端、后端:
配图节选自CMU的CS 15-745课程:https://www.cs.cmu.edu/~15745。
前端负责将高级语言转化为LLVM IR;中端处理LLVM IR,完成一系列的分析、优化任务,我们称之为Pass,再次输出LLVM IR;后端则负责将LLVM IR转化为机器码。其中,中端的玩法特别丰富,基本的优化任务:如死代码消除、常量折叠都在这一部分完成;Address Sanitizer、PC Sanitizer等编译器插桩也是在这里进行的;其他的混淆框架如讨论的较多的ollvm以及Hikari,甚至包括苹果的混淆机制,也都是基于此完成。
这一混淆方式可以基本的分为控制流混淆和数据流混淆,除此之外的一些混淆方式,比如VMP等,不在本文讨论范围内。
makeOpaque
在编译器中,为了防止一些具体的表达式被优化,我们会将表达式进行等价变化,我们暂时将这样的操作定义为makeOpaque(如Safari的JavascriptCore,其JIT组件B3就提供这样的机制),C++伪代码如下:
Expression* makeOpaque(Expression *in);
不透明谓词(Opaque Predicate)
谓词(Predicate)在计算机中,指的是执行后为True或False的表达式。数论里面的一些结论可以作为我们生成不透明谓词的基础,这些不透明谓词的结果恒为True或恒为False。比如下列表达式中,y执行的结果就恒为True:
uint32_t x = 0;
bool y = ((x * x % 4) == 0 || (x * x % 4) == 1);
不透明谓词应用到混淆中的一个例子就是bogus CFG。 如源语句如下:
foo1();
foo2();
经过变换,我们添加了一个虚假的分支(即bogus CFG) :
foo1();
if ( false )
junk_code();
else
foo2();
但是如果没有经过特别处理,编译器、反编译器的死代码消除就会将虚假分支去除掉,因此我们需要makeOpaque的引入,假设我们引入了前面示例中的表达式:
foo1();
uint32_t x = rand();
bool y = ((x * x % 4) == 0 || (x * x % 4) == 1);
if ( !y )
junk_code();
else
foo2();
那么如果编译器、反编译器没有相应的识别机制的话,这一部分的死代码就保留了下来,通过在死代码里面插入大量干扰指令,可以为逆向的人员带来极大的困扰。经测试在-O2优化下,Clang 11已经可以识别这个规则,但是GCC 5.4无法识别。
可逆变换
这里我们介绍一下目前混淆技术中常用的等价变换方式。
异或
异或规则是最常见的变换,这里不再赘述。
x ^ c ^ c = x;
仿射变换(Affine transformation)
我们先来看一下仿射函数。
下面我们来看一下实际应用。
由于计算机中的运算属于隐式的模运算,因此会具有一些有意思的性质。如对于一个uint32上的运算,模运算逆元定义如下:
//对于
uint32_t a, r_a;
//如果满足
(a * r_a) % UINT32_MAX == 1;
//那么 a 和 r_a 互为模反元素
对于互为模反元素的a和r_a(可通过扩展欧几里得算法求得),有这样的特性:
uint32_t x = rand();
uint32_t y1 = a * x + c;
//那么满足
x == ra * y1 + (- ra * c)
最后举个例子来说明:
//对于互为模反元素的4872655123 * 3980501275,取
uint32_t x = 0xdeadbeef;
uint32_t c = 0xbeefbeef;
//则 -ra * c = 0x57f38dcb,且
((x * 4872655123) + 0xbeefbeef) * 3980501275 + 0x57f38dcb == x
/*
可在lldb中验证如下
(lldb) p/x uint32_t x=0xdeadbeef; (uint32_t)(((x * 4872655123) + 0xbeefbeef) * 3980501275 + 0x57f38dcb)
(uint32_t) $8 = 0xdeadbeef
*/
MBA表达式(Mixed Boolean-Arithmetic Expression)
MBA表达式是把算术运算(+,-,*,/)和位运算(&,|,~)混合在一起用以隐藏原本表达式的混淆方法。它基于不同的数学原理存在多种形式,这里主要介绍多项式MBA,这是目前混淆技术中最常遇到的形式。
类似的,在Fairplay混淆中用到的MBA表达式为:
//OperationSet(+, -, *, &, |, ~)
x - c = (x ^ ~c) + ((2 * x) & ~(2 * c + 1)) + 1;
而使用MBA进行混淆操作主要依靠以下两个步骤:
不透明常量(Opaque Constant)
不透明常量是基于MBA混淆的方法,用于隐藏数据流中的常量。它使用了置换多项式,是一种在有限域上的可逆多项式。
控制流平坦化
这一部分是逆向工程中讨论的最热门的话题,即将正常的控制流转换等价替换为一个状态机,从而干扰静态的控制流分析,业界也有较多的解决方案。同时因为Fairplay DRM中没有明显用到这种类型的混淆,不再多讨论。
非直接跳转(Indirect Branch)
将一些基本块的起始地址保存在全局变量中,通过不透明常量的生成,使得反汇编工具和肉眼无法直接获取到基本块跳转的目标,模型如下:
//记录基本块地址到全局查找表LUT
LUT[i] = PC;
//执行跳转
jmp/call LUT[makeOpaque(i)]
具体的实例:
这样,逆向人员就无法直接获取跳转的目标函数、基本块了。同理,通过将判断语句的条件映射到跳转表,也可以实现对条件跳转的混淆。
所以,在逆向被混淆的Fairplay代码时,IDA Pro大多数时刻,只能识别出来函数的第一个基本块,无法分析出函数的边界。
跨函数混淆 + 调用约定混淆
正常情况下,编程语言如C语言的参数传递遵循特定的调用约定,但是部分混淆工具会对一些内部函数的调用约定进行修改,以Fairplay DRM为例:
我们可以看到常规的以寄存器和栈传递参数的方式被替换成了以堆传递参数的方式了,在构造好了结构体的情况下,这个参数传递的特征可以被清晰的看出来。同时,这里面对一些传递的参数进行了异或混淆,在子函数里面再恢复出来,使得我们难以直接得到原始数据,而静态分析的工具比如IDA Pro也不支持跨函数的数据流分析。
更严重的是,一些影响子函数运行的重要依赖数据,被提升到了父函数内,导致在没有恢复调用关系前,我们根本无法推测子函数的运行流程。
那么,Fairplay DRM的破解之道就是要找到它的弱点。
Fairplay混淆的弱点
通过前边的工作,我们已经能Fairplay正常的完成打开和解密工作了,通过一系列的静态分析和追踪调试,我们发现了这一套混淆系统的一些对抗方案。
这些问题的本质原因是:混淆系统在IR层面设计,对机器相关的部分操作没有混淆,因此在生成的机器码里面,我们可以推断得到混淆前的一些特征信息。
函数边界识别
前面提到,由于Fairplay用到了非直接跳转的混淆技术,IDA Pro无法直接分析函数的边界。通过跟踪,我们发现在arm64e设备下,该内核驱动中,同一个函数的所有基本块在运行到跳转指令时,均使用了同一个PAC Context,或者称之为PAC Modifier。
借由这个特性,我们可以将函数的边界和基本块分组,尽管目前为止这些基本块之间并不是连通的。
非直接跳转
对于无条件跳转,我们通过设置断点跟踪执行流,就可以解决了。
再通过KeyPatch这样的工具,我们可以将一些简单的函数恢复到比较易于理解的地步。
但是这里的难点在于恢复混淆里面的非直接跳转指令,如下图所示:
对于这个跳转指令,我们可以生成如下的表达式:
//cmp x0, #0
w10 = qword[x12 + (EQ * 0xB + w19) << 3]
//0xB代表两个基本块的在LUT中的下标差
通过CSET指令的形式,我们已经可以推断跳转指令应该是J.NE或者J.EQ了,通过我们的调试器插件,我们可以得到其中一个分支的跳转地址和原本的跳转指令,再通过表达式的信息,我们可以很快推断出另外一个分支的地址。
再通过Keypatch,我们可以得到混淆前的分支语句结构:
至此,我们已经可以完整地恢复Fairplay的大多数控制流了。
数据流混淆
这一部分在前面已经提及到了一些,目前我们已经找到了MBA表达式的模式,但还没能找到Fairplay中生成不透明常量的完整规则。其中MBA表达式的重写规则目前看起来仅有一个,即:
x - c = (x ^ ~c) + ((2 * x) & ~(2 * c + 1)) + 1;
一些基于模式匹配的工具,比如D810已经可以比较好的处理这样的情况了。
结束语
目前,我们已经可以获取到解密每一段Mach-O的AES密钥了,通过大量的调试和反混淆,我们已经得出了这些密钥生成的初步结论。我们希望最终的目的是不依赖Apple设备的前提下,完成Fairplay DRM加解密的研究。
最后,附上源码,欢迎大家进行参考和研究。
参考文献
- Eyrolles, N. (2017). Obfuscation with Mixed Boolean-Arithmetic Expressions: reconstruction, analysis and simplification tools (Doctoral dissertation, Université Paris-Saclay)
- https://github.com/obfuscator-llvm/obfuscator
- https://github.com/HikariObfuscator/Hikari
- https://github.com/keystone-engine/keypatch
- https://eshard.com/posts/d810_blog_post_1
作者简介
吴聊、落落、朱米,均来自美团信息安全部。
| 本文系美团技术团队出品,著作权归属美团。欢迎出于分享和交流等非商业目的转载或使用本文内容,敬请注明“内容转载自美团技术团队”。本文未经许可,不得进行商业性转载或者使用。任何商用行为,请发送邮件至tech@meituan.com申请授权。
相关推荐
- Mysql和Oracle实现序列自增(oracle创建序列的sql)
-
Mysql和Oracle实现序列自增/*ORACLE设置自增序列oracle本身不支持如mysql的AUTO_INCREMENT自增方式,我们可以用序列加触发器的形式实现,假如有一个表T_WORKM...
- 关于Oracle数据库12c 新特性总结(oracle数据库19c与12c)
-
概述今天主要简单介绍一下Oracle12c的一些新特性,仅供参考。参考:http://docs.oracle.com/database/121/NEWFT/chapter12102.htm#NEWFT...
- MySQL CREATE TABLE 简单设计模板交流
-
推荐用MySQL8.0(2018/4/19发布,开发者说同比5.7快2倍)或同类型以上版本....
- mysql学习9:创建数据库(mysql5.5创建数据库)
-
前言:我也是在学习过程中,不对的地方请谅解showdatabases;#查看数据库表createdatabasename...
- MySQL面试题-CREATE TABLE AS 与CREATE TABLE LIKE的区别
-
执行"CREATETABLE新表ASSELECT*FROM原表;"后,新表与原表的字段一致,但主键、索引不会复制到新表,会把原表的表记录复制到新表。...
- Nike Dunk High Volt 和 Bright Spruce 预计将于 12 月推出
-
在街上看到的PandaDunk的超载可能让一些球鞋迷们望而却步,但Dunk的浪潮仍然强劲,看不到尽头。我们看到的很多版本都是为女性和儿童制作的,这种新配色为后者引入了一种令人耳目一新的新选择,而...
- 美国多功能舰载雷达及美国海军舰载多功能雷达系统技术介绍
-
多功能雷达AN/SPY-1的特性和技术能力,该雷达已经在美国海军服役了30多年,其修改-AN/SPY-1A、AN/SPY-1B(V)、AN/SPY-1D、AN/SPY-1D(V),以及雷神...
- 汽车音响怎么玩,安装技术知识(汽车音响怎么玩,安装技术知识视频)
-
全面分析汽车音响使用或安装技术常识一:主机是大多数人最熟习的音响器材,有关主机的各种性能及规格,也是耳熟能详的事,以下是一些在使用或安装时,比较需要注意的事项:LOUDNESS:几年前的主机,此按...
- 【推荐】ProAc Response系列扬声器逐个看
-
有考牌(公认好声音)扬声器之称ProAcTablette小音箱,相信不少音响发烧友都曾经,或者现在依然持有,正当大家逐渐掌握Tablette的摆位设定与器材配搭之后,下一步就会考虑升级至表现更全...
- #本站首晒# 漂洋过海来看你 — BLACK&DECKER 百得 BDH2000L无绳吸尘器 开箱
-
作者:初吻给了烟sco混迹张大妈时日不短了,手没少剁。家里有了汪星人,吸尘器使用频率相当高,偶尔零星打扫用卧式的实在麻烦(汪星人:你这分明是找借口,我掉毛是满屋子都有,铲屎君都是用卧式满屋子吸的,你...
- 专题|一个品牌一件产品(英国篇)之Quested(罗杰之声)
-
Quested(罗杰之声)代表产品:Q212FS品牌介绍Quested(罗杰之声)是录音监听领域的传奇品牌,由英国录音师RogerQuested于1985年创立。在成立Quested之前,Roger...
- 常用半导体中英对照表(建议收藏)(半导体英文术语)
-
作为一个源自国外的技术,半导体产业涉及许多英文术语。加之从业者很多都有海外经历或习惯于用英文表达相关技术和工艺节点,这就导致许多英文术语翻译成中文后,仍有不少人照应不上或不知如何翻译。为此,我们整理了...
- Fyne Audio F502SP 2.5音路低音反射式落地音箱评测
-
FyneAudio的F500系列,有新成员了!不过,新成员不是新的款式,却是根据原有款式提出特别版。特别版产品在原有型号后标注了SP字样,意思是SpecialProduction。Fyne一共推出...
- 有哪些免费的内存数据库(In-Memory Database)
-
以下是一些常见的免费的内存数据库:1.Redis:Redis是一个开源的内存数据库,它支持多种数据结构,如字符串、哈希表、列表、集合和有序集合。Redis提供了快速的读写操作,并且支持持久化数据到磁...
- RazorSQL Mac版(SQL数据库查询工具)
-
RazorSQLMac特别版是一款看似简单实则功能非常出色的SQL数据库查询、编辑、浏览和管理工具。RazorSQLformac特别版可以帮你管理多个数据库,支持主流的30多种数据库,包括Ca...
你 发表评论:
欢迎- 一周热门
-
-
前端面试:iframe 的优缺点? iframe有那些缺点
-
带斜线的表头制作好了,如何填充内容?这几种方法你更喜欢哪个?
-
漫学笔记之PHP.ini常用的配置信息
-
其实模版网站在开发工作中很重要,推荐几个参考站给大家
-
推荐7个模板代码和其他游戏源码下载的网址
-
[干货] JAVA - JVM - 2 内存两分 [干货]+java+-+jvm+-+2+内存两分吗
-
正在学习使用python搭建自动化测试框架?这个系统包你可能会用到
-
织梦(Dedecms)建站教程 织梦建站详细步骤
-
【开源分享】2024PHP在线客服系统源码(搭建教程+终身使用)
-
2024PHP在线客服系统源码+完全开源 带详细搭建教程
-
- 最近发表
-
- Mysql和Oracle实现序列自增(oracle创建序列的sql)
- 关于Oracle数据库12c 新特性总结(oracle数据库19c与12c)
- MySQL CREATE TABLE 简单设计模板交流
- mysql学习9:创建数据库(mysql5.5创建数据库)
- MySQL面试题-CREATE TABLE AS 与CREATE TABLE LIKE的区别
- Nike Dunk High Volt 和 Bright Spruce 预计将于 12 月推出
- 美国多功能舰载雷达及美国海军舰载多功能雷达系统技术介绍
- 汽车音响怎么玩,安装技术知识(汽车音响怎么玩,安装技术知识视频)
- 【推荐】ProAc Response系列扬声器逐个看
- #本站首晒# 漂洋过海来看你 — BLACK&DECKER 百得 BDH2000L无绳吸尘器 开箱
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)