OpenCTF 2018 firewalker_2 writeup ######################################## Challenge Definition We were initially provided with the following challenge definition: Flag: http://server:15646/flag-742c78b2.txt Firewall rules: https://server/firewalker_2-6f7372d77e19ebab1ada24014ba6993c556d4b4c Downloading the firewall rules link gave a tar file containing the following text file, port_15646_rules.txt: Chain PORT_15646 (1 references) target prot opt source destination RETURN all -- anywhere anywhere match bpf 48 0 0 32,84 0 0 240,116 0 0 2,4 0 0 20,2 0 0 0,1 0 0 40,96 0 0 0,28 0 0 0,21 51 0 0,80 0 0 0,21 20 0 2,37 3 0 1,135 0 0 0,4 0 0 1,5 0 0 2,80 0 0 1,12 0 0 0,7 0 0 0,96 0 0 0,28 0 0 0,21 39 0 0,80 0 0 0,21 8 0 2,37 3 0 1,135 0 0 0,4 0 0 1,5 0 0 2,80 0 0 1,12 0 0 0,7 0 0 0,5 0 0 29,72 0 0 2,2 0 0 1,20 0 0 1461,53 0 25 12648430,96 0 0 1,148 0 0 2,21 0 22 1,96 0 0 1,148 0 0 3,21 0 19 2,96 0 0 1,148 0 0 5,53 16 0 4,96 0 0 1,148 0 0 11,37 0 13 2,96 0 0 1,148 0 0 17,37 0 10 9,96 0 0 1,148 0 0 23,53 7 0 5,96 0 0 1,148 0 0 42,37 0 4 30,96 0 0 1,148 0 0 61,37 0 1 50,6 0 0 262144,6 0 0 0 REJECT all -- anywhere anywhere reject-with icmp-admin-prohibited This is a fragment of an IPTables firewall ruleset file, defining a chain that is presumably applied to packets sent to port 15646. The first line says to pass the packet (subject to other rules we don't see here) if it is matched by a Berkeley Packet Filter program. The second line rejects everything else. To solve this challenge, we need to retrieve the flag file from the http server, using packets that the BPF program will like. ######################################## BPF and Tooling BPF is a bytecode runtime embedded in the linux kernel that allows flexible, high-performance filtering and selection of packets. Documentation is available online from: https://www.kernel.org/doc/Documentation/networking/filter.txt The provided rule file contains compiled BPF bytecode, which is simple but not very human-readable. To better understand the requirements of this rule, we would like to convert it into readable assembly-language code. Fortunately, a BPF debugger called bpf_dbg is included with the Linux kernel source. Search the internet for "bpf_dbg.c", install binary and headers for readline, and compile with a command such as this one: cc -o bpf_dbg bpf_dbg.c -lreadline ######################################## Disassembly of the BPF Bytecode filter.txt contains a listing of all 33 BPF instructions. bpf_dbg has a "disassemble" command which will turn the raw bytecode into readable assembly for us! First we will need to reformat the bytecode into the format required by bpf_dbg. It's almost there, but needs an instruction count added to the beginning of the bytecode listing. Count up the number of instructions and prepend it to the bytecode with a comma. Because the number of instructions is one more than the number of commas, you can use this oneliner to count them easily: echo "bytecode" | sed 's/[^,]//g' | wc -c To disassemble the code, start bpf_dbg and issue the following commands: load bpf bytecount,bytecode disassemble This will produce a beautiful diassembly output, in the syntax described in filter.txt. Using the examples and language definition from filter.txt with diagrams of the IP and TCP packet headers, read the assembly code line-by-line. Translate each instruction group into human-readable constraints on the packet. ######################################## Annotated PBF Assembly Code Here's my annotations for the firewalker_2 BPF assembly code. Note that it's hand-written, obfuscated code, but still somewhat readable because BPF jumps can only go forward and BPF cannot execute data. ######## # drop the packet if there are no tcp options (data offset = 5) #Load the data offset of the TCP packet into register A l0: ldb [32] #Mask it off to only the high-order nybble (i.e. extract the data offset) l1: and #0xf0 #Right shift it by 2: ABCD0000 -> 00ABCD00 l2: rsh #2 #Add 0x14 (10100) l3: add #20 #Store that into array element 0 l4: st M[0] #X = 0x28 (101000) l5: ldx #0x28 #Load register A from M[0] l6: ld M[0] #A = A - 101000 l7: sub x #If A = 0, drop the packet because data offset = 5 (101) l8: jeq #0, l60, l9 ########### # Handle TCP options #Load the first byte of TCP option data into A l9: ldb [x+0] #If A = 2 (maximum segment size), goto 31 l10: jeq #0x2, l31, l11 #If A > 1 (anything but a NOP ), goto 15 l11: jgt #0x1, l15, l12 # what to do if first option byte is a NOP #Copy X into A l12: txa #A = A + 1 l13: add #1 #Goto 17 ( currently A = 101001 ) l14: ja 17 # non-nop option handler #Load the second option byte into A l15: ldb [x+1] #A = A + 0x28 (101000) l16: add x #X = A l17: tax #Load register A from M[0] l18: ld M[0] #A=A-X l19: sub x #Block the packet if A=0 after subtraction l20: jeq #0, l60, l21 #At this point I skipped a few instructions because they can only drop # the packet or transfer control to line 31 l21: ldb [x+0] l22: jeq #0x2, l31, l23 l23: jgt #0x1, l27, l24 l24: txa l25: add #1 l26: ja 29 l27: ldb [x+1] l28: add x l29: tax #block the packet l30: ja 60 # MSS handler #Save the third and fourth bytes of option data (the MSS value) into A and M[1] l31: ldh [x+2] l32: st M[1] # Drop the packet if A is lower than 12648430, which it always is # unless the subtraction above gives a negative number. # So really this means drop the packet unless MSS < 1460 l33: sub #1461 l34: jge #0xc0ffee, l35, l60 #block the packet if the MSS is even l35: ld M[1] l36: mod #2 l37: jeq #0x1, l38, l60 #Block the packet unless MSS mod 3 = 2 (i.e. MSS = 3X + 2) l38: ld M[1] l39: mod #3 l40: jeq #0x2, l41, l60 #Block the packet if MSS mod 5 = 4 l41: ld M[1] l42: mod #5 l43: jge #0x4, l60, l44 #Block the packet if MSS mod 11 = 0 1 or 2 l44: ld M[1] l45: mod #11 l46: jgt #0x2, l47, l60 #Block the packet if MSS mod 17 < 10 l47: ld M[1] l48: mod #17 l49: jgt #0x9, l50, l60 #Block the packet unless MSS mod 23 < 4 l50: ld M[1] l51: mod #23 l52: jge #0x5, l60, l53 #Block the packet if MSS mod 42 < 30 l53: ld M[1] l54: mod #42 l55: jgt #0x1e, l56, l60 #Pass the packet if MSS mod 61 > 50 l56: ld M[1] l57: mod #61 l58: jgt #0x32, l59, l60 l59: ret #0x40000 #If we get to here, block the packet. l60: ret #0 ######################################## Calculating MSS Read through the annotations on the assembly code, and note the properies a packet must have to make it through this filter rule: *Must have TCP options *MSS < 1460 *MSS is odd *MSS mod 3 = 2 *MSS mod 5 != 4 *MSS mod 11 != 0 1 or 2 *MSS mod 17 > 10 *MSS mod 23 < 4 *MSS mod 52 > 29 *MSS mod 61 > 50 Using tcpdump and wget on Ubuntu 18.04, we noticed that Linux was sending packets by default that meet all the requirements except for the MSS value. We must determine an MSS value that meets all the numerical requirements and then send our packet with that MSS value. There are many ways to solve this system of math relationships for the MSS. We used a numerical sieve method in bash: First, we generated a list of all MSS values. Then, we applied each constraint, reducing the number of available choices. Eventually there was only one choice remainig. Example bash oneliners for this job look something like this: #Generate a list of all odd MSS < 1460: for ((i=1;i<1460;i=i+2)); do echo $i; done > mss.txt #Grab the ones = 2 mod 3 cat mss.txt | while read mss; do echo $mss `echo "$mss%3" | bc`; done | \ sed -n 's/ 2$//p' > mss-2.txt #Grab the ones != 4 mod 5 cat mss-2.txt | while read mss; do echo $mss `echo "$mss%3" | bc`; done | \ sed -n 's/ [0123]$//p' > mss-3.txt ######################################## Exploitation The numerical sieve revealed that MSS must = 1337 for the firewall to accept the packet. We can ensure that MSS by lowering the MTU on the network interface to the desired MSS + 40 bytes for IP and TCP headers. For example: ifconfig eth0 mtu 1377 With the proper MTU configured, we can retrieve the flag with an ordinary call to wget: wget http://server:15646/flag-742c78b2.txt Cat the text file and claim the flag!