Retrieve HttpOnly Session Cookie in WebBrowser

Introduction

In order to help mitigate the risk of cross-site scripting, a new feature has been introduced in Microsoft Internet Explorer 6 SP1. This feature is a new attribute for cookies which prevents them from being accessed through client-side script. A cookie with this attribute is called an HTTP-only cookie.  

The demo program accesses the webpage http://www.codeproject.com/index.aspx at startup, and gets the response HTTP headers: 

Collapse Copy Code
HTTP/1.1 200 OK
Cache-Control: private
Date: Fri, 31 Jul 2009 08:38:03 GMT
Content-Type: text/html; charset=utf-8
Server: Microsoft-IIS/6.0
X-Powered-By: ASP.NET
X-AspNet-Version: 2.0.50727
Set-Cookie: SessionGUID=03e40347-534c-4160-8936-19745e6dae20; path=/
Set-Cookie: ASP.NET_SessionId=beegcn2fu2pmjjeyvdu1uzrv; path=/; HttpOnly
Transfer-Encoding: chunked  

The cookie ASP.Net_SessionId is marked as HttpOnly, and it cannot be obtained by IHTMLDocument2::get_cookie method. 

Background 

The WebBrowser(mshtml.dll) accesses the HTTP web server by invoking the methods exposed by WININET.dll.

Hooking the methods exposed by WININET.DLL gives the ability to interact with each request sending to server, including the AJAX request! 

The probable call sequence is shown as below: 

Collapse Copy Code
1=> InternetOpen
2=> InternetConnect
3=> HttpOpenRequest
// .............
X=> InternetCloseHandle  
By hooking these methods, it is possible for us to detect each request sent to the web server, and inject our codes before / after each request.

Why not WINSOCK? It is easier to hook methods exposed by WININET.DLL than hooking WINSOCK methods, since the former wraps the HTTP protocol.

There are two different ways in hooking, inline hook and routine hook. Inline hook is universal for almost all of the cases, however, routine hook is more robust. The sample implements it by modifying the IAT(Import Address Table), one of the routine hooks.

If you need more details about IAT hook, here is an article for reference. 

Code Snippets

Collapse Copy Code
// required header files and library files
#include <WinInet.h>  
#pragma comment( lib, "WinInet.lib")  
#include <Dbghelp.h>  
#include <DelayImp.h>  
#pragma comment( lib, "Dbghelp.lib")  
#include <Psapi.h>  
#pragma comment( lib, "Psapi.lib")  
Collapse Copy Code
// The following macro can be found in IE8 SDK
#ifndef INTERNET_COOKIE_NON_SCRIPT
#define INTERNET_COOKIE_NON_SCRIPT      0x00001000
#endif

#ifndef INTERNET_COOKIE_HTTPONLY
#define INTERNET_COOKIE_HTTPONLY      0x00002000
#endif
Collapse Copy Code
// The following macro can be found in DDK/WDK header files
typedef struct _STRING {  
    USHORT  Length;  
    USHORT  MaximumLength;  
    PCHAR   Buffer;  
} ANSI_STRING, *PANSI_STRING;  
typedef struct _LSA_UNICODE_STRING {  
    USHORT Length;  
    USHORT MaximumLength;  
    PWSTR  Buffer;  
}LSA_UNICODE_STRING, *PLSA_UNICODE_STRING, UNICODE_STRING, *PUNICODE_STRING;  
typedef LONG NTSTATUS;  

#ifndef STATUS_SUCCESS
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) 
#endif
Collapse Copy Code
// The function pointer statements for those function to hook
typedef NTSTATUS (WINAPI* PFN_LdrGetProcedureAddress)
	(IN HMODULE ModuleHandle, IN PANSI_STRING FunctionName OPTIONAL, 
	IN WORD Oridinal OPTIONAL, OUT PVOID *FunctionAddress );  
typedef NTSTATUS (WINAPI* PFN_LdrLoadDll)(IN PWCHAR PathToFile OPTIONAL, 
	IN ULONG Flags OPTIONAL, IN PUNICODE_STRING ModuleFileName, 
	OUT PHANDLE ModuleHandle);  
typedef BOOL (WINAPI* PFN_HttpSendRequestA)(HINTERNET hRequest, 
	LPCSTR lpszHeaders, DWORD dwHeadersLength, 
	LPVOID lpOptional, DWORD dwOptionalLength );  
typedef BOOL (WINAPI* PFN_HttpSendRequestW)
	(HINTERNET hRequest, LPCWSTR lpszHeaders, 
	DWORD dwHeadersLength, LPVOID lpOptional, DWORD dwOptionalLength );  
typedef BOOL (WINAPI* PFN_HttpSendRequestExA)
	( __in HINTERNET hRequest, __in_opt LPINTERNET_BUFFERSA lpBuffersIn, 
	__out_opt LPINTERNET_BUFFERSA lpBuffersOut, __in DWORD dwFlags, 
	__in_opt DWORD_PTR dwContext);  
typedef BOOL (WINAPI* PFN_HttpSendRequestExW)
	( __in HINTERNET hRequest, __in_opt LPINTERNET_BUFFERSW lpBuffersIn, 
	__out_opt LPINTERNET_BUFFERSW lpBuffersOut, __in DWORD dwFlags, 
	__in_opt DWORD_PTR dwContext);  
typedef BOOL (WINAPI* PFN_HttpEndRequestA)( __in HINTERNET hRequest, 
	__out_opt LPINTERNET_BUFFERSA lpBuffersOut, __in DWORD dwFlags, 
	__in_opt DWORD_PTR dwContext);  
typedef BOOL (WINAPI* PFN_HttpEndRequestW)( __in HINTERNET hRequest, 
	__out_opt LPINTERNET_BUFFERSW lpBuffersOut, __in DWORD dwFlags, 
	__in_opt DWORD_PTR dwContext);  
typedef HINTERNET (WINAPI* PFN_HttpOpenRequestA)
	(__in HINTERNET hConnect,__in_opt LPCSTR lpszVerb, 
	__in_opt LPCSTR lpszObjectName, __in_opt LPCSTR lpszVersion, 
	__in_opt LPCSTR lpszReferrer, __in_z_opt LPCSTR FAR * lplpszAcceptTypes, 
	__in DWORD dwFlags, __in_opt DWORD_PTR dwContext);  
typedef HINTERNET (WINAPI* PFN_HttpOpenRequestW)
	(__in HINTERNET hConnect,__in_opt LPCWSTR lpszVerb,
	__in_opt LPCWSTR lpszObjectName,__in_opt LPCWSTR lpszVersion,
	__in_opt LPCWSTR lpszReferrer,__in_z_opt LPCWSTR FAR * lplpszAcceptTypes,
	__in DWORD dwFlags, __in_opt DWORD_PTR dwContext);  
typedef HINTERNET (WINAPI* PFN_InternetConnectA)
	(__in HINTERNET hInternet,__in LPCSTR lpszServerName,
	__in INTERNET_PORT nServerPort,__in_opt LPCSTR lpszUserName,
	__in_opt LPCSTR lpszPassword,__in DWORD dwService,__in DWORD dwFlags,
	__in_opt DWORD_PTR dwContext);  
typedef HINTERNET (WINAPI* PFN_InternetConnectW)
	(__in HINTERNET hInternet,__in LPCWSTR lpszServerName,
	__in INTERNET_PORT nServerPort,__in_opt LPCWSTR lpszUserName,
	__in_opt LPCWSTR lpszPassword,__in DWORD dwService,__in DWORD dwFlags,
	__in_opt DWORD_PTR dwContext);  
typedef BOOL (WINAPI* PFN_HttpAddRequestHeadersA)
	(__in HINTERNET hRequest,__in_ecount(dwHeadersLength) LPCSTR lpszHeaders,
	__in DWORD dwHeadersLength,__in DWORD dwModifiers);  
typedef BOOL (WINAPI* PFN_HttpAddRequestHeadersW)
	(__in HINTERNET hRequest,__in_ecount(dwHeadersLength) LPCWSTR lpszHeaders,
	__in DWORD dwHeadersLength,__in DWORD dwModifiers);  
typedef HINTERNET (WINAPI* PFN_InternetOpenUrlA)
	(HINTERNET hInternet,LPCSTR lpszUrl, LPCSTR lpszHeaders, 
	DWORD dwHeadersLength, DWORD dwFlags, DWORD_PTR dwContext);
typedef HINTERNET (WINAPI* PFN_InternetOpenUrlW)
	(HINTERNET hInternet,LPCWSTR lpszUrl,LPCWSTR lpszHeaders,
	DWORD dwHeadersLength,DWORD dwFlags,DWORD_PTR dwContext);
typedef HINTERNET (WINAPI* PFN_InternetOpenA)
	( LPCSTR lpszAgent, DWORD dwAccessType, LPCSTR lpszProxy, 
	LPCSTR lpszProxyBypass, DWORD dwFlags );
typedef HINTERNET (WINAPI* PFN_InternetOpenW)
	( LPCWSTR lpszAgent, DWORD dwAccessType, LPCWSTR lpszProxy, 
	LPCWSTR lpszProxyBypass, DWORD dwFlags );
typedef BOOL (WINAPI* PFN_InternetCloseHandle)(__in HINTERNET hInternet);
typedef BOOL (WINAPI* PFN_InternetSetCookieA)
	(LPCSTR lpszUrl, LPCSTR lpszCookieName, LPCSTR lpszCookieData );
typedef BOOL (WINAPI* PFN_InternetSetCookieW)
	( LPCWSTR lpszUrl, LPCWSTR lpszCookieName, LPCWSTR lpszCookieData);
typedef DWORD (WINAPI* PFN_InternetSetCookieExA)
	(LPCSTR lpszUrl, LPCSTR lpszCookieName, 
	LPCSTR lpszCookieData, DWORD dwFlags, DWORD_PTR dwReserved);
typedef DWORD (WINAPI* PFN_InternetSetCookieExW)
	( LPCWSTR lpszUrl, LPCWSTR lpszCookieName, 
	LPCWSTR lpszCookieData, DWORD dwFlags, DWORD_PTR dwReserved );
Collapse Copy Code
class CWininetHook
{
public:
	CWininetHook(void);
	~CWininetHook(void);

private:
	// variables to store the original function address
	static PFN_LdrLoadDll s_pfnLdrLoadDll;  
	static PFN_LdrGetProcedureAddress s_pfnLdrGetProcedureAddress;  
	static PFN_HttpSendRequestA s_pfnHttpSendRequestA;  
	static PFN_HttpSendRequestW s_pfnHttpSendRequestW;  
	static PFN_HttpSendRequestExA s_pfnHttpSendRequestExA;  
	static PFN_HttpSendRequestExW s_pfnHttpSendRequestExW;  
	static PFN_HttpEndRequestA s_pfnHttpEndRequestA;  
	static PFN_HttpEndRequestW s_pfnHttpEndRequestW;  
	static PFN_HttpOpenRequestA s_pfnHttpOpenRequestA;  
	static PFN_HttpOpenRequestW s_pfnHttpOpenRequestW;  
	static PFN_InternetConnectA s_pfnInternetConnectA;  
	static PFN_InternetConnectW s_pfnInternetConnectW;    
	static PFN_HttpAddRequestHeadersA s_pfnHttpAddRequestHeadersA;  
	static PFN_HttpAddRequestHeadersW s_pfnHttpAddRequestHeadersW;  
	static PFN_InternetOpenUrlA s_pfnInternetOpenUrlA;  
	static PFN_InternetOpenUrlW s_pfnInternetOpenUrlW;  
	static PFN_InternetOpenA s_pfnInternetOpenA;  
	static PFN_InternetOpenW s_pfnInternetOpenW;  
	static PFN_InternetCloseHandle s_pfnInternetCloseHandle;
	static PFN_InternetSetCookieA s_pfnInternetSetCookieA;
	static PFN_InternetSetCookieW s_pfnInternetSetCookieW;
	static PFN_InternetSetCookieExA s_pfnInternetSetCookieExA;
	static PFN_InternetSetCookieExW s_pfnInternetSetCookieExW;
};

static CWininetHook g_oHook;  

// initialize the static variables  
PFN_LdrLoadDll CWininetHook::s_pfnLdrLoadDll = NULL;  
PFN_LdrGetProcedureAddress CWininetHook::s_pfnLdrGetProcedureAddress = NULL;  

// save the original address
PFN_HttpSendRequestA CWininetHook::s_pfnHttpSendRequestA = HttpSendRequestA;  
PFN_HttpSendRequestW CWininetHook::s_pfnHttpSendRequestW = HttpSendRequestW;  
PFN_HttpAddRequestHeadersA CWininetHook::s_pfnHttpAddRequestHeadersA = 
						HttpAddRequestHeadersA;  
PFN_HttpAddRequestHeadersW CWininetHook::s_pfnHttpAddRequestHeadersW = 
						HttpAddRequestHeadersW;  
PFN_HttpSendRequestExA CWininetHook::s_pfnHttpSendRequestExA = HttpSendRequestExA;  
PFN_HttpSendRequestExW CWininetHook::s_pfnHttpSendRequestExW = HttpSendRequestExW;  
PFN_HttpEndRequestA CWininetHook::s_pfnHttpEndRequestA = HttpEndRequestA;  
PFN_HttpEndRequestW CWininetHook::s_pfnHttpEndRequestW = HttpEndRequestW;  
PFN_HttpOpenRequestA CWininetHook::s_pfnHttpOpenRequestA = HttpOpenRequestA;  
PFN_HttpOpenRequestW CWininetHook::s_pfnHttpOpenRequestW = HttpOpenRequestW;  
PFN_InternetConnectA CWininetHook::s_pfnInternetConnectA = InternetConnectA;  
PFN_InternetConnectW CWininetHook::s_pfnInternetConnectW = InternetConnectW;  
PFN_InternetOpenUrlA CWininetHook::s_pfnInternetOpenUrlA = InternetOpenUrlA;  
PFN_InternetOpenUrlW CWininetHook::s_pfnInternetOpenUrlW = InternetOpenUrlW;  
PFN_InternetOpenA CWininetHook::s_pfnInternetOpenA = InternetOpenA;
PFN_InternetOpenW CWininetHook::s_pfnInternetOpenW = InternetOpenW;
PFN_InternetCloseHandle CWininetHook::s_pfnInternetCloseHandle = InternetCloseHandle;
PFN_InternetSetCookieA CWininetHook::s_pfnInternetSetCookieA = InternetSetCookieA;
PFN_InternetSetCookieW CWininetHook::s_pfnInternetSetCookieW = InternetSetCookieW;
PFN_InternetSetCookieExA CWininetHook::s_pfnInternetSetCookieExA = InternetSetCookieExA;
PFN_InternetSetCookieExW CWininetHook::s_pfnInternetSetCookieExW = InternetSetCookieExW;
Collapse Copy Code
 // Enumerate all the loaded modules in the current process,
 // and search for the IAT to be processed
void CWininetHook::ReplaceIATEntryForAll(LPCSTR lpszDllName, 
				LPVOID pfnCurrent, LPVOID pfnNew)  
{  
    HMODULE hMods[1024] = {0};  
    DWORD cbNeeded;  
    HANDLE hProcess = ::GetCurrentProcess();  
    if( ::EnumProcessModules( hProcess, hMods, sizeof(hMods), &cbNeeded))  
    {  
        for ( UINT i = 0; i < (cbNeeded / sizeof(HMODULE)); i++ )  
        {  
            ReplaceIATEntryInImageImportTable( hMods[i]  
            , lpszDllName  
                , pfnCurrent  
                , pfnNew  
                );  
        }  
    }  
}  
Collapse Copy Code
// search in IMAGE_IMPORT_DESCRIPTOR for the special module
BOOL CWininetHook::ReplaceIATEntryInImageImportTable( HANDLE hBaseAddress  
                                                     , LPCSTR lpszDllName  
                                                     , LPVOID pfnCurrent  
                                                     , LPVOID pfnNew  
                                                     )  
{  
    ASSERT(hBaseAddress && lpszDllName && pfnCurrent && pfnNew );  
    // retrieve IMAGE_IMPORT_DESCRIPTOR  
    DWORD dwSize = 0;  
    PIMAGE_SECTION_HEADER pFoundHeader = NULL;  
    PIMAGE_IMPORT_DESCRIPTOR pImgImportDescriptor  
        = (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToDataEx( hBaseAddress  
        , TRUE  
        , IMAGE_DIRECTORY_ENTRY_IMPORT  
        , &dwSize  
        , &pFoundHeader  
        );  
    if( pImgImportDescriptor == NULL ){ return FALSE; }  
    while (pImgImportDescriptor->Name)  
    {  
        if ( _strcmpi((CHAR*)((PBYTE)hBaseAddress+pImgImportDescriptor->Name), 
							lpszDllName) == 0 )  
        {  
            break; // found  
        }  
        ++pImgImportDescriptor;  
    }  
    // NOTE:
    // If the special module can not be found in IMAGE_DIRECTORY_ENTRY_IMPORT
    // Then should try to search it in IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
    if( !pImgImportDescriptor->Name )  
        return ReplaceIATEntryInDelayImageImportTable
		( hBaseAddress, lpszDllName, pfnCurrent, pfnNew);  

    // retrieve IAT  
    PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)
	(((LPBYTE)hBaseAddress) + pImgImportDescriptor->FirstThunk);  

    // enumerate functions in the IAT
    while(pThunk->u1.Function)    
    {    
        PDWORD lpAddr = (PDWORD)&(pThunk->u1.Function);    
        if(*lpAddr == (DWORD)pfnCurrent)    
        {  
            // modify the address
            ::WriteProcessMemory(::GetCurrentProcess()  
                , lpAddr  
                , &pfnNew  
                , sizeof(DWORD)  
                , NULL  
                );  
            return TRUE;  
        }     
        pThunk++;    
    }  
    return FALSE;  
}   

For the delay-load DLLs, search in IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT.

Collapse Copy Code
// search in IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT for the special module
BOOL CWininetHook::ReplaceIATEntryInDelayImageImportTable( HANDLE hBaseAddress  
                                                          , LPCSTR lpszDllName  
                                                          , LPVOID pfnCurrent  
                                                          , LPVOID pfnNew  
                                                          )  
{  
    ASSERT(hBaseAddress && lpszDllName && pfnCurrent && pfnNew );  
    // retrieve IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT  
    DWORD dwSize = 0;  
    PIMAGE_SECTION_HEADER pFoundHeader = NULL;  
    PImgDelayDescr pImgDelayDescr  
        = (PImgDelayDescr)ImageDirectoryEntryToDataEx( hBaseAddress  
        , TRUE  
        , IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT  
        , &dwSize  
        , &pFoundHeader  
        );  
    if( pImgDelayDescr == NULL ){ return FALSE; }  
    while (pImgDelayDescr->rvaDLLName)  
    {  
        if ( _strcmpi((CHAR*)((PBYTE)hBaseAddress+pImgDelayDescr->rvaDLLName), 
							lpszDllName) == 0 )  
        {  
            break;  
        }  
        ++pImgDelayDescr;  
    }  
    // Not found in IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
    if( !pImgDelayDescr->rvaDLLName )  
        return FALSE;  

    // retrieve IAT  
    PIMAGE_THUNK_DATA pThunk = NULL;  
    if( (pImgDelayDescr->grAttrs & dlattrRva) == 0 )  
        return FALSE;  
    pThunk = (PIMAGE_THUNK_DATA)(((LPBYTE)hBaseAddress) + pImgDelayDescr->rvaIAT); 

    // enumerate functions in the IAT
    while(pThunk->u1.Function)    
    {    
        PDWORD lpAddr = (PDWORD)&(pThunk->u1.Function);    
        if(*lpAddr == (DWORD)pfnCurrent)    
        {  
            // modify the address
            ::WriteProcessMemory(::GetCurrentProcess()  
                , lpAddr  
                , &pfnNew  
                , sizeof(DWORD)  
                , NULL  
                );  
            return TRUE;
        }     
        pThunk++;    
    }

    return FALSE;  
}  

The Windows PE loader may load extra DLLs into the process after process startup, also the function address may be retrieved by the GetProcAddress API, so it is necessary to modify the IAT in the new loaded DLL.

Generally, LoadLibary(Ex) and GetProcAddress are hooked. However, the sample hooks LdrLoadDll and LdrGetProcedureAddress instead. LoadLibary(Ex) and GetProcAddress are implemented depending on LdrLoadDll and LdrGetProcedureAddress, which is exposed in ntdll.dll, the gate from Ring3 to Ring0.

Collapse Copy Code
// Process those modules loaded dynamically, 
NTSTATUS WINAPI CWininetHook::_LdrLoadDll
	(IN PWCHAR PathToFile OPTIONAL, IN ULONG Flags OPTIONAL, 
	IN PUNICODE_STRING ModuleFileName, OUT PHANDLE ModuleHandle)  
{     
    NTSTATUS ntStatus = s_pfnLdrLoadDll
	( PathToFile, Flags, ModuleFileName, ModuleHandle);  
    if( ntStatus == STATUS_SUCCESS && (Flags & LOAD_LIBRARY_AS_DATAFILE) == 0 )  
    {
#ifdef _DEBUG
        if( ModuleFileName->Length > 0 )
        {
            WCHAR wszPath[MAX_PATH] = {0};  
            memcpy_s( wszPath, MAX_PATH*sizeof(WCHAR), 
		ModuleFileName->Buffer, MAX_PATH*sizeof(WCHAR));
            TRACE( L"LdrLoadDll %s\n", wszPath);
        }        
#endif                

        HANDLE hDll = *ModuleHandle;  
        ReplaceIATEntryInImageImportTable( hDll, "NTDLL.DLL", 
			s_pfnLdrLoadDll, &CWininetHook::_LdrLoadDll);  
        //......
    }  
    return ntStatus;  
}  

// Return the user-provided function address instead of the read one
NTSTATUS WINAPI CWininetHook::_LdrGetProcedureAddress
	(IN HMODULE ModuleHandle, IN PANSI_STRING FunctionName OPTIONAL, 
	IN WORD Oridinal OPTIONAL, OUT PVOID *FunctionAddress )  
{  
    NTSTATUS ntStatus = s_pfnLdrGetProcedureAddress
	( ModuleHandle, FunctionName, Oridinal, FunctionAddress);  
    if( ntStatus == STATUS_SUCCESS )  
    {  
        TCHAR tszPath[MAX_PATH] = {0};  
        if( GetModuleFileName( ModuleHandle, tszPath, MAX_PATH) )  
        {  
            CString strFile(tszPath);  
            int nFind = strFile.ReverseFind('\\');  
            if( nFind > 0 )  
                strFile = strFile.Mid(nFind+1);  
            if( strFile.CompareNoCase(_T("WININET.dll")) == 0 )  
            {  
                CHAR szFunName[1024] = {0};  
                memcpy( szFunName, FunctionName->Buffer, FunctionName->Length );  
#ifdef _DEBUG
                USES_CONVERSION;
                TRACE( _T("LdrGetProcedureAddress %s\n"), A2T(szFunName));
#endif                

                TRY_GET_ADDRESS( "HttpSendRequestA", 
		s_pfnHttpSendRequestA, _HttpSendRequestA,  PFN_HttpSendRequestA);
                // ......
             }  
        }  
    }  

    return ntStatus;  
}   

Now, we can inject into every request, including AJAX requests. How to get/set the HTTP-Only cookies when a request is being sent? There is a new added flag INTERNET_COOKIE_HTTPONLY in IE8 SDK for InternetGetCookieEx / InternetSetCookieEx.

Collapse Copy Code
TCHAR tszTemp[10240] = {0};
DWORD dwSize = sizeof(tszTemp);

InternetGetCookieEx( _T("http://www.codeproject.com/index.aspx")
                    , _T("ASP.NET_SessionId")
                    , tszTemp
                    , &dwSize
                    , INTERNET_COOKIE_HTTPONLY
                    , NULL
                    );  

InternetSetCookieEx( strUrl
                    , _T("ASP.NET_SessionId")
                    , _T("XXXXXXXXX")
                    , INTERNET_COOKIE_HTTPONLY
                    , NULL			
                    ); 

Though MSDN says "Requires Internet Explorer 8.0 or later", it does work in my Internet Explorer 7.0.

It should just have something to do with WININET.dll, nothing to do with mshtml.dll

posted on 2010-06-01 11:44  carekee  阅读(1810)  评论(0编辑  收藏  举报