Lesser known perks of a corporate job

Fixing corporate VPN, one route at a time

One of the lesser known perks of working in a corporate environment albeit remotely is that you can’t connect to devices on LAN(at-least for me). This educational post shows how to circumvent that restriction for educational purposes.

One of the first things to notice is that whenever I ping any of my devices on LAN, I don’t get any response and this is only when the VPN is on.

Hmm, so the basic hunch is that when the VPN turns on, it creates an entry in the route table that superseeds the existing one taking in all the traffic to the VPNs interface.

Let’s take a look at our route table, shall we?

1
2
3
4
5
6
7
IPv4 Route Table
===========================================================================
Active Routes:
Network Destination Netmask Gateway Interface Metric
192.168.2.0 255.255.255.0 On-link 172.16.42.129 55
192.168.2.0 255.255.255.0 On-link 192.168.2.187 281
===========================================================================

For this write up: the VPN’s network is 172.16.42.0/24 and my LAN is 192.168.2.0/24

Tada! We see our culprit.
Although, there is an entry in the table for 192.168.2.0/24 on the LAN’s interface(on 192.168.2.187) but because the metric is lower for the entry on VPN’s interface(on 172.16.42.129), it takes precedence and we get:

1
2
3
4
5
Tracing route to machine [192.168.2.1]
over a maximum of 30 hops:

1 * * * Request timed out.
2 * * * Request timed out.

What if we just delete the said entry, can we then ping any device on LAN?

1
2
3
4
5
6
7
> route delete 192.168.2.0 METRIC 55 IF 6
> ping 192.168.2.1 -t
Pinging 192.168.2.1 with 32 bytes of data:
Reply from 192.168.2.1: bytes=32 time=6ms TTL=128
Request timed out.
Request timed out.
Request timed out.

It’s a success. Hooray! Or is it?
Well the success is short lived as the entry is added back nearly instantly.

Can we do better?

Let’s write a simple loop that keeps deleting this entry again and again.

1
2
3
4
@echo off
:1
route delete 192.168.2.0 METRIC 55 IF 6
goto 1

It works but we do have intermittent connection issues for the period when the entry is added to the route table.

1
2
3
4
5
6
7
> ping 192.168.2.1 -t 
Pinging 192.168.2.1 with 32 bytes of data:
Reply from 192.168.2.1: bytes=32 time=6ms TTL=128
Reply from 192.168.2.1: bytes=32 time=5ms TTL=128
Request timed out.
Request timed out.
Reply from 192.168.2.1: bytes=32 time=6ms TTL=128

What should we do? Let’s dig in.

If we can deny the ability of the VPN to create an entry in the route table then we won’t have any issues to face with.

On Windows, to create an entry in the route table, we have to use Iphlpapi.dll!CreateIpForwardEntry, taking a look on the function header

1
2
3
IPHLPAPI_DLL_LINKAGE DWORD CreateIpForwardEntry(
[in] PMIB_IPFORWARDROW pRoute
);

with the structure defined as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct _MIB_IPFORWARDROW {
DWORD dwForwardDest;
DWORD dwForwardMask;
DWORD dwForwardPolicy;
DWORD dwForwardNextHop;
IF_INDEX dwForwardIfIndex;
union {
DWORD dwForwardType;
MIB_IPFORWARD_TYPE ForwardType;
};
union {
DWORD dwForwardProto;
MIB_IPFORWARD_PROTO ForwardProto;
};
DWORD dwForwardAge;
DWORD dwForwardNextHopAS;
DWORD dwForwardMetric1;
DWORD dwForwardMetric2;
DWORD dwForwardMetric3;
DWORD dwForwardMetric4;
DWORD dwForwardMetric5;
} MIB_IPFORWARDROW, *PMIB_IPFORWARDROW;

The variable names are self explanatory so I won’t explain them.

Let’s take a look if our target process calls this function.

What should we do about it?
Let’s do user-mode hooking!

Below a simple diagram on how does user-mode hooking works. Let’s assume that FncA calls FncB and then gives control back to FncA.

graph LR
    B[FncB] --> A[FncA]
    A[FncA] --> B[FncB]

And, instead, we want to call FncC instead of FncB. How can I achieve that?
I can in practice place a hook on FncB such that we will perform a jump to FncC and execute that instead of FncB.

graph LR
    
    A[FncA] --> B[FncB]
    B -.-> C[FncC]
    C -.-> A

The dashed line shows that FncB won’t be executed rather the control will be passed on to FncC and FncC can decide what to do. That’s how most AVs work these days to detect malicious things during runtime.

Now that we know what we can do with user-mode hooking.
Let’s write a hook for it, our function should have the same parameters and it’s types. Let’s name our function as hk_CreateIpForwardEntry.

Our function hk_CreateIpForwardEntry will have a simple mechanism, we will monitor for calls containing specific dwForwardDest and dwForwardMask, which are Network Destination and Netmask respectively(see route table above) and if the request to create a route entry contains those we will simply return NO_ERROR and the VPN service will think that the route is added to the table.

We know that our VPN will try to create an entry with dwForwardDest set to 192.168.2.0 and dwForwardMask set to 255.255.255.0. We need to convert them to DWORDs with little-endian ordering;
192.168.2.0 -> 174272
255.255.255.0 -> 16777215

We will use OutputDebugString to output logs from our hk_CreateIpForwardEntry and use DbgView to view them.

After changing some memory bytes of the running VPN Service (heh), we see the output from our own bytes:
Hooked CreateIpForwardEntry @FUNC_ADDRESS
But the entry is already present in the route table so the VPN service won’t try to create it again, so let’s delete it to trigger the process :D

And, now we got a reliable way to access devices on our LAN :D.
Source code is not going to be provided for obvious reasons.