首页 > 系统相关 >SSDT Hook—— 本质上和inline hook没有区别,无非是在内核层面而已!注意Windows Vista X64 驱动需要签名,32位才能用

SSDT Hook—— 本质上和inline hook没有区别,无非是在内核层面而已!注意Windows Vista X64 驱动需要签名,32位才能用

时间:2023-01-02 21:22:37浏览次数:69  
标签:Vista ULONGLONG 函数 32 hook NtTerminateProcess KeServiceDescriptorTable fffff803 

SSDT Hook

SSDT Hook属于内核层Hook,也是最底层的Hook。由于用户层的API最后实质也是调用内核API(Kernel32->Ntdll->Ntoskrnl),所以该Hook方法最为强大。不过值得注意的是

https://bbs.pediy.com/thread-187613.htm   win7以上64位下驱动需要签名,32 随便吧==》x86可以 . x64需要过PG 可以参考紫水晶的一篇帖子,通过已签名的驱动加载未签名驱动,然后任意执行..

查了下:为了确保系统的安全性与稳定性,微软从 Windows Vista X64 开始对系统内核增加了一定的限制,其主要增加了两种保护措施,一是KPP (内核补丁保护),KPP是机制其利用了PG(PatchGuard)技术,PG技术在x64系统下加入了内核哨兵,用于检测系统内核是否被恶意篡改(打补丁),如果发现被打了补丁,则会导致关键结构损毁直接蓝屏,二是DSE (驱动强制签名),DSE技术则是拒绝加载不包含正确签名的驱动。

这里有说明:https://www.cnblogs.com/LyShark/p/11639533.html

 

6.1SSDT原理

内核通过SSDT(System Service Descriptor Table)调用各种内核函数,SSDT就是一个函数表,只要得到一个索引值,就能根据这个索引值在该表中得到想要的函数地址。

 

下图0x80563520处就是ntoskrnl对应的服务描述符表结构SSDT。那么第一个32位的0x804e58a0则是SSDT Base,即SSDT的首地址。

 

通过对这些地址反汇编,就能得到相应的函数,下图中0x80591bfb是SSDT表中的第一个函数NtAcceptConnectPort的地址。

 

我们接下来试着寻找NtQuerySystemInformation的地址,首先反汇编ZwQuerySystemInformation,得知它要寻找SSDT中索引号为0xAD的地址。

 

从上面我们可以知道,NtQuerySystemInformation的索引号为0xAD,那么我们就可以算出NtQuerySystemInformation的地址:
0x80591bfb + 0xAD = 0x8056ff1

 

6.2SSDT Hook

其实内核层Hook并没想象中的那么高大上,Hook的原理相同,只不过Hook的对象不一样罢了。Hook步骤还是那5步:
1.修改内存属性为RWX。
2.拼接汇编码jmp [HookFunc]。
3.保存原代码头5个字节。
4.将头5个字节替换为2的汇编码。
5.恢复前5个字节。
6.恢复内存属性。

 

windows SSDT和驱动保护

 

  对于windwos逆向人员来说,不论是写外挂、写病毒/木马,都需要打开其他内存的空间,改写某些关键数据,达到改变其原有执行流程的目的。那么日常的工作肯定涉及到openprocess、readprocessmemory、writeprocessmemory等函数;这些函数都是怎么被调用的了?

  1、windows提供了大量的系统函数供3层的应用调用。这些函数被统一编号,入口地址放在一张表里,编号就是索引,通过编号就能找到函数的入口地址;64位的表结构可以通过windbg查看,如下:

     

      x64的SSDT表和32位比复杂了一些,为了便于读者理解,我用不同颜色(黄、绿、蓝、橙)做了标注;开发人员在3环调用openprocess、readprocessmemory、writeprocessmemory等函数,最终都会通过这个表找到对应的内核入口地址,进而跳转到内核空间执行;具体的函数实现可以通过逆向ntdll.dll、kerner32.dll、ntoskrl.exe等内核文件查看,这里不赘述(SSDT hook已经烂大街了,google一下资料大堆);各大厂商最初的驱动保护就是hook SSDT表的关键函数,一旦发现第三方程序打开自己的进程,直接返回false,达到保护自己进程数据不被篡改的目的;今天演示一下hook terminalprocess函数,让其无法关闭计算器或记事本的进程;

  2、通过微软官网查询得知:windwos提供的terminalProcess函数在kerner32.dll中:

 

       

   用IDA打开kernerl32.dll,切换到import,发现terminalProcess是从ntdll.dll导入的

       

  继续追查ntdll.dll,在export找到目标函数,双击进入函数体,如下:

   

  这个函数有两个重要信息:

  (1)mov eax, 2Ch: 2c=44,是系统调用号(同一函数在windwos不同版本的调用号是不一样的,我刚开始做实验时总是蓝屏,调试了好长时间才发现是调用号搞错了),也就是terminalprocess在SSDT中的编号,根据这个编号就能找到函数的入口地址(当然不是直接现成地展示在表内,而要经过一些简单地计算)

       (2)通过syscall进入内核

   3、核心代码(下面的参考【3】);注意:本人的测试环境是win10.0.16299.125,调用号是0x2c;其他版本的系统可能不一样,建议读者自己用IDA查查ntdll.dll,否则直接蓝屏

复制代码
#include "hook.h"
#include "asmUtil.h"

PSYSTEM_SERVICE_TABLE KeServiceDescriptorTable;
NTTERMINATEPROCESS NtTerminateProcess = NULL;
ULONG OldTpVal;

/*
用户点击关闭,系统会调用原NtTerminateProcess,并传递ProcessHandle和ExitStatus两个参数;但SSDT已经被改成了KeBugCheckEx,所以
会先执行KeBugCheckEx。进入后又执行jmp,跳转到我们自己定义的Fake_NtTerminateProcess。这时EIP变了好几次,但是堆栈一直没变,所以
Fake_NtTerminateProcess的参数就是原NtTerminateProcess的参数ProcessHandle和ExitStatus;所以后续也能重新调回NtTerminateProcess
走原来正常的流程;
*/
NTSTATUS __fastcall Fake_NtTerminateProcess(IN HANDLE ProcessHandle, IN NTSTATUS ExitStatus)
{
    //Dbg_Break();
    PEPROCESS Process;
    // 通过进程句柄来获取该进程所对应的FileObject对象,由于这里是进程对象,自然获得的是EPROCESS对象
    NTSTATUS st = ObReferenceObjectByHandle(ProcessHandle, 0, *PsProcessType, KernelMode, &Process, NULL);
    DbgPrint("\r\n-------Fake_NtTerminateProcess called! NT_SUCCESS(st):% d------------------------\r\n",NT_SUCCESS(st));
    DbgPrint("\r\n-------Fake_NtTerminateProcess called!  st:% d------------------------\r\n", st);
    if (NT_SUCCESS(st)) //#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
    {
        //if (!_stricmp(PsGetProcessImageFileName(Process), "Calculator.exe"))
        //if (strcmp(PsGetProcessImageFileName(Process), "Calculator.exe") == 0)
        DbgPrint("\r\n-------PsGetProcessImageFileName(Process):% s------------------------\r\n", PsGetProcessImageFileName(Process));
        if ((!_stricmp(PsGetProcessImageFileName(Process), "Calculator.exe"))
            || (!_stricmp(PsGetProcessImageFileName(Process), "notepad.exe")))
        {
            //ObDeReferenceObject(&Process);
            return STATUS_ACCESS_DENIED;
        }
        else 
        {
            //ObDeReferenceObject(&Process);
            /*这个已经被挂钩了,会不会形成死循环????*/
            return NtTerminateProcess(ProcessHandle, ExitStatus);
        }
            
    }
    else 
    {
        return STATUS_ACCESS_DENIED;
    }
        
}
/*关闭内核页面写保护*/
KIRQL WPOFFx64()
{
    KIRQL irql = KeRaiseIrqlToDpcLevel();
    UINT64 cr0 = __readcr0();
    cr0 &= 0xfffffffffffeffff;
    __writecr0(cr0);
    _disable();
    return irql;
}
/*打开内核页面写保护*/
void WPONx64(KIRQL irql)
{
    UINT64 cr0 = __readcr0();
    cr0 |= 0x10000;
    _enable();
    __writecr0(cr0);
    KeLowerIrql(irql);
}
// win10的变了,用下面的替代
ULONGLONG GetKeServiceDescriptorTable64_win10()
{
    PUCHAR StartSearchAddress = (PUCHAR)__readmsr(0xC0000082);
    PUCHAR EndSearchAddress = StartSearchAddress + 0x500;
    PUCHAR i = NULL;
    UCHAR b1 = 0, b2 = 0, b3 = 0;
    ULONG templong = 0;
    ULONGLONG addr = 0;
    for (i = StartSearchAddress; i < EndSearchAddress; i++)
    {
        if (MmIsAddressValid(i) && MmIsAddressValid(i + 1) && MmIsAddressValid(i + 2))
        {
            b1 = *i;
            b2 = *(i + 1);
            b3 = *(i + 2);
            if (b1 == 0x4c && b2 == 0x8d && b3 == 0x15) //4c8d15
            {
                memcpy(&templong, i + 3, 4);
                addr = (ULONGLONG)templong + (ULONGLONG)i + 7;
                return addr;
            }
        }
    }
    return 0;
}

/*
根据调用号找到目标内核函数地址
kd> x nt!KeServiceDescriptorTable
fffff803`4e9a1880 nt!KeServiceDescriptorTable = <no type information>
kd> dq fffff803`4e9a1880
fffff803`4e9a1880  fffff803`4e839c10 00000000`00000000
fffff803`4e9a1890  00000000`000001d0 fffff803`4e83a354

注意事项:
1、这4个都是指针,都是8字节的;
ServiceTableBase:fffff803`4e839c10
ServiceCounterTableBase:00000000`00000000
NumberOfServices:00000000`000001d0
ParamTableBase:fffff803`4e83a354
2、ServiceTableBase存储的是4字节的偏移:
(2.2) 第0x2c=44号函数NtTerminateProcess偏移:
kd> dd fffff803`4e839c10+0x29*4
fffff803`4e839cb4  fd9c8d00 01a27c00 01a99001 02150f00;注意低位在后面
0x29函数偏移:fffff803`4e839c10 + 02150f00>>4 =fffff803`4e839c10 + 2150F0‬ = FFFF F803 4EA4 ED00,和下面NtTerminateProcess的起始地址是吻合的:

kd> u nt!NtTerminateProcess
nt!NtTerminateProcess:
fffff803`4ea4ed00 4c8bdc          mov     r11,rsp
fffff803`4ea4ed03 49895b10        mov     qword ptr [r11+10h],rbx
fffff803`4ea4ed07 49897320        mov     qword ptr [r11+20h],rsi

*/
ULONGLONG GetSSDTFuncCurAddr(ULONG id)
{
    LONG dwtmp = 0;
    PULONG ServiceTableBase = NULL;
    ServiceTableBase = (PULONG)KeServiceDescriptorTable->ServiceTableBase;
    dwtmp = ServiceTableBase[id];
    dwtmp = dwtmp >> 4;
    return (LONGLONG)dwtmp + (ULONGLONG)ServiceTableBase;
}

/*
(2.3)反过来求偏移
(2.3.1)kd> u nt!NtTerminateProcess
nt!NtTerminateProcess:
fffff803`4ea4ed00 4c8bdc          mov     r11,rsp

(2.3.2)差距:
nt!NtTerminateProcess:fffff803`4ea4ed00 - ServiceTableBase:fffff803`4e839c10 = 21 50F0
(2.3.3)偏移:
21 50F0 << 4 = 215 0F00
*/
ULONG GetOffsetAddress(ULONGLONG FuncAddr)
{
    ULONG dwtmp = 0;
    PULONG ServiceTableBase = NULL;
    ServiceTableBase = (PULONG)KeServiceDescriptorTable->ServiceTableBase;
    dwtmp = (ULONG)(FuncAddr - (ULONGLONG)ServiceTableBase);
    return dwtmp << 4;
}

/*
SSDT在ntoskrnl中;内核函数和用户自己的驱动不在一个4GB空间,32位的偏移是直接跳不过去的;
修改这个偏移地址的值,使之跳转到  KeBugCheckEx ,然后在 x KeBugCheckEx  
的头部写一个 2 12  字节的  mov - - jmp ,这是一个可以跨越  4GB  ! 的跳转,跳到我们的函数里!
*/
VOID FuckKeBugCheckEx()
{
    KIRQL irql;
    ULONGLONG myfun;
    UCHAR jmp_code[] = "\x48\xB8\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\xFF\xE0";
    /*通过jmp跳转,而不是call,可以让Fake_NtTerminateProcess直接利用原NtTerminateProcess
    留下的参数*/
    myfun = (ULONGLONG)Fake_NtTerminateProcess;
    memcpy(jmp_code + 2, &myfun, 8);
    irql = WPOFFx64();
    memset(KeBugCheckEx, 0x90, 15);
    memcpy(KeBugCheckEx, jmp_code, 12);
    WPONx64(irql);
}

/*
填写KeBugCheckEx的地址
在KeBugCheckEx填写jmp,跳到Fake_NtTerminateProcess
不能直接填写Fake_NtTerminateProcess的地址,因为它们不再同一个4GB
*/
VOID HookSSDT(PSYSTEM_SERVICE_TABLE received)
{
    KIRQL irql;
    ULONGLONG dwtmp = 0;
    PULONG ServiceTableBase = NULL;
    KeServiceDescriptorTable = received;
    //get old address
    //Dbg_Break();
    NtTerminateProcess = (NTTERMINATEPROCESS)GetSSDTFuncCurAddr(44);
    DbgPrint("\r\n------------------------Old_NtTerminateProcess: %llx-----------------------\r\n", (ULONGLONG)NtTerminateProcess);
    //set kebugcheckex
    //Dbg_Break();
    FuckKeBugCheckEx();
    //show new address
    ServiceTableBase = (PULONG)KeServiceDescriptorTable->ServiceTableBase;
    //OldTpVal = ServiceTableBase[41];    //win7编号是0x29 = 41
    OldTpVal = ServiceTableBase[44];    //win10逆向ntdll的时候发现编号是0x2c = 44
    irql = WPOFFx64();
    /*
    我们挂钩的函数是KeBugCheckEx,所以把该函数的偏移算出来(只有32位,在4GB内)
    把SSDT原本terminalProcess的地方替换掉(都在SSDT,在同一个4GB范围内)
    这样一旦调用terminalProcess,实际会调用KeBugCheckEx,然后再到我们自己的代码;
    */
    ServiceTableBase[44] = GetOffsetAddress((ULONGLONG)KeBugCheckEx);
    WPONx64(irql);
    DbgPrint("\r\n------------------------KeBugCheckEx: %llx-----------------------\r\n", (ULONGLONG)KeBugCheckEx);
    DbgPrint("\r\n------------------------New_NtTerminateProcess: %llx-----------------------\r\n", GetSSDTFuncCurAddr(44));
}

VOID UnhookSSDT()
{
    KIRQL irql;
    PULONG ServiceTableBase = NULL;
    ServiceTableBase = (PULONG)KeServiceDescriptorTable->ServiceTableBase;
    //set value
    irql = WPOFFx64();
    ServiceTableBase[44] = GetOffsetAddress((ULONGLONG)NtTerminateProcess);    //OldTpVal;//直接填写这个旧值也行
    WPONx64(irql);
    //没必要恢复KeBugCheckEx的内容了,反正执行到KeBugCheckEx时已经完蛋了。
    DbgPrint("\r\n------------------------NtTerminateProcess: %llx-----------------------\r\n", GetSSDTFuncCurAddr(44));
}


ULONGLONG SearchforKeServiceDescriptorTable64(ULONGLONG StartSearchAddress, ULONGLONG EndSearchAddress)
{
    UCHAR b1 = 0, b2 = 0, b3 = 0;
    ULONG templong = 0;
    ULONGLONG KeServiceDescriptorTable = 0;

    //地址效验
    if (MmIsAddressValid(StartSearchAddress) == FALSE)return NULL;
    if (MmIsAddressValid(EndSearchAddress) == FALSE)return NULL;

    for (PUCHAR i = StartSearchAddress; i < EndSearchAddress; i++)
    {
        if (MmIsAddressValid(i) && MmIsAddressValid(i + 1) && MmIsAddressValid(i + 2))
        {
            b1 = *i;
            b2 = *(i + 1);
            b3 = *(i + 2);
            //if (b1 == 0x4c && b2 == 0x8d && b3 == 0x15);//4c8d15
            //if (b1 == "4c" && b2 == "8d" && b3 == "15" );//4c8d15
            //if (*i == 0x4c && *(i + 1) == 0x8d && *(i + 2) == 0x15);// 不能有;号,否则下面的代码一定会执行
            if (b1 == 0x4c && b2 == 0x8d && b3 == 0x15)
            {
                DbgPrint("\r\n--------- StartSearchAddress: %llx----------------- -------------\r\n", StartSearchAddress);
                DbgPrint("\r\n--------- matched targetAddress: %llx----------------- -------------\r\n", i);
                DbgPrint("\r\n--------- targetAddress offset: %d----------------- -------------\r\n", (i - StartSearchAddress));
                //Dbg_Break();
                memcpy(&templong, i + 3, 4);
                KeServiceDescriptorTable = (ULONGLONG)templong + (ULONGLONG)i + 7;//i是当前地址,templong是相对SSDT的偏移
                DbgPrint("\r\n--------- KeServiceDescriptorTable: %llx----------------- -------------\r\n", KeServiceDescriptorTable);
                return KeServiceDescriptorTable;
                //当前地址 + 长度 + 数值
                //fffff800`03c8c772+7 + 002320c7 = FFFFF80003EBE840
                /*
                fffff800`03c8c772 4c8d15c7202300  lea     r10,[nt!KeServiceDescriptorTable (fffff800`03ebe840)]
                fffff800`03c8c779 4c8d1d00212300  lea     r11,[nt!KeServiceDescriptorTableShadow (fffff800`03ebe880)]
                */
            }
        }
    }
    return NULL;
}

//获取SSDT KeServiceDescriptorTable
ULONGLONG GetKeServiceDescriptorTable64()
{
    PUCHAR pKiSystemCall64 = (PUCHAR)__readmsr(0xc0000082);//rdmsr c0000082   //定位KiSystemCall64
    PUCHAR EndSearchAddress = pKiSystemCall64 + 0x500;//在1280个字节的范围内搜索
    ULONGLONG KeServiceDescriptorTable = 0;

    KeServiceDescriptorTable = SearchforKeServiceDescriptorTable64(pKiSystemCall64, EndSearchAddress);
    if (KeServiceDescriptorTable)
    {
        return KeServiceDescriptorTable;
    }
        
    //msr[0xc0000082]变成了KiSystemCall64Shadow函数
    //原来我们64位搜索KeServiceDescriptorTable是通过msr的0xc0000082获得KiSystemCall64字段, 
    //但是现在msr[0xc0000082]变成了KiSystemCall64Shadow函数, 而且这个函数无法直接搜索到KeServiceDescriptorTable。
    ULONGLONG KiSystemServiceUser = 0;
    ULONGLONG templong = 0xffffffffffffffff;
    for (PUCHAR i = pKiSystemCall64; i < EndSearchAddress + 0xff; i++)//在pKiSystemCall64的0x5ff=1535字节范围内查找
    {
        if (*(PUCHAR)i == 0xe9 && *(PUCHAR)(i + 5) == 0xc3)//找到KiSystemServiceUser
        //if (*(PUCHAR)i == "e9" && *(PUCHAR)(i + 5) == "c3")//找到KiSystemServiceUser
        {
            //fffff803`23733383 e9631ae9ff      jmp     nt!KiSystemServiceUser(fffff803`235c4deb)
            //fffff803`23733388 c3              ret
            RtlCopyMemory(&templong, (PUCHAR)(i + 1), 4);
            KiSystemServiceUser = templong + 5 + i;//KiSystemServiceUser
            EndSearchAddress = KiSystemServiceUser + 0x500;
            KeServiceDescriptorTable = SearchforKeServiceDescriptorTable64(KiSystemServiceUser, EndSearchAddress);
            return KeServiceDescriptorTable;
        }
    }
    return 0;
}
复制代码

  4、效果:想要关闭计算器,直接弹框拒绝访问;

       

       windbg也看到了打印的日志,说明自己写的Fake_NtTerminateProcess函数已经被调用;

       

   其他的窗口能够随意结束;

  这次没刻意做驱动隐藏,还是被PCHUNTER发现了:(todo,自己实测下)

       

  SSDT hook是好多年以前的老办法了;因为驱动在0环,和windows 内核平起平坐,权力相当大。为了保护自己的客户端,各个厂家都在争先恐后地hook,把内核搞得一团糟,严重影响了用户体验;微软终于坐不住了,近些年在64位的windows做了以下改动:

  • 增加PG保护,一旦发现自己的内核代码被改,大概率会直接蓝屏
  • 增加驱动签名。运行的驱动必须强制签名。一旦发现某些驱动改内核,直接吊销签名的资格

       那么问题又来了,既然不让hook SSDT,各大厂家怎么知道自己的客户端有没有被逆向人员搞了?微软又提供了新的解决方案:注册回调函数;一旦第三方调用openprocess、readprocessmemory、writeprocessmemory等函数搞事,自己的客户端就能收到通知,然后采取响应的措施;回调函数的具体用法见下方【1】;

  最后,系统调用的好处/意义:

  •   3环应用只需要知道调用号就可以调用系统提供的服务,不需要知道这些服务是怎么实现的,有效地保护了系统服务代码不被看见;
  •        3环的权限下也不能修改系统调用! 

   syscall和中断实现的系统调用对比:

  

 

 

参考:

1、https://www.write-bug.com/article/2170.html   基于ObRegisterCallbacks实现的线程和进程监控及其保护

2、https://www.cnblogs.com/freesec/p/7623675.html  windows 64位 系统非HOOK方式监控进程创建

3、http://www.m5home.com/bbs/thread-8378-1-1.html  x64 SSDT正确的偏移计算,一键恢复SSDT表中的所有HOOK

标签:Vista,ULONGLONG,函数,32,hook,NtTerminateProcess,KeServiceDescriptorTable,fffff803,
From: https://www.cnblogs.com/bonelee/p/17020550.html

相关文章

  • [ABC232G] Modulo Shortest Path
    ProblemStatementWehaveadirectedgraphwith$N$vertices,calledVertex$1$,Vertex$2$,$\ldots$,Vertex$N$.Foreachpairofintegerssuchthat$1\leqi......
  • 第 324 场周赛
    1.统计相似字符串对的数目统计相似字符串对的数目SolutionclassSolution{public:intsimilarPairs(vector<string>&words){unordered_map<int,int......
  • 合宙ESP32C3 + VSCode + OpenOCD调试经历
    合宙ESP32C3+VSCode+OpenOCD调试经历环境Windows10VSCode+ESP-IDF合宙ESP32C3(无串口芯片版本)理论想要直接使用内置JTAG,USB要求连接GPIO18和GPIO19合宙ESP32......
  • GD32学习-GPIO学习
    在配置GPIO的过程中,经常遇到需要配置上拉下拉等,不同的模式可能不同;关于GD32的GPIO口的描述如下:每个GPIO引脚可以由软件配置为输出(推挽或开漏)、输入、外设备用功能或者......
  • 第 326 场周赛
    1.统计能整除数字的位数统计能整除数字的位数SolutionclassSolution{public:intcountDigits(intnum){intans=0;intn=num;......
  • 移植linux2.6.32.2到mini2440
    移植一个干净的源码,便于学习linux驱动准备工作:1.主机--ubuntu10.042.编译工具--友善arm-linux-gcc-4.4.33.硬件--mini2440(预装友善的supervivi+kernel+root_fs......
  • ros2订阅esp32发布的电池电压数据-补充
    ​​ros2订阅esp32发布的电池电压数据​​电池电压数据能订阅但是不显示,数据QoS不匹配,需要修改。默认: 需要使用的是外部机器人通过wifi传递的数据,设置://createpublisher......
  • OpenOCD+DAP-LINK调试ESP32的失败经历
    目的手里有调试STM32的DAP-LINK,想试试通过JTAG调试ESP32OpenOCD支持CMSIS-DAPDAP-LINK支持的芯片,我手上这款描述如下,应该JTAG协议的都支持平台windows10+ESP-IDFE......
  • [第326场周赛]分解质因数,埃氏筛,欧拉筛
    leetcode新年福利,本次周赛没有Hard难度的题目,然后我就第一次AK了~总的来说不是很难,涉及到了三个算法,在此记录一下。分解质因数题目链接:​​6279.数组乘积中的不同质因数数......
  • 【React自学笔记08】React18Hook补充
    关于钩子函数的使用注意事项:钩子只能在React组件和自定义钩子中使用钩子不能在嵌套函数或其他语句(if、switch、white、for等)中使用React中自带的钩子函数useStateu......