BreizhCTF 2019: Filter
Event | Challenge | Category | Points | Solves |
---|---|---|---|---|
BreizhCTF 2018 | Filter | Reverse | 400 | 2 |
Binary: filter
TL;DR
The binary is a simple UDP server that use a BPF Filter to check the flag.
First assessment
The binary seems to wait for the flag on port 3213.
$ ./filter
Waiting for flag on port 3213
But when I try to connect on port 3213, I get an error.
$ nc 127.0.0.1 3213
(UNKNOWN) [127.0.0.1] 3213 (?) : Connection refused
I verify that the server is well running with netstat.
$ netstat -lapute
[...]
udp 0 0 localhost:3213 0.0.0.0:* thomas 33861 7934/./filter
[...]
It’s well running but it’s a udp server x). So I tried to type something with nc.
$ nc -vvv -u 127.0.0.1 3212
localhost [127.0.0.1] 3213 (?) open
flag
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
On the server side nothing happens.
$ ./filter
Waiting for flag on port 3213
Static analysis
I started from the main function. This is the code which creates a UDP socket and binds it to 127.0.0.1. Everything seems normal.
After the binary read 255 bytes from the socket into buf and directly print buf without checking the flag.
I didn’t expect this because the binary doesn’t print that I send. So there is something special in the socket configuration.
I forgot to look into assert function which is not an assert x). As you can see there is a loop which calls setsockopt 8191 times.
setsockopt manipulate options for the socket, this confirms my first hypothesis (there is something special in the socket configuration). But as you can see level and optname have respectively values 0xA5D3D9C7 and 0x85449686 at first round. The values seems invalids. At each round of the loop, level is multiplied by 0x13371337 and optname by 0xDEADBEEF.
I run binary with strace to confirm this.
$ strace ./main 2>&1 | more
[...]
socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) = 3
setsockopt(3, 0xa5d3d9c7 /* SOL_?? */, 2235864710, "K\0\0\0\0\0\0\0\340\352\225\16\374\177\0\0", 16) = -1 ENOPROTOOPT (Protocol not available)
setsockopt(3, 0xe86e8ec1 /* SOL_?? */, 1062075162, "K\0\0\0\0\0\0\0\340\352\225\16\374\177\0\0", 16) = -1 ENOPROTOOPT (Protocol not available)
setsockopt(3, 0x22cffe77 /* SOL_?? */, 918337862, "K\0\0\0\0\0\0\0\340\352\225\16\374\177\0\0", 16) = -1 ENOPROTOOPT (Protocol not available)
setsockopt(3, 0x6b238091 /* SOL_?? */, 335604826, "K\0\0\0\0\0\0\0\340\352\225\16\374\177\0\0", 16) = -1 ENOPROTOOPT (Protocol not available)
setsockopt(3, 0x9526227 /* SOL_?? */, 555316230, "K\0\0\0\0\0\0\0\340\352\225\16\374\177\0\0", 16) = -1 ENOPROTOOPT (Protocol not available)
setsockopt(3, 0x195cfb61 /* SOL_?? */, 1726858650, "K\0\0\0\0\0\0\0\340\352\225\16\374\177\0\0", 16) = -1 ENOPROTOOPT (Protocol not available)
setsockopt(3, 0x8e7934d7 /* SOL_?? */, 2780153542, "K\0\0\0\0\0\0\0\340\352\225\16\374\177\0\0", 16) = -1 ENOPROTOOPT (Protocol not available)
setsockopt(3, 0xea274f31 /* SOL_?? */, 2785642202, "K\0\0\0\0\0\0\0\340\352\225\16\374\177\0\0", 16) = -1 ENOPROTOOPT (Protocol not available)
setsockopt(3, 0xdfd9a687 /* SOL_?? */, 1112322438, "K\0\0\0\0\0\0\0\340\352\225\16\374\177\0\0", 16) = -1 ENOPROTOOPT (Protocol not available)
setsockopt(3, 0xb1fcc01 /* SOL_?? */, 4243140634, "K\0\0\0\0\0\0\0\340\352\225\16\374\177\0\0", 16) = -1 ENOPROTOOPT (Protocol not available)
[...]
There is only one call which is valid, the loop is just some kind of obfuscation to hide the real setsockopt call.
$ strace ./main 2>&1 | grep -v 'ENOPROTOOPT'
[...]
socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) = 3
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, "K\0ite\0ge0*\311\272\377\177\0\0", 16) = 0
bind(3, {sa_family=AF_INET, sin_port=htons(3213), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
fstat(1, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
[...]
setsockopt is call with SO_ATTACH_FILTER as optname. So RTFM, I look for SO_ATTACH_FILTER on internet and I found this http://man7.org/linux/man-pages/man7/socket.7.html.
That mean’s that the flag is probably checked with a BPF. The filter is written in a very basic language and it is executed in kernel-land. The filter code deals with the raw packet.
In assert function, I set the optval type which is struct sock_fprog.
I found the filter code which have to be disassembled. During the CTF I wrote a dirty disassembler of BPF code, but there is a bpf debugger here : https://github.com/cloudflare/bpftools
Each filter instructions is encoded with the following structure.
struct sock_filter { /* Filter block */
__u16 code; /* Actual filter code */
__u8 jt; /* Jump true */
__u8 jf; /* Jump false */
__u32 k; /* Generic multiuse field */
};
I wrote a python script which extracts the BPF filter from binary (at 0x2020) and converts it into a readable format for bpf_dbg.
from pwn import *
f = open("filter","rb")
f.seek(0x2020)
size = 75
value = []
for i in range(0,size):
data = f.read(8)
code = u16(data[0:2])
jt = u8(data[2:3])
jf = u8(data[3:4])
k = u32(data[4:8])
value.append("%d %d %d %d" % (code,jt,jf,k))
code = ",".join(value)
print("%d, %s" % (size,code))
f.close()
I can load the bpf in bpf_dbg with the following command.
$ ./bpf_dbg
> load bpf 75, 32 0 0 8,21 0 72 1113212995,32 0 0 12,84 0 0 4294967040,7 0 0 0,48 0 0 45,76 0 0 0,21 0 66 1413905277,40 0 0 4,100 0 0 10,7 0 0 0,40 0 0 2,172 0 0 0,7 0 0 0,40 0 0 15,172 0 0 0,21 0 57 63988,7 0 0 0,40 0 0 17,172 0 0 0,21 0 53 42679,7 0 0 0,40 0 0 19,172 0 0 0,21 0 49 62678,7 0 0 0,40 0 0 21,172 0 0 0,21 0 45 47005,7 0 0 0,40 0 0 23,172 0 0 0,21 0 41 64248,7 0 0 0,40 0 0 25,172 0 0 0,21 0 37 43431,7 0 0 0,40 0 0 27,172 0 0 0,21 0 33 64466,7 0 0 0,40 0 0 29,172 0 0 0,21 0 29 46477,7 0 0 0,40 0 0 31,172 0 0 0,21 0 25 56515,7 0 0 0,40 0 0 33,172 0 0 0,21 0 21 33722,7 0 0 0,40 0 0 35,172 0 0 0,21 0 17 60623,7 0 0 0,40 0 0 37,172 0 0 0,21 0 13 48784,7 0 0 0,40 0 0 39,172 0 0 0,21 0 9 62965,7 0 0 0,40 0 0 41,172 0 0 0,21 0 5 42939,7 0 0 0,40 0 0 43,172 0 0 0,21 0 1 49911,6 0 0 46,6 0 0 0
The bpf filter can be disassemble with the following command.
> disassemble
disassemble
l0: ld [8]
l1: jeq #0x425a4843, l2, l74
l2: ld [12]
l3: and #0xffffff00
l4: tax
l5: ldb [45]
l6: or x
l7: jeq #0x54467b7d, l8, l74
l8: ldh [4]
l9: lsh #10
l10: tax
l11: ldh [2]
l12: xor x
l13: tax
l14: ldh [15]
l15: xor x
l16: jeq #0xf9f4, l17, l74
l17: tax
l18: ldh [17]
l19: xor x
l20: jeq #0xa6b7, l21, l74
l21: tax
l22: ldh [19]
l23: xor x
l24: jeq #0xf4d6, l25, l74
l25: tax
l26: ldh [21]
l27: xor x
l28: jeq #0xb79d, l29, l74
[...]
I will explain the beginning of the code in details:
- ld [8]: loads 4 bytes at offset 8 from the packet and store it into register A (accumulator). Rember that filter code deals with raw udp packet.
So the code loads 4 bytes from data.
- jeq #0x425a4843, l2, l74: compares A with value 0x425a4843 (BZHC in ASCII). If it equals the pseudo CPU jumps to instruction 12 else it jumps to instruction 174.
- ld [12] it loads 4 bytes from data at offset 4 (12 in the raw packet). The code keeps only the first three bytes in register A because of the instruction and #0xffffff00.
- tax save register A into X.
- ldb [45] loads 1 byte from data at offset 37 (45 in the raw packet).
- or x make a or between A and X and store the result in A.
- jeq #0x54467b7d, l8, l74 compare the result with 0x54467b7d (TF{} in ascii)
We have the beginning of the flag “BZHCTF{” and the last char “}” which is at offset 37. So the data length is 38 that will be useful for the next steps.
- ldh [2] : load 2 bytes at offset 2 which corresponds to the packet size (so 38 + 8 bytes for header) in register A
- lsh #10 : A << 10, so A contains 38 << 10 = 0xb800
- tax : A is saved into X (X = 0xb800)
- ldh [4] : loads the destination port in register A so 3213
- xor x : packet size << 10 in register X is xored with destination port in register A
- tax : the result (0xb48d) is stored into X
- ldh [15] : loads 2 bytes from data at offset 7 (15 in raw packet).
- xor x : 0xb48d ^ data[7:9]
- jeq #0xf9f4, l17, l74 the result is compared with 0xf9f4.
So we need to resolve : data[7:9] ^ 0xb48d = 0xf9f4 => 0xf9f4 ^ 0xb48d = 0x4d79 = ‘My’.
- tax : save A into X register, A must be equal to 0xf9f4 to reach this code.
- ldh [17] : loads 2 bytes from data at offset 9 (17 in raw packet).
- xor x : 0xf9f4 (X) is xored with A (data[9:11])
- jeq #0xa6b7, l21, l74 : compare result with 0xa6b7
So we need to resolve : data[9:11] ^ 0xf9f4 = 0xa6b7 => 0xf9f4 ^ 0xa6b7 = ‘_C’ The following instructions perform the same operation with different constants.
I stored the disassembled BPF filter in a text file and I extracted only the constants with the following command.
$ cat disas2.txt | grep 'jeq' | cut -d ' ' -f 6 | sed 's/[#,]//g'
0xf9f4
0xa6b7
0xf4d6
0xb79d
0xfaf8
0xa9a7
0xfbd2
0xb58d
0xdcc3
0x83ba
0xeccf
0xbe90
0xf5f5
0xa7bb
0xc2f7
The following python script reads the constants and xor them to retrieve the flag.
f = open("hexa.txt","r")
data = f.readlines()
values = [ int(l,16) for l in data ] # convert strings constants into integer to do arithmetic operations
result = ""
for i in range(0,len(values)-1):
result+=hex(values[i] ^ values[i+1])[2:].decode('hex')
print(result)
I checked the flag with the following commands ;).
$ echo -n "BZHCTF{My_CRaCKMeS_RuN_iN_youR_KeRNeL}" | nc -u 127.0.0.1 3213
$ ./main
Waiting for flag on port 3213
Flag: BZHCTF{My_CRaCKMeS_RuN_iN_youR_KeRNeL}
Thank to @XeR_0x2A for this awesome challenge \o/
TomTomBinary