Table of Contents
Introduction
Upon completing the Windows Kernel Exploitation by Ashfaq Ansari he provides a challenge to write a exploit for the System Mechanic driver. In this blog post we’ll dive deep into the driver recon, vulnerability discovery, and the eventual driver exploitation. I’ll also explain how I potentially discovered a new vulnerability (Vulnerability #2) which allows for arbitrary read and write of memory. Currently all of the public exploits/blogs (that I found) reference Vulnerability #1 which is why I think Vulnerability #2 has either gone undiscovered or is at least less widely known.
Driver Recon
Device Symbolic Links
In order for a user mode application to communicate with a kernel driver through the use of IOCTL’s (explained below) the user mode process must open up a handle to the device object using a symbol link that the driver has created. The following kernel API functions are used to create the symlink.
- IoCreateDevice: Creates device name
- IoCreateSymbolicLink: Creates the symbolic link to the previously created device name.
When reversing the System Mechanic Driver entry routine in Ghidra, I eventually came across the function that creates this symbolic link. This function I renamed to InitializeDevice and is shown below.
We can verify that this symbolic link exists by using a tool called WinObj (from SysInternals).
Major Functions
A Driver_Object instance is provided by the windows kernel to a driver through the DriverEntry parameters when it is loaded.
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject,_In_ PUNICODE_STRING RegistryPath);
{
<< Driver Initialization Code>>
}
The DriverObject Structure is shown below. The member that is most important for us when reviewing IOCTL’s is the MajorFunction pointer.
typedef struct _DRIVER_OBJECT {
CSHORT Type;
CSHORT Size;
PDEVICE_OBJECT DeviceObject;
ULONG Flags;
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
UNICODE_STRING DriverName;
PUNICODE_STRING HardwareDatabase;
PFAST_IO_DISPATCH FastIoDispatch;
PDRIVER_INITIALIZE DriverInit;
PDRIVER_STARTIO DriverStartIo;
PDRIVER_UNLOAD DriverUnload;
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT, *PDRIVER_OBJECT;
The MajorFunction pointer is a pointer to a table of callback function pointers. The Windows I/O manager uses this callback table to direct the proper handling of I/O Request Packets (IRP’s). The callback table size is static and each index in the callback table is associated with a MajorFunction. Driver developers looking to implement functionality for a specific MajorFunction will place the callback function address into the proper table index.
When driver developers need user mode applications to communicate with the kernel driver the IRP_MJ_DEVICE_CONTROL
(0x0e
) is used. The user mode application uses the DeviceIOControl windows API to then communicate with the driver. When DeviceIOControl is called the kernel I/O Manger will go to the specified drivers MajorFunction table and call the function located at the index 0x0e
(IRP_MJ_DEVICE_CONTROL
). The callback function must support the following routine.
NTSTATUS DriverDispatch(
[in, out] _DEVICE_OBJECT *DeviceObject,
[in, out] _IRP *Irp
)
When viewing the System Mechanic driver, We can see the callback function for IRP_MJ_DEVICE_CONTROL
being set down at the bottom of the bottom of the InitializeDevice
routine. I renamed the callback function to IrpDeviceControlHandler
.
Device IOCTL’s
Input and Output Controls (IOCTL’s) is a 32bit value passed into the DeviceIOControl API. The receiving driver can obtain this IOCTL by parsing the IRP structure, once the IOCTL code is retrieved the specified function can be invoked. An example of this is shown below.
switch(IOCTL) {
case 0xAAAAAAAA:
functionA();
break;
case 0xBBBBBBBB:
functionB();
break;
}
Although it may appear that IOCTL’s would be arbitrary numbers selected by the driver developers, they are not. IOCTL’s have a specific structure that the I/O manager also interprets.
IOCTL Definition
----------------------
CTL_CODE( DeviceType, Function, Method, Access ) (((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))
DDDD.DDDD.DDDD.DDDD.AAFF.FFFF.FFFF.FFMM
D = Device type bits
A = Access bits
F = Function bits
M = Method bits
Device type: Are bits specifying the underlying hardware for the driver, this can be found as a parameter to IoCreateDevice.
Access bits: can be 0, 1, 2, or 3.
FILE_ANY_ACCESS 0x0 -> anyone with a handle with the device can call the this IOCTL function
FILE_READ_ACCESS 0x1 -> the caller must have read access to the device to call the IOCTL function
FILE_WRITE_ACCESS 0x2 -> the caller must have read access to the device to call the IOCTL function
(FILE_READ_ACCESS | FILE_WRITE_ACCESS) 0x3 -> the caller must have read & write access to the device to call the IOCTL function
Function Bits: This value is defined by the driver developers however Microsoft reserves values under 0x800
, thus vendors use 0x800
and greater.
Method Bits: can be 0, 1, 2, or 3. These bits are arguably the most important as they determine how the I/O manager passes the usermode buffer to the driver.
- Buffered I/O 0x0
- I/O manager validates that the buffer is in usermode address space
- I/O manager copies the buffer into newly allocated memory in kernel space, this is where the driver can work on the buffer.
- METHOD_IN_DIRECT 0x1 & METHOD_OUT_DIRECT 0x2
- I/O manager validates that the caller has read or write privileges on the buffer provided
- I/O manager locks the buffered pages by calling MmProbeAndLockPages & and creates a Memory Descriptor List for the receiving driver to access the locked pages.
- The receiving driver should call MmGetSystemAddressForMdlSafe
- Neither I/O 0x3
- No validation on the callers buffer (size or address)
- Both the address & the size of the buffer are supplied by the user.
- Driver must be in the same application context to work on this buffer
- No validation on the callers buffer (size or address)
We previously saw that the IRP_MJ_DEVICE_CONTROL
callback routine jumps to a function that I renamed to IrpDeviceControlHandler
. The IrpDeviceControlHandler
will be the function that will handle the IOCTL control codes as seen below.
In the screenshot above we can see that the IRP is parsed and the IOCTL received equals 0x226003
. If this IOCTL is not found System Mechanic returns the status 0xc0000010
(STATUS_INVALID_DEVICE_REQUEST
). The IOCTL code can be parsed using the OSR Online IOCTL Decoder.
The parsed IOCTL provides two important pieces of information. First it shows that METHOD_NEITHER is used, this is useful because we know that the buffer will not be verified by the I/O Manager. Second we know that for a usermode application to send this IOCTL code, it will need to have Read privileges on the device handle. We can find who has these privileges by viewing the device in WinObj as shown below.
Vulnerability Discovery
Initial Fuzzing
Initial fuzzing begins with a tool called IOCTLBF. This basic fuzzer will provide a starting point for vulnerability research, the command used to fuzz the AMP driver.
- Parameters:
- -d
- Device Driver Symlink
- -i
- IOCTL to fuzz
- -u
- Only fuzz the specified IOCTL
- -d
Crash Analysis
The crash we’re about to observe isn’t caused by IOCTLBF, Instead I used ioctlplus (updated by voidsec, created by Jackson T). This was used to emulate the crash that IOCTLBF would have produced but allows me to control the buffer for explanation purposes. The buffer created with ioctlplus is shown below.
The first the to notice is that the crash happens while attempting to dereference the address 0x6262626262626262
that is stored is RSI
. This is great because it is the same address stored in the second QWORD of our buffer, which means the value of RSI
is controlled by the user. In the lines just after the initial crash we can see that RSI
doesn’t just get dereferenced once, but three more times. Each time RSI
increases by a QWORD and gets dereferenced. The dereferenced value is then placed into RCX
,RDX
,R8
,R9
right before the call to RBX
. Because we control RSI
, we control the input parameters to the call (shown below in the screenshot on line 48). The screenshot below is of the decompiled function (referred to as VulnFunc
) that the kernel crashed in. Line 40 (offset0x6C8D
) is the line that crashed the kernel.
Now that we know we can control the parameters to the Dynamic call on line 48, the next step is to understand how the function pointer ControlledFunctionPointer
gets passed. If we can modify ControlledFunctionPointer
we can modify the function called on line 48. To start we first need to understand the call stack to VulnFunc
.
If you recall IrpDeviceControlHandler
will be the function that handles incoming IOCTL requests. This function will call IOCTL_FUNC
and pass in 3 parameters. Is32Bit
Boolean, InputBuffer
pointer (pointer to our usermode buffer we supplied), InputBufferLength
(user mode supplied length).
The IOCTL_FUNC
is shown below, The variable InputBuffer_DWORD1
will contain the first DWORD supplied in the user buffer, which acts as an index into UnknownGlobal
array which points to a function pointer.
The UnknownGlobal
variable is a global variable that leads to a area of memory that is filled in dynamically. This memory is some sort of structure that appears to look like the following:
UnknownGlobal Struct {
DWORD unknown_DWORD_0,
DWORD unnown_DWORD_1,
VOID* FunctionPointer
}
In the IOCTL_FUNC
code we can also see that InputBuffer_DWORD1
has a check that ensures it is less than 9, thus there are 9 functions (0-8) that the UnknownGlobal
Array contains.
Lets review what we know, The user supplied buffer pointer is passed to from IrpDeviceControlHandler
to IOCTL_FUNC
. The first DWORD from the user supplied buffer is retrieved and then checked to ensure that it is less than 9. Once the check is successful it is used as in index to an array of function pointers. The function pointer in this array is retrieved and stored in a local variable (InputBuffer_DWORD1
), the address of this variable (InputBuffer_DWORD1
) is then passed to VulnFunc
as the parameter ControlledFunctionPointer
. We know that VulnFunc
retrieves the second QWORD from the user supplied buffer (0x6262626262626262
), because the dereference of the second QWORD (0x6262626262626262
) is what causes the crash as it tries to resolve the parameters for the next call. This leaves a question, how does the code in VulnFunc
retrieve the second QWORD value if it’s not passed into the function? The below picture of the stackframe just before entering VulnFunc
should help provide the answer.
Now the decompiled code makes more sense as we can see that line 39 in VulnFunc
is manipulating the (InputBuffer_DWORD1
) pointer to grab the 2nd QWORD stored on the previous stack frame. If this is difficult to understand right now, don’t worry, we’ll dive into it deeper in Vulnerability #1.
Now we know we can call a total of 9 (0-8) functions and control each parameter to them, we should start to do static and dynamic analysis on each function in search of vulnerabilities. The first vulnerability, which we’ll see in a second, was found this way. The second vulnerability was much more illusive and required the creation of a custom fuzzer to assist in discovery.
Vulnerability #1
For the first vulnerability we actually don’t need to dive into any of the 9 functions functions I discussed earlier, instead, we only care about their return value. The dynamic function we control returns a value and places it into the dereferencedInputBufferQword2
variable that was previously used to hold the pointer to our parameters (line 48). then it places it into the location **(undefined **)(ControlledFunctionPointer + 6)
. We have seen a similar pattern before when we were grabbing the 2nd QWORD from the previous stack. The question now is, where are we placing this returned value?
Lets go back to the decompiled view of IOCT_FUNC
and determine where this value is placed. I believe this picture provides a good representation on what’s going on. Remember that the ControlledFunctionPointer
variable in VulnFunc
is actually the address of InputBuffer_DWORD1
in IOCTL_FUNC
.
The return value of the dynamic function call in VulnFunc
is stored in V10
! On line 51 V10
appears to be placed in a dereferenced location that the user supplies! We can see this because InputBuffer_DwordPointer
is assigned on line 20 from the InputBuffer
array at the second index (which means it will take the 3rd QWORD we provide in the user buffer). This is great, as it means we can supply a pointer and write the return value anywhere in memory.
It’s important to note, that this vulnerability is enough to write the token privilege exploit that we will be creating by the end of this write up. However, This vulnerability only allows you to write values that get returned from the dynamic function call (line 48) in VulnFunc
, Vulnerability #2 allows for arbitrary read write.
Vulnerability #2
We’ll look for the second vulnerability inside the functions we can control through VulnFunc
. A good place to start is to send 9 different IOCTL requests (With the supplied first DWORD being 0-8) and set a breakpoint in VulnFunc
at line 48 (dereferencedInputBufferQword2 = (*pcVar4)(dereferencedInputBufferQword2,uVar1,uVar2,uVar3)
). Once the breakpoint is hit we determine the address of each function you can call. You should start static analysis of those functions and determine if anything stands out. Initially this was what I did, however, some of the functions were difficult to reverse (function 5 in particular) and I didn’t come up with anything useful. My next step was to create a custom fuzzer.
Custom Fuzzer Creation
- The custom fuzzer must be able to do the following
- Check all 9 functions we can control
- Control the parameters between random values (Represented by Param X [in the diagram below]) and usermode address buffer pointers (Represented by Pointer to userspace X [in the diagram below])
- If the parameter points to a usermode address buffer and if the buffer that this address points to is modified, it should be reported
- Supply a usermode return address
- if the buffer that this address points to is modified, it should be reported
- Should a usermode buffer that is pointed to by the return address or the parameters be modified we should be able to repeat the same IOCTL request again. This is to ensure we’ll be able to setup breakpoints and see the exact same request come through.
I’m not going to go through the code of the fuzzer, as it is hacky, although I’ll upload to my Github where you can view it. Instead I’ll provide a high level diagram of how this works.
- We’re interested in the following differences
- Return Value
- Helps us understand what values we can write using Vulnerability #1
- Param X or Pointer to userspace X
- This value should not be overwritten, however, if it is then this indicates something we do not understand is happening which may be interesting
- userspace X
- Indicates one of parameters was dereferenced and used to store information at that location
- slack space
- If this value is overwritten then we know that System Mechanic Driver has written a large buffer of memory
- Return Value
Custom Fuzzer Usage
Previously I mentioned function 5 being the most difficult to reverse, but this function also revealed the most interesting results of the fuzzer.
The important thing to note from the image above is that the values at userspace 3 & 4 were modified from their original values (9 & 15f31c32ca0) after the IOCTL call to system mechanic. This could mean that param3 & param4 (Pointer to userspace 3 & 4) was dereferenced by the system mechanic driver and used to place some memory into, this means that we may be able to supply an arbitrary address and have the kernel write into it.
Before starting to dig deeper into function 5, I decided to run more tests from the fuzzer and determine any consistencies that lies within.
There are a few takeaways from the data above:
- param2 is always equal to 2 when the userspace buffer is modified
- If param3 does not point to userspace 3 and param4 points to userspace 4, userspace 4 is still overwrote. This means that param3 & param4 are both dereferenced and have values placed into them. If userspace 4 was not overwrote, this could mean that only param3 was being dereferenced and the kernel was writing more than 8 bytes thus entering into the memory where the value userspace 4 is contained.
Reversing Function 5
Armed with the knowledge above, lets dig into decompiled code of function 5 (called opcode5
in the screenshot below).
The function starts by checking that param2 < 3
and param1 < 30 (0x1e)
, our fuzzer examples indicates both these conditions are met. The third conditional statement does a dynamic check that relies on param1 to determine if a value is 0, if true, execution will continue on line 13 (offset 0x8de6
), otherwise execution will jump to line 50 offset (0x8dc2
). We’ll rerun the fuzzer and until we hit a userspace overwrite, then we’ll setup breakpoints at these offsets (0x8de6,0x8dc2
) to determine where our execution takes us. Additionally we’ll set a breakpoint at line 26 offset (0x8e98
), to see where the dynamic call goes.
The following was the fuzzer reran until we had a userspace overwrite.
Now we’ll set the breakpoints and rerun the last request done by the fuzzer.
We hit breakpoint at offset amp+0x8e98
which means we hit line 26. Lets first verify where this Call will take us (by figuring out what is stored at qword ptr [rsp+38]
) and then verify our parameters.
The function that will be called is at offset 0x8730
, we’ll take a look at that shortly, but first lets verify our parameters.
The parameters are as we expect. RDX
holds our input param3 (Pointer to userspace3) and R8
holds our input param4 (Pointer to userspace4).
Finding the Arbitrary Read/Write
The function at offset 0x8730
is shown below. From now on we’ll refer to it as dynamic_function_5_read_write
.
From the image above we can clearly see where the writes to our user buffer happens, line 12 & 20. On line 12, Param_3
is dereferenced (this is our user supplied param4) and the value of 4 is placed inside it (placing it at userspace 4). On line 20, Param_2
is dereferenced (this is our user supplied param3) and the value of dereferenced param_1
is placed inside it (placing it at userspace 3). If we can manipulate param_1
we will have an arbitrary read/write!
Lets take a look at where param1 takes us. From the registers image above, we know that param_1
in this case is 0xfffff805308c8ab4
. from the image below we can see it points to the value 0x00010000
:
Lets look at decompiled function 5 (opcode5
) again to see what control we have over param_1
in dynamic_function_5_read_write
.
We can see that param_1
inside dynamic_function_5_read_write
is actually puVar2
inside opcode5
. It appears that we can somewhat control puVar2
by modifying param_1
in opcode_5
. lets take a look at the memory of array_of_puVar2_pointers
and determine which values can be placed into puVar2
by modifying param_1
.
The highlighted block above shows the value of param_1
from dynamic_function_5_read_write
when we caught it in the debugger (see images above). The pointer values we could return from array_of_puVar2_pointers
into puVar2
didn’t turn out to be that interesting.
At this point I was a little disheartened as I realized I would not be able to fully control the pointer returned from array_of_puVar2_pointers
into puVar2
. The pointers I was able to store into puVar2
didn’t appear to be pointing to anything important. I took a small hiatus from the project before returning and giving it another look.
I came back to the project with a new idea. I’m unable to modify the puVar2
pointer sent to dynamic_function_5_read_write
, However, perhaps I can modify the value the pointer points to. This would be like modifying the value 0x00010000
that 0xfffff805308c8ab4
was pointing to. To see if we can modify this value, lets put a write breakpoint at the 0xfffff805308c8ab4
address and rerun the fuzzer.
Awesome we received a write breakpoint! Lets take the instruction offset 0x0884b
and see what the decompiled function looks like in Ghidra. We’ll refer to this function as our dynamic_function_call_store
.
This function is perfect, as it is extremely simple. The next step is to evaluate the parameters passed into the function, we can view the stack for this.
param_1
is the address we have seen passed to dynamic_function_5_read_write
and param_2
looks like a userbuffer pointer we supplied, param3
! The return address leads back to line 28 in function 5 (opcode5
), as shown below.
We now know that param_1
is familiar to us, because it’s puVar2
and by viewing the values from the previous stack frame, we can determine what the values of the each parameter were that the fuzzer had input to get this result.
- param1:
0x16
- param2:
0x0
- param3:
0x15f31c32c98
- param4:
0x15f31c32ca0
This means that we should be able to specify an arbitrary address with param3 and place it into the value that puVar2
is pointing at by using dynamic_function_call_store
then send another IOCTL request and read or write the value we just stored into puVar2
by calling dynamic_function_call_read_write
and providing the address we want to write to in param3
. We have just found an arbitrary read and arbitrary write! Lets review the IOCTL requests so we’re on the same page.
The first IOCTL request will call function 5 (opcode5
) with the following parameters
- param1:
0x16
- param2:
0x0
- param3:
<target address to read from>
- param4:
<anything>
This will call dynamic_function_call_store
with a specified puVar2
that we can read from later and param3
the target address we want to read from. This will place the value of a DWORD stored the target address (param3
) into puVar2
.
The second IOCTL request will call function 5 (opcode5
) with the following parameters
- param1:
0x16
- param2:
0x2
- param3:
<target address to write to>
- param4:
<anything greater than 4>
This will call dynamic_function_call_read_write
with the first parameter being the puVar2
address pointing to the value we chose with the previous IOCTL request callingdynamic_function_call_store
. The second parameter being param3
in our IOCTL request, will point to the address that we want the value stored at puVar2
to be written to. The third parameter passed to dynamic_function_call_read_write
is param4
of our IOCTL request. This parameter needs to be greater than 4 to satisfy a conditional statement. Once completed, the address passed in param3:<target address to write to>
from our second IOCTL request will contain the DWORD that the second IOCTL’s request param3: <target address to read from>
pointed to.
Now that we have an arbitrary read/write we can begin to work on the exploit.
Exploitation
This exploit will overwrite the token permission in our current process. It is possible to do this exploit only with vulnerability #1 as the token address for the current process can be found with usermode calls, therefore it does not require an arbitrary read. However, this code strictly uses Vulnerability #2 and obtains the current process token by manually enumerating kernel structures, to show off what it’s capable of doing.
The Code can be found on my Github. The steps are explained below:
- Get the base address of the windows kernel driver ntoskrnl, by using
EnumDeviceDrivers
to get the base address of each driver in the kernel &GetDeviceDriverBaseNameA
to find which address refers tontoskrnl.exe
. - Next use
LoadLibraryA
to loadntoskrnl.exe
into the current process (Yes we’re usingLoadLibrary
on a non.dll
, but.dll
and.exe
files have the same PE file format so enumerating the exports will be the same routine), then useGetProcAddress
to find the address ofPsInitialSystemProcess
. The following will give you the address ofPsInitialSystemProcess
in userspace. Substract the userspace address ofntoskrnl.exe
from thePsInitialSystemProcess
address to get the offset ofPsInitialSystemProcess
. Armed with the offset ofPsInitialSystemProcess
and the base address in the kernel ofntoskrnl
we can now determine the address ofPsInitialSystemProcess
in the kernel by using the formula<Address of ntoskrnl> + <Offset of PsInitialSystemProcess> = <Kernel Address of PsInitialSystemProcess>
PsInitialSystemProcess
is a pointer to theEPROCESS
Structure, this structure holds information for each process. In this step we’ll supply the<Kernel Address of PsInitialSystemProcess>
into Vulnerability#2 (arbitrary read), the end result is we’ll leak the address of theEPROCESS
structure.- The next step all about enumerating the
EPROCESS
.EPROCESS
Structure changes on different versions of windows, but below is the important parts of the structure based on Windows 10 build1909.
struct _EPROCESS
{
struct _KPROCESS Pcb; //0x
struct _EX_PUSH_LOCK ProcessLock; //0x2e0
VOID* UniqueProcessId; //0x2e8
struct _LIST_ENTRY ActiveProcessLinks; //0x2f0
...
struct _EX_FAST_REF Token; //0x360
...
}
====================
Sub Structures are listed below
====================
struct _LIST_ENTRY
{
struct _LIST_ENTRY* Flink; //0x0
struct _LIST_ENTRY* Blink; //0x8
};
struct _EX_FAST_REF
{
union
{
VOID* Object; //0x0
ULONGLONG RefCnt:4; //0x0
ULONGLONG Value; //0x0
};
};
We’ll use our arbitrary read vulnerability to read the UniqueProcessId (0x2e8)
and determine if it matches the process PID of our exploit process. If it doesn’t we’ll read Flink (0x2f0)
pointer stored in ActiveProcessLinks(0x2f0)
which will bring us to the next EPROCESS
structures ActiveProcessLinks(0x2f0)
. We can subtract 0x2f0
to get the Next EPROCESS
Structure base. We’ll repeat this until we find a UniqueProcessId (0x2e8)
match.
- Once the
EPROCESS
Structure for our process is found we’ll leak theToken (0x360)
structure. The Token Structure is_EX_FAST_REF
which is essentially a pointer with the last 4 bits being a reference count. Thus the value we receive from this structure will need to be masked to get the pointer to theTOKEN
Structure. The mask will look like<tokenFastRef> & 0xFFFFFFFFFFFFFFF0 = <Address of Our Process Token>
. - The
<Address of Our Process Token>
points to theTOKEN
Structure shown below.
struct _TOKEN
{
struct _TOKEN_SOURCE TokenSource; //0x0
struct _LUID TokenId; //0x10
struct _LUID AuthenticationId; //0x18
struct _LUID ParentTokenId; //0x20
union _LARGE_INTEGER ExpirationTime; //0x28
struct _ERESOURCE* TokenLock; //0x30
struct _LUID ModifiedId; //0x38
struct _SEP_TOKEN_PRIVILEGES Privileges; //0x40
...
}
====================
Sub Structures are listed below
====================
struct _SEP_TOKEN_PRIVILEGES
{
ULONGLONG Present; //0x0
ULONGLONG Enabled; //0x8
ULONGLONG EnabledByDefault; //0x10
};
Using write portion vulnerability #2 and with the knowledge of the address of our Token
Structure we can Write 0xffffffffffffffff
into _SEP_TOKEN_PRIVILEGES->Present
and _SEP_TOKEN_PRIVILEGES->Enabled
. With that write our process will now have full permissions and we have just escalated our privilege.
Final Notes
I appreciate you reading the blog, hope you learned something new. If you have any comments or would like to further discuss the post, feel free to reach out on twitter.