庆祝元旦快乐!从看雪论坛学到很多,也回报点。
获取 EVE Online游戏中Python脚本代码的方法
首先,通过观察游戏文件不难发现 decompiled.code 文件中可能包含的就是Python脚本代码。但是如何才能得到解密的原始字节码呢?因此我们的思路首先围绕这个问题展开。
既然代码已经加密保存,那么程序运行中自然需要解密,因此如果我们能够跟踪到解密代码的部分,自然容易得到Python代码的字节码。为此,我们不妨从Python运行引擎的源代码中开始分析,可能更容易找到线索。
首先,我们先用IDA反汇编一下游戏的主程序文件Exefile.exe ,可以发现程序中有Python的不少API函数调用。并且调用位于blue.dll模块中,通过动态跟踪我们可以进一步确认blue.dll模块即为Python23.dll换名而得。具体过程这里不多叙述,我们只把注意力集中在Python源代码分析上。
我们可以猜想一下那些API是游戏运行时必须调用的?可能这会有个更好的开始。我猜想字节码的版本识别函数PyImport_GetMagicNumber可能是必须调用的。因此可以从这里入手看看。为此我们通过动态跟踪来确认一下,并且从动态跟踪的堆栈中了解函数的调用流程。
通过跟踪我们发现PyImport_GetMagicNumber 函数由 unmarshal_code函数调用。我们来了解一下这个函数的流程:
static PyObject *
unmarshal_code(char *pathname, PyObject *data, time_t mtime)
{
... ( 略 )
if (get_long((unsigned char *)buf) != PyImport_GetMagicNumber()) {
... ( 错误处理,略 )
}
if (mtime != 0 && !eq_mtime(get_long((unsigned char *)buf + 4),
mtime)) {
... ( 错误处理,略 )
}
code = PyMarshal_ReadObjectFromString(buf + 8, size - 8);
... ( 略 )
}
为了让我们只关注关键部分代码,因此一些不太重要的代码这里都略过。从上面的代码中我们可以看到程序首先进行获取MagicNumber的工作和时间戳的认证,然后就通过PyMarshal_ReadObjectFromString函数把缓冲区中的代码数据转换为code代码对象。因此我们只要能够在这一行代码处进行拦截即可以得到每个Python模块的原始字节码。不过这个方法并不能得到那些没有运行的模块的字节码,因此这个方法仍有缺陷。而我们最好的方法是能够知道decompiled.code文件的加密方式,然后编写解密代码解密整个文件得到所有Python模块的字节码,为此我们继续追踪下去,希望能够找到解密的源头。
我们向上追溯跟踪,可以发现unmarshal_code函数由get_code_from_data函数调用,我们来分析一下源代码:
static PyObject *
get_code_from_data(ZipImporter *self, int ispackage, int isbytecode,
time_t mtime, PyObject *toc_entry)
{
PyObject *data, *code;
char *modpath;
char *archive = PyString_AsString(self->archive);
if (archive == NULL)
return NULL;
data = get_data(archive, toc_entry);
if (data == NULL)
return NULL;
modpath = PyString_AsString(PyTuple_GetItem(toc_entry, 0));
if (isbytecode) {
code = unmarshal_code(modpath, data, mtime);
}
else {
code = compile_source(modpath, data);
}
Py_DECREF(data);
return code;
}
我们可以看到程序实际上是通过ZipImporter来管理数据,首先通过这个接口来提取原始数据流(即文件中保存的字节码),然后通过unmarshal_code函数把数据流转换成code对象并作为结果返回。这时我们如果通过IDA逆向blue.dll这段函数的汇编代码就会发现Stackless Python源代码与blue.dll中的汇编代码稍有不同(当然实际上我是走了不少弯路才发现这点的),我们来看一下汇编代码:
.text:100F5C30 get_code_from_data proc near ;
.... ( 略 )
.text:100F5C47 push esi
.text:100F5C48 push ebp
.text:100F5C49 call get_data
.text:100F5C4E mov esi, eax
.text:100F5C50 add esp, 4
.text:100F5C53 test esi, esi
.text:100F5C55 jnz short loc_100F5C5A
.text:100F5C57 pop esi
.text:100F5C58 pop ebp
.text:100F5C59 retn
.text:100F5C5A ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
.text:100F5C5A
.text:100F5C5A loc_100F5C5A: ; CODE XREF: get_code_from_data+25 j
.text:100F5C5A cmp [esp+8+arg_4], 0
.text:100F5C5F push ebx
.text:100F5C60 push edi
.text:100F5C61 jz short loc_100F5CBC
.text:100F5C63 push offset aBlue_crypto ; "blue.crypto"
.text:100F5C68 call PyImport_ImportModule
.text:100F5C6D mov edi, eax
.text:100F5C6F add esp, 4
.text:100F5C72 test edi, edi
.text:100F5C74 jz short loc_100F5CA4
.text:100F5C76 push offset _Py_TrueStruct
.text:100F5C7B push esi
.text:100F5C7C push offset aOo_1 ; "OO"
.text:100F5C81 push offset aUnjumblestring ; "UnjumbleString"
.text:100F5C86 push edi
.text:100F5C87 call PyObject_CallMethod
.text:100F5C8C add esp, 14h
.text:100F5C8F add dword ptr [edi], 0FFFFFFFFh
.text:100F5C92 mov ebx, eax
.text:100F5C94 jnz short loc_100F5CA0
.text:100F5C96 mov edx, [edi+4]
.text:100F5C99 push edi
.text:100F5C9A call dword ptr [edx+18h]
.text:100F5C9D add esp, 4
....( 略 )
get_code_from_data+C6 j
.text:100F5D02 mov eax, edi
.text:100F5D04 pop edi
.text:100F5D05 pop ebx
.text:100F5D06 pop esi
.text:100F5D07 pop ebp
.text:100F5D08 retn
.text:100F5D08 get_code_from_data endp
为了便于分析,同样我略去了很多无关紧要的汇编代码。从上面的代码中我们可以看到在get_data函数获取了数据流后到传入unmarshal_code函数之间多了一段对数据流进行进一步处理的转换代码。通过跟踪我们发现这段代码实现的功能正是解密加密的字节码的功能。这一段代码如果用 Python 表示即为 blue.crypto. UnjumbleString(data) 因此,我们需要进一步找到这个解密函数的位置。
通过搜索字符串我们不难发现这个函数的位置:
.text:10044680 UnjumbleString proc near ; DATA XREF: .data:101EC254 o
( 略 )
.text:100446AF push esi
.text:100446B0 push edi
.text:100446B1 push 0
.text:100446B3 call PyTuple_New
.text:100446B8 mov esi, eax
.text:100446BA push esi
.text:100446BB push offset _Py_NoneStruct
.text:100446C0 mov [esp+34h+var_8], esi
.text:100446C4 call _GetVerCryptKey
.text:100446C9 mov edi, eax
( 略 )
.text:100446F1 mov ecx, [esp+28h+var_4]
.text:100446F5 push ebx
.text:100446F6 push ecx
.text:100446F7 push 0
.text:100446F9 push offset _Py_TrueStruct
.text:100446FE push offset _Py_NoneStruct
.text:10044703 push edi
.text:10044704 push offset aOooio ; "OOOiO"
.text:10044709 call Py_BuildValue
.text:1004470E mov ebx, eax
( 略 )
.text:10044746 push ebp
.text:10044747 push ebx
.text:10044748 push offset _Py_NoneStruct
.text:1004474D call _CryptDecrypt
.text:10044752 mov ebp, eax
( 略 )
.text:1004479A mov ecx, [esp+30h+var_1C]
.text:1004479E push ecx
.text:1004479F call PyObject_IsTrue
.text:100447A4 add esp, 4
.text:100447A7 test eax, eax
.text:100447A9 jz loc_10044837
.text:100447AF push offset aZlib ; "zlib"
.text:100447B4 call PyImport_ImportModule
.text:100447B9 add esp, 4
.text:100447BC test eax, eax
.text:100447BE mov [esp+30h+var_14], eax
( 略 )
.text:100447FB push ebp
.text:100447FC push offset aO_2 ; "O"
.text:10044801 push offset aDecompress ; "decompress"
.text:10044806 push eax
.text:10044807 call PyObject_CallMethod
( 略 )
.text:1004486C UnjumbleString endp
从以上代码中我们来分析一下 UnjumbleString函数的解密流程,首先程序通过_GetVerCryptKey函数获取一个解密密钥,然后通过 _CryptDecrypt函数对加密数据进行解密操作,最后通过导入zlib模块,并调用其方法decompress对解密后的数据进行解压缩。到这里我们只需要了解密钥如何产生以及如何利用密钥进行解密操作即可以实现整个解密过程。因此我们进一步需要分析的目标是 _GetVerCryptKey 以及 _CryptDecrypt 函数的具体实现。
分析 _CryptDecrypt 函数我们可以发现其只是利用Windows API 中的 CryptDecrypt 函数对数据进行解密,_GetVerCryptKey 函数也只是简单地获取内存中的密钥,而函数内并不具体产生密钥。从这些信息中我们初步可以判断程序是通过 Windows 的Crypto API 函数实现加解密操作。由于Crypto API 规范基本一致,因此最终我们的目标锁定在密钥是如何产生的问题上?
通过查找对密钥地址进行引用的函数,我们可以得知密钥的产生位于sub_10042520函数的代码中:
.text:10042520 ; 圹圹圹圹圹圹圹?S U B R O U T I N E 圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹?
.text:10042520
.text:10042520
.text:10042520 sub_10042520 proc near ; CODE XREF: sub_10012900+150 p
.text:10042520
.text:10042520 var_18 = dword ptr -18h
.text:10042520 hKey = dword ptr -0Ch
.text:10042520 hPubKey = dword ptr -8
.text:10042520 dwDataLen = dword ptr -4
.text:10042520
.text:10042520 sub esp, 0Ch
.text:10042523 push 0F0000000h ; dwFlags
.text:10042528 push 1 ; dwProvType
.text:1004252A push offset pszProvider ; "Microsoft Enhanced Cryptographic Provid"...
.text:1004252F push 0 ; pszContainer
.text:10042531 push offset phProv ; phProv
.text:10042536 call ds:CryptAcquireContextA ; Acquire a handle to a particular
.text:10042536 ; key container within a particular CSP
.text:1004253C test eax, eax
.text:1004253E jz short loc_10042564
.text:10042540 mov edx, phProv
.text:10042546 push offset phKey ; phKey
.text:1004254B push 0 ; dwFlags
.text:1004254D push 0 ; hPubKey
.text:1004254F push 94h ; dwDataLen
.text:10042554 push offset pbData ; pbData
.text:10042559 push edx ; hProv
.text:1004255A call ds:CryptImportKey ; Transfer a cryptographic key
.text:1004255A ; from a key blob to the CSP
.text:10042560 test eax, eax
.text:10042562 jnz short loc_10042581
.text:10042564
.text:10042564 loc_10042564: ; CODE XREF: sub_10042520+1E j
.text:10042564 mov eax, BeOS
.text:10042569 mov ecx, [eax]
.text:1004256B push offset byte_101A8F68
.text:10042570 push 0
.text:10042572 push 0FFFFFFFEh
.text:10042574 push eax
.text:10042575 call dword ptr [ecx+34h]
.text:10042578 add esp, 10h
.text:1004257B xor al, al
.text:1004257D add esp, 0Ch
.text:10042580 retn
.text:10042581 ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
.text:10042581
.text:10042581 loc_10042581: ; CODE XREF: sub_10042520+42 j
.text:10042581 mov eax, phProv
.text:10042586 push ebp
.text:10042587 push esi
.text:10042588 push edi
.text:10042589 lea edx, [esp+18h+hKey]
.text:1004258D push edx ; phKey
.text:1004258E push 1 ; dwFlags
.text:10042590 push 1 ; Algid
.text:10042592 push eax ; hProv
.text:10042593 mov [esp+28h+hPubKey], 0
.text:1004259B mov ebp, eax
.text:1004259D mov [esp+28h+hKey], 0
.text:100425A5 call ds:CryptGenKey ; Generate random cryptographic keys
.text:100425A5 ; for use with the CSP module
.text:100425AB test eax, eax
.text:100425AD jz short loc_100425FE
.text:100425AF mov ecx, [esp+18h+hKey]
.text:100425B3 mov esi, ds:CryptExportKey ; Export cryptographic keys out of
.text:100425B3 ; a cryptographic service provider
.text:100425B3 ; in a secure manner
.text:100425B9 lea eax, [esp+18h+dwDataLen]
.text:100425BD push eax ; pdwDataLen
.text:100425BE push 0 ; pbData
.text:100425C0 push 0 ; dwFlags
.text:100425C2 push 7 ; dwBlobType
.text:100425C4 push 0 ; hExpKey
.text:100425C6 push ecx ; hKey
.text:100425C7 call esi ; CryptExportKey ; Export cryptographic keys out of
.text:100425C7 ; a cryptographic service provider
.text:100425C7 ; in a secure manner
.text:100425C9 test eax, eax
.text:100425CB jz short loc_100425FE
.text:100425CD mov edx, [esp+18h+dwDataLen]
.text:100425D1 push edx ; uBytes
.text:100425D2 push 40h ; uFlags
.text:100425D4 call ds:LocalAlloc
.text:100425DA mov edi, eax
.text:100425DC test edi, edi
.text:100425DE jz short loc_100425FE
.text:100425E0 mov ecx, [esp+18h+hKey]
.text:100425E4 lea eax, [esp+18h+dwDataLen]
.text:100425E8 push eax ; pdwDataLen
.text:100425E9 push edi ; pbData
.text:100425EA push 0 ; dwFlags
.text:100425EC push 7 ; dwBlobType
.text:100425EE push 0 ; hExpKey
.text:100425F0 push ecx ; hKey
.text:100425F1 call esi ; CryptExportKey ; Export cryptographic keys out of
.text:100425F1 ; a cryptographic service provider
.text:100425F1 ; in a secure manner
.text:100425F3 test eax, eax
.text:100425F5 jnz short loc_10042616
.text:100425F7 push edi ; hMem
.text:100425F8 call ds:LocalFree
.text:100425FE
.text:100425FE loc_100425FE: ; CODE XREF: sub_10042520+8D j
.text:100425FE ; sub_10042520+AB j ...
.text:100425FE mov eax, [esp+18h+hKey]
.text:10042602 test eax, eax
.text:10042604 jz loc_100426E8
.text:1004260A push eax ; hKey
.text:1004260B call ds:CryptDestroyKey
.text:10042611 jmp loc_100426E8
.text:10042616 ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
.text:10042616
.text:10042616 loc_10042616: ; CODE XREF: sub_10042520+D5 j
.text:10042616 mov eax, [esp+18h+hKey]
.text:1004261A test eax, eax
.text:1004261C jz short loc_10042625
.text:1004261E push eax ; hKey
.text:1004261F call ds:CryptDestroyKey
.text:10042625
.text:10042625 loc_10042625: ; CODE XREF: sub_10042520+FC j
.text:10042625 mov [esp+18h+hKey], 0
.text:1004262D mov edx, [edi+0Ch]
.text:10042630 lea ecx, [edi+10h]
.text:10042633 xor eax, eax
.text:10042635
.text:10042635 loc_10042635: ; CODE XREF: sub_10042520+128 j
.text:10042635 test eax, eax
.text:10042637 jnz short loc_1004263E
.text:10042639 mov byte ptr [ecx], 1
.text:1004263C jmp short loc_10042642
.text:1004263E ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
.text:1004263E
.text:1004263E loc_1004263E: ; CODE XREF: sub_10042520+117 j
.text:1004263E mov byte ptr [eax+ecx], 0
.text:10042642
.text:10042642 loc_10042642: ; CODE XREF: sub_10042520+11C j
.text:10042642 add eax, 1
.text:10042645 cmp eax, 4
.text:10042648 jb short loc_10042635
.text:1004264A mov eax, edx
.text:1004264C mov esi, edx
.text:1004264E shr eax, 4
.text:10042651 shr esi, 3
.text:10042654 lea edx, [esi+eax*2+4]
.text:10042658 add ecx, edx
.text:1004265A xor edx, edx
.text:1004265C test eax, eax
.text:1004265E jbe short loc_10042674
.text:10042660
.text:10042660 loc_10042660: ; CODE XREF: sub_10042520+152 j
.text:10042660 test edx, edx
.text:10042662 jnz short loc_10042669
.text:10042664 mov byte ptr [ecx], 1
.text:10042667 jmp short loc_1004266D
.text:10042669 ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
.text:10042669
.text:10042669 loc_10042669: ; CODE XREF: sub_10042520+142 j
.text:10042669 mov byte ptr [edx+ecx], 0
.text:1004266D
.text:1004266D loc_1004266D: ; CODE XREF: sub_10042520+147 j
.text:1004266D add edx, 1
.text:10042670 cmp edx, eax
.text:10042672 jb short loc_10042660
.text:10042674
.text:10042674 loc_10042674: ; CODE XREF: sub_10042520+13E j
.text:10042674 add ecx, eax
.text:10042676 xor edx, edx
.text:10042678 test eax, eax
.text:1004267A jbe short loc_10042694
.text:1004267C lea esp, [esp+0]
.text:10042680
.text:10042680 loc_10042680: ; CODE XREF: sub_10042520+172 j
.text:10042680 test edx, edx
.text:10042682 jnz short loc_10042689
.text:10042684 mov byte ptr [ecx], 1
.text:10042687 jmp short loc_1004268D
.text:10042689 ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
.text:10042689
.text:10042689 loc_10042689: ; CODE XREF: sub_10042520+162 j
.text:10042689 mov byte ptr [edx+ecx], 0
.text:1004268D
.text:1004268D loc_1004268D: ; CODE XREF: sub_10042520+167 j
.text:1004268D add edx, 1
.text:10042690 cmp edx, eax
.text:10042692 jb short loc_10042680
.text:10042694
.text:10042694 loc_10042694: ; CODE XREF: sub_10042520+15A j
.text:10042694 lea ecx, [ecx+eax*2]
.text:10042697 xor eax, eax
.text:10042699 test esi, esi
.text:1004269B jbe short loc_100426B4
.text:1004269D lea ecx, [ecx+0]
.text:100426A0
.text:100426A0 loc_100426A0: ; CODE XREF: sub_10042520+192 j
.text:100426A0 test eax, eax
.text:100426A2 jnz short loc_100426A9
.text:100426A4 mov byte ptr [ecx], 1
.text:100426A7 jmp short loc_100426AD
.text:100426A9 ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
.text:100426A9
.text:100426A9 loc_100426A9: ; CODE XREF: sub_10042520+182 j
.text:100426A9 mov byte ptr [eax+ecx], 0
.text:100426AD
.text:100426AD loc_100426AD: ; CODE XREF: sub_10042520+187 j
.text:100426AD add eax, 1
.text:100426B0 cmp eax, esi
.text:100426B2 jb short loc_100426A0
.text:100426B4
.text:100426B4 loc_100426B4: ; CODE XREF: sub_10042520+17B j
.text:100426B4 mov ecx, [esp+18h+dwDataLen]
.text:100426B8 lea eax, [esp+18h+hPubKey]
.text:100426BC push eax ; phKey
.text:100426BD push 0 ; dwFlags
.text:100426BF push 0 ; hPubKey
.text:100426C1 push ecx ; dwDataLen
.text:100426C2 push edi ; pbData
.text:100426C3 push ebp ; hProv
.text:100426C4 mov ebp, ds:CryptImportKey ; Transfer a cryptographic key
.text:100426C4 ; from a key blob to the CSP
.text:100426CA call ebp ; CryptImportKey ; Transfer a cryptographic key
.text:100426CA ; from a key blob to the CSP
.text:100426CC push edi ; hMem
.text:100426CD mov esi, eax
.text:100426CF call ds:LocalFree
.text:100426D5 mov eax, [esp+18h+hKey]
.text:100426D9 test eax, eax
.text:100426DB jz short loc_100426E4
.text:100426DD push eax ; hKey
.text:100426DE call ds:CryptDestroyKey
.text:100426E4
.text:100426E4 loc_100426E4: ; CODE XREF: sub_10042520+1BB j
.text:100426E4 test esi, esi
.text:100426E6 jnz short loc_10042717
.text:100426E8
.text:100426E8 loc_100426E8: ; CODE XREF: sub_10042520+E4 j
.text:100426E8 ; sub_10042520+F1 j ...
.text:100426E8 mov eax, BeOS
.text:100426ED mov edx, [eax]
.text:100426EF push offset byte_101A8F68
.text:100426F4 push 0
.text:100426F6 push 0FFFFFFFEh
.text:100426F8 push eax
.text:100426F9 call dword ptr [edx+34h]
.text:100426FC mov eax, [esp+28h+hPubKey]
.text:10042700 add esp, 10h
.text:10042703 test eax, eax
.text:10042705 jz short loc_1004270E
.text:10042707 push eax ; hKey
.text:10042708 call ds:CryptDestroyKey
.text:1004270E
.text:1004270E loc_1004270E: ; CODE XREF: sub_10042520+1E5 j
.text:1004270E pop edi
.text:1004270F pop esi
.text:10042710 xor al, al
.text:10042712 pop ebp
.text:10042713 add esp, 0Ch
.text:10042716 retn
.text:10042717 ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
.text:10042717
.text:10042717 loc_10042717: ; CODE XREF: sub_10042520+1C6 j
.text:10042717 mov eax, [esp+18h+hPubKey]
.text:1004271B mov ecx, phProv
.text:10042721 push offset hKey ; phKey
.text:10042726 push 0 ; dwFlags
.text:10042728 push eax ; hPubKey
.text:10042729 push 8Ch ; dwDataLen
.text:1004272E push offset byte_101AE848 ; pbData
.text:10042733 push ecx ; hProv
.text:10042734 call ebp ; CryptImportKey ; Transfer a cryptographic key
.text:10042734 ; from a key blob to the CSP
.text:10042736 test eax, eax
.text:10042738 jz short loc_100426E8
.text:1004273A mov eax, [esp+18h+hPubKey]
.text:1004273E test eax, eax
.text:10042740 jz short loc_10042749
.text:10042742 push eax ; hKey
.text:10042743 call ds:CryptDestroyKey
.text:10042749
.text:10042749 loc_10042749: ; CODE XREF: sub_10042520+220 j
.text:10042749 pop edi
.text:1004274A pop esi
.text:1004274B mov al, 1
.text:1004274D pop ebp
.text:1004274E add esp, 0Ch
.text:10042751 retn
.text:10042751 sub_10042520 endp
以上代码虽然稍长,我们分析后还是很容易了解其功能实现。大体上它可以分为以下几个步骤:
1) 通过CryptAcquireContextA 函数向CSP申请一个新的密钥容器。
2) 通过CryptImportKey函数把一段blob数据转换成密钥放入容器中。
3) 通过CryptGenKey函数生成一个随机的会话密钥或者是公/私密钥对。
4) 通过CryptExportKey函数导出密钥到申请的内存中。
5) 对内存中的密钥数据进行多重变换。
6) 重新通过CryptImportKey 函数导入内存中的数据到密钥容器中。
到此,我们只要详细分析并实现这些步骤即可以得到一个合法的密钥,然后通过调用相应的Crypto API 函数对加密数据进行解密,最后用zlib.decompress解压缩即可以得到我们需要的原始字节码。
- 标 题:获取 EVE Online 游戏中Python脚本代码的方法 [祝大家元旦快乐]
- 作 者:jaskell
- 时 间:2008-12-31 23:41
- 链 接:http://bbs.pediy.com/showthread.php?t=79827