|
| 1 | +# Hex QR - 200 points |
| 2 | + |
| 3 | +We were given a mysterious hexagonal qr code and a website that could produce a similar looking qr code for a given string. |
| 4 | + |
| 5 | +The goal was to figure out the encoding schema and decode the flag. |
| 6 | + |
| 7 | +***The flag to decode:*** |
| 8 | + |
| 9 | + |
| 10 | + |
| 11 | +### Solution: |
| 12 | + |
| 13 | +#### Overview |
| 14 | + |
| 15 | +In order to get the flag, we don't have to completely understand the encoding schema, we just have to understand it *well enough* to decode a message. |
| 16 | + |
| 17 | +We had access to a web page that would encode any string. After a bit of probing, I discovered that I could enter any ascii character (7 bits) but unicode characters caused the program to throw an error. So it must only be using 7-bits. |
| 18 | + |
| 19 | +#### Single character |
| 20 | + |
| 21 | +I decided to test a bunch of single character strings and look for patterns. Here are the generated codes for the following characters: |
| 22 | + |
| 23 | +#### `A - 0x41 - 1000001` |
| 24 | + |
| 25 | + |
| 26 | +#### `B - 0x42 - 1000010` |
| 27 | + |
| 28 | + |
| 29 | +#### `C - 0x43 - 1000011` |
| 30 | + |
| 31 | + |
| 32 | +Eventually, I discovered that for all single character strings, only the following 6 bits seemed to change: |
| 33 | + |
| 34 | + |
| 35 | + |
| 36 | +How could 7-bit ascii be encoded in only 6 bits? To see what bit was left out, I tried comparing two characters with equal bits except for one. What I discovered is that the most-significant bit was being left out. For example, the image produced for the following characters was identical: |
| 37 | + |
| 38 | +``` |
| 39 | +0: 0110000 |
| 40 | +p: 1110000 |
| 41 | +``` |
| 42 | + |
| 43 | +*Later I discovered that this is not exactly the case... but it works for now* |
| 44 | + |
| 45 | +#### Mapping the bits |
| 46 | + |
| 47 | +Now I wanted to figure out which bit corresponded with each triangle. Here is the syntax I will use: |
| 48 | + |
| 49 | +``` |
| 50 | +bit | 6 5 4 3 2 1 0 |
| 51 | +------------------- |
| 52 | +A | 1 0 0 0 0 0 1 |
| 53 | +B | 1 0 0 0 0 1 0 |
| 54 | +C | 1 0 0 0 0 1 1 |
| 55 | +... |
| 56 | +``` |
| 57 | + |
| 58 | +Through some more detective work, I discovered that for all ascii characters with bit 6 set, the triangles corresponded as follows: |
| 59 | + |
| 60 | + |
| 61 | + |
| 62 | +Now for characters without bit 6 set, they appeared to be shifted left and padded with a zero. For example `4` is encoded as follows: |
| 63 | + |
| 64 | +``` |
| 65 | +4 (normal) | 0 1 1 0 1 0 0 |
| 66 | +4 (encoded) | 1 1 0 1 0 0 0 |
| 67 | +which appears the same as |
| 68 | +h | 1 1 0 1 0 0 0 |
| 69 | +``` |
| 70 | + |
| 71 | +*Note: Working with only one character caused me to stumble onto a weird edge case where the code was shifted depending on whether the 6th bit of the first character was set or not - we don't have to worry about that for now.* |
| 72 | + |
| 73 | +#### Multiple characters |
| 74 | + |
| 75 | +One obvious difference between one and two character strings is that the qr code gets bigger. Instead of three base triangles, we now have 4: |
| 76 | + |
| 77 | + |
| 78 | + |
| 79 | +Another difference is that the qr code seems to flip horizontally based on the number of characters. |
| 80 | + |
| 81 | +I repeated the same procedure of mapping as before and obtained the following character positions (I only used characters with bit 6 set without realizing it -> this will be important later): |
| 82 | + |
| 83 | +#### Two characters |
| 84 | +*I've omitted bits `1-4` but they are in order* |
| 85 | + |
| 86 | + |
| 87 | +#### Three characters |
| 88 | + |
| 89 | + |
| 90 | +Now the method of reading these codes is becoming clear: They are read in a zig-zag pattern starting from the top. For example to read a three character code, use the following path: |
| 91 | + |
| 92 | + |
| 93 | + |
| 94 | +Then you obtain binary like this: |
| 95 | + |
| 96 | +``` |
| 97 | +010 [char 1] 00 [char 2] 00 [char 3] ... |
| 98 | +``` |
| 99 | + |
| 100 | +At some point during testing, I encountered a binary string like this: |
| 101 | + |
| 102 | +``` |
| 103 | +... [char n] 01 [char n+1] ... |
| 104 | + ^^ |
| 105 | + notice |
| 106 | +``` |
| 107 | + |
| 108 | +This confused me for a moment and caused me to revisit my original idea. Why would the spacing bits be different? |
| 109 | + |
| 110 | +After some more testing, I realized that for every character after the first one, the two spacing bits corresponded to bits `6` and `7`. I rechecked a few things and discovered the correct encoding. |
| 111 | + |
| 112 | +To read these qr codes, follow the zigzag pattern above and then perform a `change, keep, change, keep...` operation on the resulting binary string. |
| 113 | + |
| 114 | +In more mathematical terms, you take the binary string and `XOR` with a string of `1010101010...` |
| 115 | + |
| 116 | +#### The correct encoding |
| 117 | + |
| 118 | +*Note this is the format after you apply the `XOR` as described above* |
| 119 | + |
| 120 | +``` |
| 121 | + binary notes |
| 122 | +-------- ------- |
| 123 | +11 - header (if this is 00, you are reading it wrong |
| 124 | + and should flip horizontally) |
| 125 | +
|
| 126 | +[char 1] - if bit 6 is set, this is bits 6 -> 0 of char 1, |
| 127 | + otherwise it is bits 5 -> 0 of char 1 |
| 128 | +
|
| 129 | +[char 2] - the remaining chars are all bits 7 -> 0 |
| 130 | +[char 3] (bit 7 is always 0 because it is ascii) |
| 131 | +[char 4] |
| 132 | +... |
| 133 | +
|
| 134 | +00000000 - once you hit zeros, you've reached the end |
| 135 | +``` |
| 136 | + |
| 137 | +In graphical form, the encoding is as follows: |
| 138 | + |
| 139 | +***Bit 6 is set*** |
| 140 | + |
| 141 | +*`b1` indicates a binary 1 value* |
| 142 | + |
| 143 | + |
| 144 | + |
| 145 | +***Bit 6 is not set*** |
| 146 | + |
| 147 | + |
| 148 | + |
| 149 | +*Notice that if bit 6 is not set, the entire rest of the code is shifted one bit* |
| 150 | + |
| 151 | +#### Decoding the flag |
| 152 | + |
| 153 | +Now that we know the encoding schema, it is trivial to read the binary and decode the flag. I read the triangles by hand and then fed it into a python program to perform `XOR` and convert to ascii. |
| 154 | + |
| 155 | +Here is the program I wrote (it's a little rough): |
| 156 | + |
| 157 | +```python |
| 158 | +# Reading the qr code directly |
| 159 | +hex_raw = """ |
| 160 | +0101100000011010000 |
| 161 | +100110001011000011011 |
| 162 | +00010000100110011001011 |
| 163 | +1000110100001001110011000 |
| 164 | +000001010001000010010011100 |
| 165 | +11110000110100001110110011001 |
| 166 | +0001110010011000000100110000010 |
| 167 | +100011100000111010001001110011000 |
| 168 | +00000101000100110001100000011011000 |
| 169 | +10000000100111001100000000101000100 |
| 170 | +001001111010011010000111011000010 |
| 171 | +1000100110001001000010000000110 |
| 172 | +10000100111001100000010011001 |
| 173 | +101010000010100011011001100 |
| 174 | +0000110000100110011001101 |
| 175 | +10001100010011000000110 |
| 176 | +111001010000101010101 |
| 177 | +0101010101010101010 |
| 178 | +""".replace('\n','') |
| 179 | + |
| 180 | +# convert the hex_code to a binary value |
| 181 | +hex_raw_value = int(hex_raw,2) |
| 182 | + |
| 183 | +# create a string of the same length to xor with |
| 184 | +xor_value = int(('10'*(len(hex_raw)/2))[0:len(hex_raw)],2) |
| 185 | + |
| 186 | +# perform XOR and convert back to binary string |
| 187 | +hex_code = str(bin(hex_raw_value ^ xor_value))[2:] |
| 188 | + |
| 189 | +out_string = "" |
| 190 | +i = 2 # skip the 2 bit header |
| 191 | + |
| 192 | +# add the first character (guessing that bit 6 is set) |
| 193 | +bit6IsSet = True |
| 194 | +out_string = out_string + chr(int(hex_code[i:i + (7 if bit6IsSet else 6)], 2)) |
| 195 | +i = i + (7 if bit6IsSet else 6) |
| 196 | + |
| 197 | +# the rest of the characters are 8 bit |
| 198 | +while i < len(hex_code)-8: |
| 199 | + out_string = out_string + chr(int(hex_code[i:i+8], 2)) |
| 200 | + i = i + 8 |
| 201 | + |
| 202 | +print(out_string) |
| 203 | +``` |
| 204 | + |
| 205 | +The output was `easyctf{are_triangles_more_secure_than_squares?_c54fcdeb}` |
| 206 | + |
| 207 | +#### Bonus |
| 208 | + |
| 209 | +On twitter, @easyctf posted this image as a teaser: |
| 210 | + |
| 211 | + |
| 212 | + |
| 213 | +Which decodes as `i <3 cheesecake` |
| 214 | + |
| 215 | +##### Writeup by Harrison Green |
| 216 | + |
| 217 | +### External Writeups |
| 218 | + |
| 219 | +* \(none\) |
0 commit comments