On Nov, 8,9 2018 I had the opportunity to help our team SomeRandomName run the Ottawa B-Sides CTF. This was our 5th year running the event with over 120 participants registered. Our biggest event yet! It’s come a long way from 30+ people huddled into a classroom bridged to the internet over a D-Link DIR-825 - affectionately known as the “D-Link of Power”. (RIP) The challenge track I wrote consisted of a series of three reversing challenges based on Web Assembly. Other than wanting to get a chance to play more with Web Assembly myself, Web assembly was something that many teams would be unfamiliar with and much of the standard reverse engineering tooling wouldn’t be as helpful. Hopefully leveling the playing field a bit between teams with and without commercial tools. All of the track could be solved with Chrome Developer Tools (seen below for the thanks example) or tools within the Emscripten SDK. I know Radare2 has support for WASM as well. Essentially the challenges were crackmes that were easy to easy-moderate in difficulty, but layered with web assembly to make it spicy. RESULTSAt the end of a hard fought 48 hours the scoreboard was as follows: Congrats to the winners! Building the WallThe challenges were constructed with the Emscripten SDK on a Ubuntu 16.04.5 LTS box. The SDK installed seamlessly using the instructions on the Web Assembly getting started: $ git clone https://github.com/juj/emsdk.git $ cd emsdk $ ./emsdk install latest $ ./emsdk activate latest Building a sample web assembly program was a straight forward process. emcc hello.c -s WASM=1 -o hello.html The compilation process produced a larger javascript file and html in addition to the hello.wasm file. At the time I didn’t really pay much attention to them. Instead I simply tried to load my hello.wasm file directly only into a sample page copied from http://webassembly.studio. Unfortunately, it produced a number of cryptic errors, relating to an invalid environment, after a bit of googling I found that the large javascript file produced by the compiler wasn’t just for show (unsurprisingly) but it prepared the execution environment (memory size, stack size, error handler, etc) that was needed to properly execute the web assembly binary. Fortunately, I was able to find a much smaller version, which provided the minimum set of items required to execute the web assembly file. (thank you Alexanderby - https://github.com/WebAssembly/binaryen/issues/670#issuecomment-344546116) Taken from the first challenge the required basic environment setup is shown below const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 }); const importObj = { env: { abortStackOverflow: () => { throw new Error('overflow'); }, table: new WebAssembly.Table({ initial: 0, maximum: 0, element: 'anyfunc' }), tableBase: 0, memory: memory, memoryBase: 1024, STACKTOP: 0, STACK_MAX: memory.buffer.byteLength, } }; The next question was the how to call a specific web assembly function from the web page. This took way longer than it should have for me to figure out, but eventually I got a minimized set as below. C Code Int is_this_the_flag() { // crack me algo here. } JS var result = instance.exports._is_this_the_flag('flag'); Compilation emcc wall.c -s ONLY_MY_CODE=1 -s WASM=1 -s EXPORTED_FUNCTIONS=”[‘ is_this_the_flag()’]” -o wall.html (And yes there’s no _ in the C file function name, but it’s needed when declared as a export function.) The Wall #1(Solved by 6 Teams) This challenge was a straight forward character by character if statement. The challenge was meant to be more of a tooling exercise and to get everyone familiar with web assembly - due to the way the file is compiled none of the actual flag characters appear in the binary. (nice!) The main check for the flag was: int is_this_the_flag(int c1, int c2, int c3, int c4, int c5, int c6, int c7, int c8, int c9, int c10, int c11, int c12, int c13, int c14, int c15, int c16, int c17, int c18, int c19, int c20, int c21, int c22, int c23, int c24, int c25, int c26, int c27, int c28, int c29, int c30, int c31, int c32, int c33, int c34, int c35, int c36, int c37, int c38, int c39, int c40) { if(c1 == 'f' && c2 == 'l' && c3 == 'a' && c4 == 'g' && c5 == '{' && c6 == 'g' && c7 == '0' && c8 == '0' && c9 == 'd' && c10 == 'W' && c11 == '0' && c12 == 'r' && c13 == 'k' && c14 == 'B' && c15 == 'u' && c16 == 't' && c17 == 'W' && c18 == 'e' && c19 =='A' && c20 == 'r' && c21 == '3' && c22 == 'J' && c23 == 'u' && c24 == 's' && c25 == 't' && c26 == 'G' && c27 == '3' && c28 == 't' && c29 == 't' && c30 == '1' && c31 == 'n' && c32 == 'g' && c33 == 'S' && c34 == 't' && c35 == '4' && c36 == 'r' && c37 == 't' && c38 == 'e' && c39 == 'd' && c40 == '}') return 1; return -99999; } As you can see it’s just a REALLY long if statement. Using an array like a normal person was a little too clean in the disassembly. (And typing out the above was much more fun..) The flag was flag{g00dW0rkButWeAr3JustG3tt1ngSt4rted} The Wall #2Solved by 3 teams) This challenge incorporated a more complicated method of verifying the flag. The CRC32b value of four characters at a time were calculated and compared against the desired value for the flag. The challenge was to recognize the algorithm, extract the desired values and calculate the flag four bytes at a time. The core of it was a CRC32b algorithm (from: https://forum.arduino.cc/index.php?topic=342371.0) unsigned int crc32b(unsigned char *message) { int i, j; unsigned int byte, crc, mask; i = 0; crc = 0xFFFFFFFF; while (message[i] != 0) { byte = message[i]; // Get next byte. crc = crc ^ byte; for (j = 7; j >= 0; j--) { // Do eight times. mask = -(crc & 1); crc = (crc >> 1) ^ (0xEDB88320 & mask); } i = i + 1; } return ~crc; } The flag was passed in by 4 bytes at a time to generate a crc32 value. int is_this_the_flag(int c1, int c2, int c3, int c4, int c5, int c6, int c7, int c8, int c9, int c10, int c11, int c12, int c13, int c14, int c15, int c16, int c17, int c18, int c19, int c20, int c21, int c22, int c23, int c24, int c25, int c26, int c27, int c28, int c29, int c30, int c31, int c32, int c33, int c34, int c35, int c36, int c37, int c38, int c39, int c40) { unsigned char msg1[5]; msg1[0] = c1 & 0xFF; msg1[1] = c2 & 0xFF; msg1[2] = c3 & 0xFF; msg1[3] = c4 & 0xFF; msg1[4] = '\0'; unsigned int frag1 = crc32b(msg1); unsigned char msg2[5]; msg2[0] = c5 & 0xFF; msg2[1] = c6 & 0xFF; msg2[2] = c7 & 0xFF; msg2[3] = c8 & 0xFF; msg2[4] = '\0'; unsigned int frag2 = crc32b(msg2); unsigned char msg3[5]; msg3[0] = c9 & 0xFF; msg3[1] = c10 & 0xFF; msg3[2] = c11 & 0xFF; msg3[3] = c12 & 0xFF; msg3[4] = '\0';; unsigned int frag3 = crc32b(msg3); unsigned char msg4[5]; msg4[0] = c13 & 0xFF; msg4[1] = c14 & 0xFF; msg4[2] = c15 & 0xFF; msg4[3] = c16 & 0xFF; msg4[4] = '\0'; unsigned int frag4 = crc32b(msg4); unsigned char msg5[5]; msg5[0] = c17 & 0xFF; msg5[1] = c18 & 0xFF; msg5[2] = c19 & 0xFF; msg5[3] = c20 & 0xFF; msg5[4] = '\0'; unsigned int frag5 = crc32b(msg5); unsigned char msg6[5]; msg6[0] = c21 & 0xFF; msg6[1] = c22 & 0xFF; msg6[2] = c23 & 0xFF; msg6[3] = c24 & 0xFF; msg6[4] = '\0'; unsigned int frag6 = crc32b(msg6); unsigned char msg7[5]; msg7[0] = c25 & 0xFF; msg7[1] = c26 & 0xFF; msg7[2] = c27 & 0xFF; msg7[3] = c28 & 0xFF; msg7[4] = '\0'; unsigned int frag7 = crc32b(msg7); if(frag1 == 0xD1F4EB9A && frag2 == 0xA8077225 && frag3 == 0x526BC817 && frag4 == 0x3FCE667C && frag5 == 0x3F2B257C && frag6 == 0x698FA851 && frag7 == 0x587EBCE6) return 1;} Solving for each of the fragments produced:. flag{WellD0n3AllM0stTh3r3} The Wall #3(Solved by 1 Team – Congrats WeDontHaveOne!)
Where as the Wall #2 was more challenging from a algorithmic point of view, Wall 3 focused on obfuscation targeted at the Chrome Developer Tools, incorporating 40 or so separate function calls to check the encrypted key value, as there was a little bit of crypto mixed. (multiple byte XOR with the key CYBER). Using the chrome developer tools this would spawn into 40 or so separate instanaces. From the C file. For Example: int check39(int c) { if( c== 0x38) return 1; return 0; } int check38(int c) { if(c == 0x2c) return 1; return 0; } int check37(int c) { if(c == 0x68) return 1; return 0; } With the following check int is_this_the_xord_flag(int c1, int c2, int c3, int c4, int c5, int c6, int c7, int c8, int c9, int c10, int c11, int c12, int c13, int c14, int c15, int c16, int c17, int c18, int c19, int c20, int c21, int c22, int c23, int c24, int c25, int c26, int c27, int c28, int c29, int c30, int c31, int c32, int c33, int c34, int c35, int c36, int c37, int c38, int c39, int c40) { if(check1(c1) && check2(c2) && check3(c3) Here was the main javascript (async () => { const fetchPromise = fetch('./wall3.wall'); const { instance } = await WebAssembly.instantiateStreaming(fetchPromise, importObj); var result = instance.exports._is_this_the_xord_flag('flag'); if (result == 1) { result = "CONGRATS - THATS THE XORD FLAG - USE THE SECRET CYBER KEY FOR THE REAL FLAG... GOOD LUCK"; } else { result = "NOTHING DEFEATS THE POWER OF THE BIG DATA AI MACHINE LEARNING BLOCKCHAIN! "; } document.querySelector('main').textContent = ` ${ result }.`; })(); The sequence that would have returned a true value was //25-35-23-22-29-00-36-0c-22-20-77-2d-18-1c-3d-16-14-23-21-61-02-2a-31-00-3f-21-35-1b-02-20-26-6d-36-71-35-22-68-2c-38 Which if XOR'd with the key CYBER produced the flag. Wall Materials Here's the challenges if you'd like to climb the wall yourself, have fun! the_walls.zip (217k) |
SRNSECThe "unvarnished" opinions of SRNSEC. Archives
February 2019
Categories |