首页
社区
课程
招聘
[原创]逆向windows数字签名验证过程
2011-6-8 15:01 15570

[原创]逆向windows数字签名验证过程

2011-6-8 15:01
15570
临近期末要考试,目前只逆了一个函数,BOOL CryptCATAdminAcquireContext(HCATADMIN *phCatAdmin,GUID *pgSubsystem,DWORD dwFlags),待有时间后再一一逆完。没什么技术含量,高手飘过。
BYTE guidtowstr[] = {0x3,0x2,0x1,0x0,0x2d,0x5,0x4,0x2d,0x7,0x6,0x2d,0x8,0x9,0x2d,0xa,0xb,0xc,0xd,0xe,0xf};
WCHAR UnicodeNumber[] = L"0123456789ABCDEF";
//FIX ME:THE PATH BELOW SHOULD BE PRODUCED ACCORDING TO YOUR OWN MACHINE!
WCHAR OrigPath[] = L"C:\\WINDOWS\\system32\\CatRoot\\";
WCHAR OrigPath2[] = L"C:\\WINDOWS\\system32\\CatRoot2\\";
WCHAR Slash[] = L"\\";

typedef struct _CAT_CONTEXT
{
	int cbSize;
	BOOL UseDefaultGUID;
	PWCHAR pwGUID;
	PWCHAR pwDirectoryPath;
	PWCHAR pwDirectoryPath2;
	int _20;
	PVOID list1;
	PVOID list2;
	PVOID list3;
	int _36;
	CRITICAL_SECTION CriticalSection;
	int _64;
	int _68;
	HANDLE hWakeEventHandle;
	HANDLE hCleanWaitObject;
	int _80;
	int _84;
}CAT_CONTEXT,*PCAT_CONTEXT;

BOOL CryptCATAdminAcquireContext(HCATADMIN *phCatAdmin,GUID *pgSubsystem,DWORD dwFlags);
BOOL CryptCATAdminAcquireContext_Internal(HCATADMIN *phCatAdmin,GUID *pgSubsystem,DWORD dwFlags,int arg);
VOID LIST_Initialize(PVOID pBegin);
BOOL guid2wstr(GUID *guid,PWCHAR pwguid);
PWCHAR  _CatAdminCreatePath(PWCHAR OrigPath,PWCHAR pwGUID,BOOL UseDefaultGUID);
BOOL _CatAdminRecursiveCreateDirectory(PWCHAR pwDirectoryPath,LPSECURITY_ATTRIBUTES lpSecurityAttributes);
VOID CALLBACK _CatAdminWaitOrTimerCallback(PVOID lpParameter,BOOLEAN TimerOrWaitFired);;

BOOL CryptCATAdminAcquireContext(HCATADMIN *phCatAdmin,GUID *pgSubsystem,DWORD dwFlags)
{
	return CryptCATAdminAcquireContext_Internal(phCatAdmin,pgSubsystem,dwFlags,0);
}

BOOL CryptCATAdminAcquireContext_Internal(HCATADMIN *phCatAdmin,GUID *pgSubsystem,DWORD dwFlags,int arg)
{
	WCHAR wGUID[0x100];
	GUID DefaultGUID = {0x127d0a1d,0x4ef2,0x11d1,{0x86,0x8,0x0,0xc0,0x4f,0xc2,0x95,0xee}};
	BOOL ReturnValue;
	GUID *pGUIDToUse;
	HCATADMIN *_phCatAdmin;
	GUID *_pgSubsystem;
	PCAT_CONTEXT pCatContext;
	BOOL UseDefaultGUID;

	_phCatAdmin = phCatAdmin;
	_pgSubsystem = pgSubsystem;

	pGUIDToUse = &DefaultGUID;
	ReturnValue = 1;
	UseDefaultGUID = TRUE;
	//FIX ME: CHECK WHEN phCatAdmin == NULL!
	*phCatAdmin = 0;

	pCatContext = (PCAT_CONTEXT)LocalAlloc(LMEM_ZEROINIT,0x54);

	memset((PVOID)pCatContext,0,0x54);
	//FIX ME: CHECK WHEN pCatAdmin == NULL!
	pCatContext->cbSize = 0x54;

	LIST_Initialize(&(pCatContext->list1));

	if(_pgSubsystem == NULL)
	{
		pCatContext->UseDefaultGUID = UseDefaultGUID;
	}
	else
	{
		UseDefaultGUID = FALSE;
		pGUIDToUse = _pgSubsystem;
	}
	
	guid2wstr(pGUIDToUse,wGUID);

	InitializeCriticalSection(&(pCatContext->CriticalSection));

	pCatContext->_64 = UseDefaultGUID;
	pCatContext->_68 = 0;
	//FIX ME:WHEN NULL RETURNED!
	pCatContext->pwGUID = (PWCHAR)LocalAlloc(LMEM_ZEROINIT,2 * lstrlenW(wGUID) + 2);
	
	wcscpy(pCatContext->pwGUID,wGUID);
	//FIX ME:WHEN NULL RETURNED!
	pCatContext->pwDirectoryPath = _CatAdminCreatePath(OrigPath,wGUID,TRUE);
	//FIX ME:WHEN NULL RETURNED!
	pCatContext->pwDirectoryPath2 = _CatAdminCreatePath(OrigPath2,wGUID,TRUE);
	//FIX ME:WHEN FALSE RETURNED!
	if(_CatAdminRecursiveCreateDirectory(pCatContext->pwDirectoryPath,NULL))
	{
		//FIX ME:WHEN FALSE RETURNED!
		if(_CatAdminRecursiveCreateDirectory(pCatContext->pwDirectoryPath2,NULL))
		{
			//FIX ME:WHEN NULL RETURNED!
			pCatContext->hWakeEventHandle = CreateEvent(NULL,FALSE,FALSE,NULL);
			if(pCatContext->hWakeEventHandle)
			{
				//FIX ME:WHEN 0 RETURNED!
				//if(RegisterWaitForSingleObject(&(pCatContext->hCleanWaitObject),pCatContext->hWakeEventHandle,(WAITORTIMERCALLBACK)_CatAdminWaitOrTimerCallback,pCatContext,INFINITE,WT_EXECUTEDEFAULT))
				//{
					*_phCatAdmin = (HCATADMIN *)pCatContext;
				//}
			}
		}
	}

	return ReturnValue;
}

VOID LIST_Initialize(PVOID pBegin)
{
	*(DWORD *)pBegin = 0;
	*(DWORD *)((BYTE *)pBegin + 4) = 0;
	*(DWORD *)((BYTE *)pBegin + 8) = 0;
}

BOOL guid2wstr(GUID *guid,PWCHAR pwguid)
{
	int i = 0;
	//FIX ME:WHEN guid OR pwguid == NULL!
	*pwguid = L'{';
	pwguid += 1;

	for(;i < 0x14;i++,pwguid++)
	{
		if(guidtowstr[i] != '-')
		{
			*pwguid = UnicodeNumber[((DWORD)(*((BYTE *)guid + guidtowstr[i]))) / 16];
			pwguid++;
			*pwguid = UnicodeNumber[((DWORD)(*((BYTE *)guid + guidtowstr[i]))) & 0xf];
		}
		else
		{
			*pwguid = L'-';
		}
	}

	*pwguid = L'}';
	*(pwguid + 1) = 0;

	return TRUE;
}

PWCHAR  _CatAdminCreatePath(PWCHAR OrigPath,PWCHAR pwGUID,BOOL UseDefaultGUID)
{
	int length = 0;
	PWCHAR pwFinalPath;

	length += lstrlenW(OrigPath);
	length += lstrlenW(pwGUID);
	length += 2;
	if(UseDefaultGUID)
	{
		length += 1;
	}
	//FIX ME:WHEN NULL RETURNED!
	pwFinalPath = (PWCHAR)LocalAlloc(LMEM_ZEROINIT,2 * length);

	wcscpy(pwFinalPath,OrigPath);
	if(OrigPath[lstrlenW(OrigPath) - 1] != L'\\')
	{
		wcscat(pwFinalPath,Slash);
	}
	wcscat(pwFinalPath,pwGUID);

	if(UseDefaultGUID)
	{
		wcscat(pwFinalPath,Slash);
	}

	return pwFinalPath;
}

BOOL _CatAdminRecursiveCreateDirectory(PWCHAR pwDirectoryPath,LPSECURITY_ATTRIBUTES lpSecurityAttributes)
{
	PWCHAR pwPath = NULL;
	BOOL ret;

	if(pwDirectoryPath[lstrlenW(pwDirectoryPath) - 1] == L'\\')
	{
		//FIX ME:WHEN NULL RETURNED!
		pwPath = (PWCHAR)LocalAlloc(LMEM_ZEROINIT,2 * lstrlenW(pwDirectoryPath));
		memcpy((PVOID)pwPath,(PVOID)pwDirectoryPath,2 * lstrlenW(pwDirectoryPath) - 2);
		pwPath[lstrlenW(pwDirectoryPath) - 1] = 0;
		ret = _CatAdminRecursiveCreateDirectory(pwPath,lpSecurityAttributes);
	}
	else
	{
		//FIX ME:CHECK WHETHER IT'S WINNT FIRST!
		if(GetFileAttributesW(pwDirectoryPath) != 0xffffffff) //INVALID_FILE_ATTRIBUTES
		{
			//FIX ME:WHEN NOT!
			if(GetFileAttributesW(pwDirectoryPath) & FILE_ATTRIBUTE_DIRECTORY)
			{
				ret = TRUE;
			}
		}
		else
		{
			//FIX ME:WHEN ERROR CODE IS OTHERS!
			if(GetLastError() == ERROR_PATH_NOT_FOUND || GetLastError() == ERROR_FILE_NOT_FOUND)
			{
				//FIX ME:CHECK WHETHER IT'S WINNT FIRST!
				CreateDirectoryW(pwDirectoryPath,lpSecurityAttributes);
				SetFileAttributesW(pwDirectoryPath,FILE_ATTRIBUTE_NORMAL);
				ret = TRUE;
			}
		}
	}

	if(pwPath)
	{
		LocalFree(pwPath);
	}
	
	return ret;
}

VOID CALLBACK _CatAdminWaitOrTimerCallback(PVOID lpParameter,BOOLEAN TimerOrWaitFired)
{
}

最后那个注册的回调函数,是用来清理释放资源的,由于我的vc6 SDK不够高级,没有RegisterWaitForSingleObject,所以也就没有还原除c代码。实际使用时,即使不加上这些,也是可以成功验证的,只是可能有一些资源泄漏问题。

[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞5
打赏
分享
最新回复 (18)
雪    币: 278
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
流氓刚 2011-6-10 08:05
2
0
       雪雪雪     看看看看看看看看看 
 雪雪雪雪雪雪            看     
     雪        看看看看看看看看看看看
  雪雪雪雪雪雪雪雪    看 看  看  看 看
    雪            看 看 看   
雪雪雪雪雪雪雪雪雪雪雪雪   看看  看  看看 
   雪     雪               
  雪雪雪雪雪雪雪雪     看看看看看看看看看 
 雪 雪     雪             看 
雪  雪雪雪雪雪雪雪      看看看看看看看看 
   雪     雪             看 
   雪雪雪雪雪雪雪     看看看看看看看看看 
                         
雪    币: 386
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ttgood 2011-6-10 09:05
3
0
好。。。。。。
雪    币: 183
活跃值: (998)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
yy大雄 2011-6-10 10:11
4
0
收藏下

等待下文
雪    币: 55
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
xiongyifly 2011-6-10 10:28
5
0
mark一下
雪    币: 202
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
albeta 2011-6-10 15:34
6
0
中间多加点注释就好了
雪    币: 30090
活跃值: (2037)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
bestbird 2011-6-11 18:27
7
0
直接从文件格式验证也是可以的

function VerifySignature(Index: Integer): Integer;
var
  Signer: TElPKCS7Signer;
  Certificate: TElX509Certificate;
  Lookup: TElCertificateLookup;
  I, TagID: Integer;
  IncludedDigest, OriginalDigest, DecryptedDigest: string;
  MD5: TMessageDigest128;
  SHA1: TMessageDigest160;
  Data: string;
begin
  try
    // check for signature index validity
    Result := SB_AUTHENTICODE_ERROR_INVALID_INDEX;
    if (Index < 0) or (Index >= FMessage.SignedData.SignerCount) then
      Exit;
    Signer := FMessage.SignedData.Signers[Index];
    if Signer = nil then
      Exit;
    // attempt to find signer's certificate
    Result := SB_AUTHENTICODE_ERROR_NO_SIGNER_CERTIFICATE;
    Certificate := nil;
    Lookup := TElCertificateLookup.Create(nil);
    try
      Lookup.Criteria := [lcIssuer];
      Lookup.Options := [loExactMatch, loMatchAll];
      Lookup.IssuerRDN.Assign(Signer.Issuer.Issuer);
      I := FMessage.SignedData.Certificates.FindFirst(Lookup);
      while I >= 0 do
      begin
        if FMessage.SignedData.Certificates.Certificates[I].SerialNumber = Signer.Issuer.SerialNumber then
        begin
          Certificate := FMessage.SignedData.Certificates.Certificates[I];
          Break;
        end;
        I := FMessage.SignedData.Certificates.FindNext(Lookup);
      end;
    finally
      Lookup.Free;
    end;
    if (Certificate = nil) and (FCertStorage <> nil) then
    begin
      Lookup := TElCertificateLookup.Create(nil);
      try
        Lookup.Criteria := [lcIssuer];
        Lookup.Options := [loExactMatch, loMatchAll];
        Lookup.IssuerRDN.Assign(Signer.Issuer.Issuer);
        I := FCertStorage.FindFirst(Lookup);
        while I >= 0 do
        begin
          if FCertStorage.Certificates[I].SerialNumber = Signer.Issuer.SerialNumber then
          begin
            Certificate := FCertStorage.Certificates[I];
            Break;
          end;
          I := FCertStorage.FindNext(Lookup);
        end;
      finally
        Lookup.Free;
      end;
    end;
    if Certificate = nil then
      Exit;
    // calculate digest for authenticated attributes
    if Signer.DigestAlgorithm = SB_OID_MD5 then
    begin
      MD5 := HashMD5(Signer.AuthenticatedAttributesPlain);
      SetLength(OriginalDigest, SizeOf(MD5));
      Move(MD5, OriginalDigest[1], SizeOf(MD5));
    end
    else
      if Signer.DigestAlgorithm = SB_OID_SHA1 then
    begin
      SHA1 := HashSHA1(Signer.AuthenticatedAttributesPlain);
      SetLength(OriginalDigest, SizeOf(SHA1));
      Move(SHA1, OriginalDigest[1], SizeOf(SHA1));
    end
    else
    begin
      Result := SB_AUTHENTICODE_ERROR_INVALID_ALGORITHM;
      Exit;
    end;
    // check for authenticated attributes digest validity
    Result := SB_AUTHENTICODE_ERROR_INVALID_SIGNATURE;
    if (Signer.DigestEncryptionAlgorithm = SB_OID_RSAENCRYPTION) and
      (Certificate.PublicKeyAlgorithm = SB_CERT_ALGORITHM_ID_RSA_ENCRYPTION) then
    begin
      DecryptedDigest := DecryptRSA(Signer.EncryptedDigest, Certificate);
      if DecryptedDigest = '' then
        Exit;
      // compare the decrypted digest with calculated one
      if DecryptedDigest <> OriginalDigest then
        Exit;
    end
    else
      if (Signer.DigestEncryptionAlgorithm = SB_OID_DSA) and
      (Certificate.PublicKeyAlgorithm = SB_CERT_ALGORITHM_ID_DSA) then
    begin
      if not VerifyDSA(Signer.DigestAlgorithm, Signer.EncryptedDigest, OriginalDigest, Certificate) then
        Exit;
    end
    else
    begin
      Result := SB_AUTHENTICODE_ERROR_INVALID_ALGORITHM;
      Exit;
    end;
    // attempt to find included digest for authenticode data
    Result := SB_AUTHENTICODE_ERROR_SIGNATURE_CORRUPTED;
    IncludedDigest := '';
    if Signer.AuthenticatedAttributes.Count > 0 then
    begin
      for I := 0 to Signer.AuthenticatedAttributes.Count - 1 do
        if (Signer.AuthenticatedAttributes.Attributes[I] = SB_OID_MESSAGE_DIGEST) and
          (Signer.AuthenticatedAttributes.Values[I].Count > 0) then
        begin
          IncludedDigest := UnformatAttributeValue(Signer.AuthenticatedAttributes.Values[I].Strings[0], TagID);
          Break;
        end;
    end;
    if IncludedDigest = '' then
      Exit;
    // calculate digest for authenticode data
    Result := SB_AUTHENTICODE_ERROR_SIGNATURE_CORRUPTED;
    if Length(FMessage.SignedData.Content) < 2 then
      Exit;
    if Ord(FMessage.SignedData.Content[1]) <> $30 then
      Exit;
    case Ord(FMessage.SignedData.Content[2]) of
      $81: Data := Copy(FMessage.SignedData.Content, 4, MaxInt);
      $82: Data := Copy(FMessage.SignedData.Content, 5, MaxInt);
      $83: Data := Copy(FMessage.SignedData.Content, 6, MaxInt);
    else
      if Ord(FMessage.SignedData.Content[2]) < $81 then
        Data := Copy(FMessage.SignedData.Content, 3, MaxInt)
      else
        Exit;
    end;
    if Signer.DigestAlgorithm = SB_OID_MD5 then
    begin
      MD5 := HashMD5(Data);
      SetLength(OriginalDigest, SizeOf(MD5));
      Move(MD5, OriginalDigest[1], SizeOf(MD5));
    end
    else
      if Signer.DigestAlgorithm = SB_OID_SHA1 then
    begin
      SHA1 := HashSHA1(Data);
      SetLength(OriginalDigest, SizeOf(SHA1));
      Move(SHA1, OriginalDigest[1], SizeOf(SHA1));
    end
    else
    begin
      Result := SB_AUTHENTICODE_ERROR_INVALID_ALGORITHM;
      Exit;
    end;
    // check for authenticode data digest validity
    if OriginalDigest <> IncludedDigest then
    begin
      Result := SB_AUTHENTICODE_ERROR_INVALID_SIGNATURE;
      Exit;
    end;
    if (FActualDigest <> FDigest) then
      Result := SB_AUTHENTICODE_ERROR_INVALID_AUTHENTICODE
    else
      Result := SB_AUTHENTICODE_ERROR_SUCCESS;
  except
    Result := SB_AUTHENTICODE_ERROR_UNKNOWN;
  end;
end;
雪    币: 30090
活跃值: (2037)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
bestbird 2011-6-11 18:28
8
0
签名也是一个鸟样:

:读取并HASH"MZ"
2:移动到$0000003C位置,并HASH前面的数据.
3:读入4字节到FHeaderOffset,判断FHeaderOffset + $A0是否大于文件大小.并HASH这FHeaderOffset.
4:移动到FHeaderOffset位置,并HASH前面的数据.
5:读入4字节到PESign,判断PESign 是否为 $00004550.并HASH这PESign.
6:
(1)保存当前流位置
(2)移动指针到当前位置+$14,并读入两字节.如果该内容为$020B,说明为64位PE,那么后面位置要加$10
(3)恢复流位置

7:移动到FHeaderOffset + $58位置,并HASH前面的数据.
8:FStream.Read(Checksum, 4)!!!注意,这里并没HASH
9:再次移动到SeekStream(FHeaderOffset + $98 + FDelta64bit)位置,并HASH前面的数据.
10:FStream.Read(SignatureOffset, 4)!!!注意,这里并没HASH
11:FStream.Read(SignatureSize, 4)!!!注意,这里并没HASH
12:根据SignatureOffset之类判断是否已经签名了.
13:SeekStream(FStream.Size)HASH余下的数据.
14:(FStream.Size and $F)判断整个文件大小是否能被16整除.不能则后面补零填充

(*
总结:
校验和的偏移位置(唯一一个不参与校验和计算的数据)
数字签名体的偏移位置
数字签名大小

*)
签名收尾:
var
  SignatureOffset, SignatureSize, Dummy: LongWord;

FStream.Position := FStream.Size;//移动到文件尾部
if FPadding <> '' then
   FStream.WriteBuffer(FPadding[1], Length(FPadding)); //补齐16字节

SignatureOffset := FStream.Position;//签名信息位置

SignatureSize := Length(S) + 8;//签名信息大小+sizeof(SignatureSize)+sizeof(Dummy)
FStream.WriteBuffer(SignatureSize, SizeOf(SignatureSize));//写入签名大小
CalcChecksum(FChecksum, @SignatureSize, SizeOf(SignatureSize));

Dummy := $00020200;
FStream.WriteBuffer(Dummy, SizeOf(Dummy));//写入Dummy
CalcChecksum(FChecksum, @Dummy, SizeOf(Dummy));

FStream.WriteBuffer(S[1], Length(S));//写入签名
CalcChecksum(FChecksum, @S[1], Length(S));

FStream.Position := FHeaderOffset + $98 +FDelta64bit;
FStream.WriteBuffer(SignatureOffset, SizeOf(SignatureOffset));//改写签名位置
CalcChecksum(FChecksum, @SignatureOffset, SizeOf(SignatureOffset));

FStream.WriteBuffer(SignatureSize, SizeOf(SignatureSize)); //改写签名大小
CalcChecksum(FChecksum, @SignatureSize, SizeOf(SignatureSize));

FStream.Position := FHeaderOffset + $58;
Dummy := FinalizeChecksum(FChecksum, FStream.Size);
FStream.WriteBuffer(Dummy, SizeOf(Dummy));//最后一个总数据校验
雪    币: 30090
活跃值: (2037)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
bestbird 2011-6-11 18:29
9
0
仅调用系统的读写文件的API就可以了.逆的蛋疼.
雪    币: 239
活跃值: (25)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
dlmu 1 2011-6-11 23:08
10
0
楼上强银~~
雪    币: 108
活跃值: (868)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
yangya 2011-6-12 01:02
11
0
都是强人!!
雪    币: 284
活跃值: (16)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
jerrynpc 2011-6-12 12:31
12
0
一个比一个厉害
雪    币: 289
活跃值: (27)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
zhuangbx 2011-6-12 13:33
13
0
都是牛人!!!
雪    币: 387
活跃值: (76)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
zycxy 2011-6-13 02:03
14
0
看了bestbird大牛,我感觉我没必要继续下去了
雪    币: 298
活跃值: (179)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
飞心男孩 2 2011-6-13 08:58
15
0
人家说的和你的完全是2个不同的东西。也没看出来你应该受打击的部分,相信他发出东西来也不是为了打击你,为啥子不继续了呢,大家只是把各人掌握的东西共享出来,方便后来人。你发的不一定那天对某些人就有用了呢。别放弃,做自己想做的。
但PE数字签名本身而言,感觉这个还不错 http://www.ccgcn.com/bbs/redirect.php?tid=885&goto=lastpost
雪    币: 123
活跃值: (50)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
kmsmxpro 2011-6-14 08:10
16
0
强帖,占位学习
雪    币: 6649
活跃值: (735)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
willapple 2011-6-16 21:42
17
0
好文章!好好学习下!
雪    币: 25
活跃值: (84)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
tihty 2 2011-6-17 01:45
18
0
好贴,向各位牛人学习!
雪    币: 201
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mytemp 2011-6-17 16:08
19
0
进来看看 呵呵!
游客
登录 | 注册 方可回帖
返回