您的位置:首页 > 运维架构

关于文件读写的监控, 通过APIHOOK来实现

2010-12-18 02:37 495 查看
有的时候,我们需要对程序读写文件的时候进行监控,尤其是文件的数据是保密的,而且不能直接存储在磁盘上。举个最简单的例子来说,当我们有个文件在磁盘上,而这个文件是加密的,这时候在程序打开文件的时候,通过输入密钥进行解密了,但是这些解密了的数据,是不能写回磁盘的,只能放在RAM中,这时候如果程序想通过正常的文件操作来访问数据的话,那么这就需要我们来对这个文件操作的API下钩子函数了。

对于钩子函数的介绍,可以通过下面的几篇文章来了解,前提是必须了解PE格式中的输入表区块内容。

1. http://zhidao.baidu.com/question/56417343.html
2. http://baike.baidu.com/view/1037259?wtp=tt-
3. http://zhidao.baidu.com/question/5555707.html
等等,百度上一大相关文章。

在网上看了不少这方面的资料,发现很多东西写的都好散,没有封装起来,因此我就花了点功夫,把它封装了起来,还没完全测试,先把代码共享一下,如果各位有什么bug的话,请留言,我会尽快改进的。

--------------------------------------------

------------- HooAPI.h -----------------

--------------------------------------------

#pragma once
#include<vector>
#include "Toolhelp.h"

struct HookAPIEntry{
// 这个是模块的名字,比如usr32.dll
CString  strMoudleName ;
// 这个是需要下钩子的API函数
CString  strFunctionName ;
// API函数的原始地址
PROC  pOriginalProc ;
// API函数的新地址
PROC  pNewProc ;
HookAPIEntry(){
pOriginalProc = pNewProc = NULL ;
};
};
typedef HookAPIEntry * PHookAPIEntry ;
typedef HookAPIEntry * LPHookAPIEntry ;
typedef std::vector<PHookAPIEntry> HookAPIEntryList ;
typedef HookAPIEntryList * PHookAPIEntryList ;
typedef HookAPIEntryList * LPHookAPIEntryList ;
typedef std::vector<PHookAPIEntry>::iterator HookAPIEntryPos ;
class CHookAPI
{
public:
CHookAPI(void);
~CHookAPI(void);
public:
// 对钩子函数列表的操作
HRESULT  AddHookAPIEntry(const HookAPIEntry & entry ) ;
HRESULT  AddHookAPIEntry(CString strMoudleName,CString strFunctionName,PROC pNewProc) ;
HRESULT  FindHookAPIEntry(CString strMoudleName,CString strFunctionName,LPHookAPIEntry * pResult);
HRESULT  DeleteHookAPIEntry(CString strMoudleName,CString strFunctionName) ;
HRESULT  ClearAllAPIEntry( void ) ;
HRESULT  ReHookAllMoudleAPI( void ) ;
HRESULT  UnHookAllMoudleAPI( void ) ;

HRESULT  GetHookAPIEntryByNewAddress(PROC pNewAddress,LPHookAPIEntry * pResult) ;
protected:
HRESULT  HookMoudleAPI(const HookAPIEntry & hookentry);
HRESULT  UnHookMoudleAPI(const HookAPIEntry & hookentry);
HRESULT  HookMoudleAPI(HMODULE hmoudle,const HookAPIEntry & hookentry);
HRESULT  UnHookMoudleAPI(HMODULE hmoudle,const HookAPIEntry & hookentry);
HRESULT  ReplaceMoudleAPI(HMODULE hmoudleCaller, const CString &strMoudleName,
PROC pCurAddress,PROC pNewAddress);
HRESULT  GetProcessAddress(const CString &strMoudleName,const CString &strFunctionName,
PROC * pAddress ) ;
HRESULT  UpdateOriginalProc( HookAPIEntry & hookentry);
protected:
// 需要下钩子的函数集合
HookAPIEntryList   m_HookAPIEntryList ;
// 系统的信息
SYSTEM_INFO     m_si;
};


--------------------------------------------

------------- HooAPI.cpp -----------------

--------------------------------------------

#include "StdAfx.h"
#include "HookAPI.h"
#include "Dbghelp.h"
#pragma comment(lib , "imagehlp.lib")
#pragma comment(lib , "Dbghelp.lib")
CHookAPI::CHookAPI(void)
{
GetSystemInfo(&m_si) ;
}

CHookAPI::~CHookAPI(void)
{
}
////
HRESULT CHookAPI::AddHookAPIEntry(const HookAPIEntry & entry ) {
// 已经存在了,就不用添加了
if( S_OK == FindHookAPIEntry(entry.strMoudleName,entry.strFunctionName,NULL ) )
return S_FALSE ;
LPHookAPIEntry pentry = new HookAPIEntry() ;
* pentry = entry ;
m_HookAPIEntryList.push_back(pentry) ;
UpdateOriginalProc(*pentry) ;
HookMoudleAPI(entry);
return S_OK ;
}
HRESULT CHookAPI::AddHookAPIEntry(CString strMoudleName,CString strFunctionName,PROC pNewProc) {
if( S_OK == FindHookAPIEntry(strMoudleName,strFunctionName,NULL ) )
return S_FALSE ;
LPHookAPIEntry pentry = new HookAPIEntry() ;
pentry->strMoudleName = strMoudleName ;
pentry->strFunctionName = strFunctionName ;
pentry->pNewProc = pNewProc ;
m_HookAPIEntryList.push_back(pentry) ;
UpdateOriginalProc(*pentry) ;
HookMoudleAPI(*pentry);
return S_OK ;
}
HRESULT CHookAPI::FindHookAPIEntry(CString strMoudleName,CString strFunctionName,LPHookAPIEntry * pResult){
HookAPIEntryPos pos ;
for( pos = m_HookAPIEntryList.begin(); pos != m_HookAPIEntryList.end() ; pos ++ ){
PHookAPIEntry pHookAPIEntry = * pos ;
if( NULL != pHookAPIEntry &&
pHookAPIEntry->strMoudleName.CompareNoCase(strMoudleName) == 0 &&
pHookAPIEntry->strFunctionName.Compare(strFunctionName) == 0 ){
// 已经找到了
if( pResult == NULL )
* pResult = pHookAPIEntry ;
return S_OK ;
}
};
return S_FALSE ;
}
HRESULT CHookAPI::DeleteHookAPIEntry(CString strMoudleName,CString strFunctionName) {
HookAPIEntryPos pos ;
for( pos = m_HookAPIEntryList.begin(); pos != m_HookAPIEntryList.end() ; pos ++ ){
PHookAPIEntry pHookAPIEntry = * pos ;
if( NULL != pHookAPIEntry &&
pHookAPIEntry->strMoudleName.CompareNoCase(strMoudleName) == 0 &&
pHookAPIEntry->strFunctionName.Compare(strFunctionName) == 0 ){
// 已经找到了
UnHookMoudleAPI(*pHookAPIEntry) ;
m_HookAPIEntryList.erase(pos) ;
delete pHookAPIEntry ;
pHookAPIEntry = NULL ;
return S_OK ;
}
};
return S_OK ;
}
HRESULT CHookAPI::ClearAllAPIEntry( void ) {
UnHookAllMoudleAPI() ;
HookAPIEntryPos pos ;
PHookAPIEntry pHookAPIEntry = NULL ;
while( m_HookAPIEntryList.size() > 0 ) {
pos = m_HookAPIEntryList.begin() ;
pHookAPIEntry = * pos ;

m_HookAPIEntryList.erase(pos) ;
if( NULL == pHookAPIEntry ){
delete pHookAPIEntry ;
pHookAPIEntry = NULL ;
}
}
return S_OK ;
}

HRESULT CHookAPI::GetHookAPIEntryByNewAddress(PROC pNewAddress,LPHookAPIEntry * pResult ) {
HookAPIEntryPos pos ;
for( pos = m_HookAPIEntryList.begin(); pos != m_HookAPIEntryList.end() ; pos ++ ){
PHookAPIEntry pHookAPIEntry = * pos ;
if( NULL != pHookAPIEntry &&
pHookAPIEntry->pNewProc ==  pNewAddress){
// 已经找到了
if( NULL != pResult ){
* pResult = * pos ;
return S_OK ;
}
}
};
return S_FALSE ;
}
HRESULT CHookAPI::UpdateOriginalProc( HookAPIEntry & hookentry){
if(S_OK != GetProcessAddress(hookentry.strMoudleName,
hookentry.strFunctionName,&hookentry.pOriginalProc)){
ASSERT(FALSE) ;
return S_FALSE ;
}
if (hookentry.pOriginalProc > m_si.lpMaximumApplicationAddress)  {
PBYTE pb = (PBYTE) hookentry.pOriginalProc;
if (pb[0] == 0x86){
PVOID pv = * (PVOID*) &pb[1];
hookentry.pOriginalProc = (PROC) pv;
}
}
return S_OK ;
}
HRESULT CHookAPI::GetProcessAddress(const CString &strMoudleName,const CString&strFunctionName,FARPROC * pAddress ){
* pAddress = ::GetProcAddress(GetModuleHandle(strMoudleName), strFunctionName) ;
return S_OK ;
}
HRESULT CHookAPI::ReHookAllMoudleAPI( void ) {
CToolhelp th(TH32CS_SNAPMODULE, GetCurrentProcessId());
MODULEENTRY32 me = { sizeof(me) };
for (BOOL fOk = th.ModuleFirst(&me); fOk; fOk = th.ModuleNext(&me))
{
//TRACE(me.szModule);
// 监控所有的有关文件读写情况
HookAPIEntryPos pos ;
for( pos = m_HookAPIEntryList.begin(); pos != m_HookAPIEntryList.end() ; pos ++ ){
PHookAPIEntry pHookAPIEntry = * pos ;
if(pHookAPIEntry == NULL ){
ASSERT(FALSE) ;
continue ;
}
// 重新获取一下原来的地址
if( pHookAPIEntry->pOriginalProc == NULL ){
if(S_OK != UpdateOriginalProc(*pHookAPIEntry)){
ASSERT(FALSE) ;
continue ;
}
}
// 对所有模块的输入表进行替换
HookMoudleAPI(me.hModule,*pHookAPIEntry) ;
}
};
return S_OK ;
}
HRESULT CHookAPI::UnHookAllMoudleAPI( void ) {
CToolhelp th(TH32CS_SNAPMODULE, GetCurrentProcessId());
MODULEENTRY32 me = { sizeof(me) };
for (BOOL fOk = th.ModuleFirst(&me); fOk; fOk = th.ModuleNext(&me))
{
//TRACE(me.szModule);
// 监控所有的有关文件读写情况
HookAPIEntryPos pos ;
for( pos = m_HookAPIEntryList.begin(); pos != m_HookAPIEntryList.end() ; pos ++ ){
PHookAPIEntry pHookAPIEntry = * pos ;
if(pHookAPIEntry == NULL ){
ASSERT(FALSE) ;
continue ;
}
// 重新获取一下原来的地址
if( pHookAPIEntry->pOriginalProc == NULL ){
if(S_OK != UpdateOriginalProc(*pHookAPIEntry)){
ASSERT(FALSE) ;
continue ;
}
}
// 对所有模块的输入表进行替换
UnHookMoudleAPI(me.hModule,*pHookAPIEntry) ;
}
};
return S_OK ;
}
HRESULT CHookAPI::HookMoudleAPI(const HookAPIEntry & hookentry){
CToolhelp th(TH32CS_SNAPMODULE, GetCurrentProcessId());
MODULEENTRY32 me = { sizeof(me) };
for (BOOL fOk = th.ModuleFirst(&me); fOk; fOk = th.ModuleNext(&me))
{
//TRACE(me.szModule);
// 监控所有的有关文件读写情况
HookMoudleAPI(me.hModule,hookentry) ;
};
return S_OK ;
}
HRESULT CHookAPI::HookMoudleAPI(HMODULE hmoudle,const HookAPIEntry & hookentry){
return ReplaceMoudleAPI(hmoudle,hookentry.strMoudleName,hookentry.pOriginalProc,hookentry.pNewProc);
}
HRESULT CHookAPI::UnHookMoudleAPI(const HookAPIEntry & hookentry){
CToolhelp th(TH32CS_SNAPMODULE, GetCurrentProcessId());
MODULEENTRY32 me = { sizeof(me) };
for (BOOL fOk = th.ModuleFirst(&me); fOk; fOk = th.ModuleNext(&me))
{
//TRACE(me.szModule);
// 监控所有的有关文件读写情况
UnHookMoudleAPI(me.hModule,hookentry) ;
};
return S_OK ;
}
HRESULT CHookAPI::UnHookMoudleAPI(HMODULE hmoudle,const HookAPIEntry & hookentry){
return ReplaceMoudleAPI(hmoudle,hookentry.strMoudleName,hookentry.pNewProc,hookentry.pOriginalProc);
}
HRESULT CHookAPI::ReplaceMoudleAPI(HMODULE hmoduleCaller, const CString &strMoudleName,
PROC pCurAddress,PROC pNewAddress){
// 找到的IAT表
ULONG lsize = 0 ;
PIMAGE_SECTION_HEADER psecHeader ;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)
ImageDirectoryEntryToDataEx(hmoduleCaller,TRUE,IMAGE_DIRECTORY_ENTRY_IMPORT,&lsize,&psecHeader);
if( pImportDesc == NULL ){
// ASSERT(FALSE) ;
return S_FALSE ;
}
// 找输入表的相应的模块
while( pImportDesc->Name != 0){
// RVA
CString strdllname = (PSTR)((PBYTE)hmoduleCaller + pImportDesc->Name);
if( strdllname.CompareNoCase(strMoudleName) == 0 )
break ;
pImportDesc ++ ;
}
if( pImportDesc->Name == 0){
//ASSERT(FALSE) ;
return S_FALSE ;
}
// 找输入表的函数地址,FirstThunk为RVA
PIMAGE_THUNK_DATA pThunk=(PIMAGE_THUNK_DATA)((PBYTE)hmoduleCaller + pImportDesc->FirstThunk);
PROC* ppfn;
while( pThunk->u1.Function != 0 ){
ppfn = (PROC*)&pThunk->u1.Function;
if( *ppfn == pCurAddress ){
// 找到了这个输入表中的函数地址,将其修改一下
MEMORY_BASIC_INFORMATION mbi;
VirtualQuery(ppfn,&mbi,sizeof(MEMORY_BASIC_INFORMATION));
VirtualProtect(mbi.BaseAddress,mbi.RegionSize,PAGE_READWRITE,&mbi.Protect);
*ppfn=*pNewAddress;
DWORD dwOldProtect;
VirtualProtect(mbi.BaseAddress,mbi.RegionSize,mbi.Protect,&dwOldProtect);
return S_OK ;
}
pThunk ++ ;
}
return S_OK ;

}


--------------------------------------------

------------- ToolHelp.h -----------------

--------------------------------------------

#pragma once
#include "stdafx.h"
///////////////////////////////////////////////////////////////////////////////
#include <tlhelp32.h>
#include <tchar.h>
#define chINRANGE(low, Num, High) (((low) <= (Num)) && ((Num) <= (High)))
///////////////////////////////////////////////////////////////////////////////

class CToolhelp {
private:
HANDLE m_hSnapshot;
public:
CToolhelp(DWORD dwFlags = 0, DWORD dwProcessID = 0);
~CToolhelp();
BOOL CreateSnapshot(DWORD dwFlags, DWORD dwProcessID = 0);

BOOL ProcessFirst(PPROCESSENTRY32 ppe) const;
BOOL ProcessNext(PPROCESSENTRY32 ppe) const;
BOOL ProcessFind(DWORD dwProcessId, PPROCESSENTRY32 ppe) const;
BOOL ModuleFirst(PMODULEENTRY32 pme) const;
BOOL ModuleNext(PMODULEENTRY32 pme) const;
BOOL ModuleFind(PVOID pvBaseAddr, PMODULEENTRY32 pme) const;
BOOL ModuleFind(PTSTR pszModName, PMODULEENTRY32 pme) const;

BOOL ThreadFirst(PTHREADENTRY32 pte) const;
BOOL ThreadNext(PTHREADENTRY32 pte) const;

BOOL HeapListFirst(PHEAPLIST32 phl) const;
BOOL HeapListNext(PHEAPLIST32 phl) const;
int  HowManyHeaps() const;
// Note: The heap block functions do not reference a snapshot and
// just walk the process's heap from the beginning each time. Infinite
// loops can occur if the target process changes its heap while the
// functions below are enumerating the blocks in the heap.
BOOL HeapFirst(PHEAPENTRY32 phe, DWORD dwProcessID,
UINT_PTR dwHeapID) const;
BOOL HeapNext(PHEAPENTRY32 phe) const;
int  HowManyBlocksInHeap(DWORD dwProcessID, DWORD dwHeapId) const;
BOOL IsAHeap(HANDLE hProcess, PVOID pvBlock, PDWORD pdwFlags) const;
public:
static BOOL EnableDebugPrivilege(BOOL fEnable = TRUE);
static BOOL ReadProcessMemory(DWORD dwProcessID, LPCVOID pvBaseAddress,
PVOID pvBuffer, DWORD cbRead, PDWORD pdwNumberOfBytesRead = NULL);
};


--------------------------------------------

------------- ToolHelp.cpp -----------------

--------------------------------------------

///////////////////////////////////////////////////////////////////////////////

inline CToolhelp::CToolhelp(DWORD dwFlags, DWORD dwProcessID) {
m_hSnapshot = INVALID_HANDLE_VALUE;
CreateSnapshot(dwFlags, dwProcessID);
}

///////////////////////////////////////////////////////////////////////////////

inline CToolhelp::~CToolhelp() {
if (m_hSnapshot != INVALID_HANDLE_VALUE)
CloseHandle(m_hSnapshot);
}

///////////////////////////////////////////////////////////////////////////////

inline int CToolhelp::CreateSnapshot(DWORD dwFlags, DWORD dwProcessID) {
if (m_hSnapshot != INVALID_HANDLE_VALUE)
CloseHandle(m_hSnapshot);
if (dwFlags == 0) {
m_hSnapshot = INVALID_HANDLE_VALUE;
} else {
m_hSnapshot = CreateToolhelp32Snapshot(dwFlags, dwProcessID);
}
return(m_hSnapshot != INVALID_HANDLE_VALUE);
}

///////////////////////////////////////////////////////////////////////////////

inline BOOL CToolhelp::EnableDebugPrivilege(BOOL fEnable) {
// Enabling the debug privilege allows the application to see
// information about service applications
BOOL fOk = FALSE;    // Assume function fails
HANDLE hToken;
// Try to open this process's access token
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES,
&hToken)) {
// Attempt to modify the "Debug" privilege
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
tp.Privileges[0].Attributes = fEnable ? SE_PRIVILEGE_ENABLED : 0;
AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
fOk = (GetLastError() == ERROR_SUCCESS);
CloseHandle(hToken);
}
return(fOk);
}

///////////////////////////////////////////////////////////////////////////////

inline BOOL CToolhelp::ReadProcessMemory(DWORD dwProcessID,
LPCVOID pvBaseAddress, PVOID pvBuffer, DWORD cbRead,
PDWORD pdwNumberOfBytesRead) {
return(Toolhelp32ReadProcessMemory(dwProcessID, pvBaseAddress, pvBuffer,
cbRead, pdwNumberOfBytesRead));
}

///////////////////////////////////////////////////////////////////////////////

inline BOOL CToolhelp::ProcessFirst(PPROCESSENTRY32 ppe) const {
BOOL fOk = Process32First(m_hSnapshot, ppe);
if (fOk && (ppe->th32ProcessID == 0))
fOk = ProcessNext(ppe); // Remove the "[System Process]" (PID = 0)
return(fOk);
}

inline BOOL CToolhelp::ProcessNext(PPROCESSENTRY32 ppe) const {
BOOL fOk = Process32Next(m_hSnapshot, ppe);
if (fOk && (ppe->th32ProcessID == 0))
fOk = ProcessNext(ppe); // Remove the "[System Process]" (PID = 0)
return(fOk);
}

inline BOOL CToolhelp::ProcessFind(DWORD dwProcessId, PPROCESSENTRY32 ppe)
const {
BOOL fFound = FALSE;
for (BOOL fOk = ProcessFirst(ppe); fOk; fOk = ProcessNext(ppe)) {
fFound = (ppe->th32ProcessID == dwProcessId);
if (fFound) break;
}
return(fFound);
}

///////////////////////////////////////////////////////////////////////////////

inline BOOL CToolhelp::ModuleFirst(PMODULEENTRY32 pme) const {
return(Module32First(m_hSnapshot, pme));
}

inline BOOL CToolhelp::ModuleNext(PMODULEENTRY32 pme) const {
return(Module32Next(m_hSnapshot, pme));
}
inline BOOL CToolhelp::ModuleFind(PVOID pvBaseAddr, PMODULEENTRY32 pme) const {
BOOL fFound = FALSE;
for (BOOL fOk = ModuleFirst(pme); fOk; fOk = ModuleNext(pme)) {
fFound = (pme->modBaseAddr == pvBaseAddr);
if (fFound) break;
}
return(fFound);
}
inline BOOL CToolhelp::ModuleFind(PTSTR pszModName, PMODULEENTRY32 pme) const {
BOOL fFound = FALSE;
for (BOOL fOk = ModuleFirst(pme); fOk; fOk = ModuleNext(pme)) {
fFound = (lstrcmpi(pme->szModule,  pszModName) == 0) ||
(lstrcmpi(pme->szExePath, pszModName) == 0);
if (fFound) break;
}
return(fFound);
}

///////////////////////////////////////////////////////////////////////////////

inline BOOL CToolhelp::ThreadFirst(PTHREADENTRY32 pte) const {
return(Thread32First(m_hSnapshot, pte));
}

inline BOOL CToolhelp::ThreadNext(PTHREADENTRY32 pte) const {
return(Thread32Next(m_hSnapshot, pte));
}

///////////////////////////////////////////////////////////////////////////////

inline int CToolhelp::HowManyHeaps() const {
int nHowManyHeaps = 0;
HEAPLIST32 hl = { sizeof(hl) };
for (BOOL fOk = HeapListFirst(&hl); fOk; fOk = HeapListNext(&hl))
nHowManyHeaps++;
return(nHowManyHeaps);
}

inline int CToolhelp::HowManyBlocksInHeap(DWORD dwProcessID,
DWORD dwHeapID) const {
int nHowManyBlocksInHeap = 0;
HEAPENTRY32 he = { sizeof(he) };
BOOL fOk = HeapFirst(&he, dwProcessID, dwHeapID);
for (; fOk; fOk = HeapNext(&he))
nHowManyBlocksInHeap++;
return(nHowManyBlocksInHeap);
}

inline BOOL CToolhelp::HeapListFirst(PHEAPLIST32 phl) const {
return(Heap32ListFirst(m_hSnapshot, phl));
}

inline BOOL CToolhelp::HeapListNext(PHEAPLIST32 phl) const {
return(Heap32ListNext(m_hSnapshot, phl));
}

inline BOOL CToolhelp::HeapFirst(PHEAPENTRY32 phe, DWORD dwProcessID,
UINT_PTR dwHeapID) const {
return(Heap32First(phe, dwProcessID, dwHeapID));
}

inline BOOL CToolhelp::HeapNext(PHEAPENTRY32 phe) const {
return(Heap32Next(phe));
}

inline BOOL CToolhelp::IsAHeap(HANDLE hProcess, PVOID pvBlock,
PDWORD pdwFlags) const {
HEAPLIST32 hl = { sizeof(hl) };
for (BOOL fOkHL = HeapListFirst(&hl); fOkHL; fOkHL = HeapListNext(&hl)) {
HEAPENTRY32 he = { sizeof(he) };
BOOL fOkHE = HeapFirst(&he, hl.th32ProcessID, hl.th32HeapID);
for (; fOkHE; fOkHE = HeapNext(&he)) {
MEMORY_BASIC_INFORMATION mbi;
VirtualQueryEx(hProcess, (PVOID) he.dwAddress, &mbi, sizeof(mbi));
if (chINRANGE(mbi.AllocationBase, pvBlock,
(PBYTE) mbi.AllocationBase + mbi.RegionSize)) {
*pdwFlags = hl.dwFlags;
return(TRUE);
}
}
}
return(FALSE);
}


//////////////////////////////// End of File //////////////////////////////////

Jekkay Hu,胡杨

2010-12-18
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐