|
| 1 | ++++ |
| 2 | +date = 2025-05-21T18:00:00-03:00 |
| 3 | +draft = false |
| 4 | +title = "DEF CON Quals 2025" |
| 5 | +description = "DEF CON Quals 2025 — writeups" |
| 6 | +tags = ['CTF', 'reversing', 'crypto'] |
| 7 | +categories = [] |
| 8 | +featured = true |
| 9 | +[params] |
| 10 | + locale = "en" |
| 11 | ++++ |
| 12 | + |
| 13 | +More than one month has past since DEF CON Quals, but we were so exhausted by it only now we managed to put together this set of write-ups. This year, several [CTF-BR](https://ctf-br.org) affiliated teams united to form `pwn de queijo` (a pun with [pão de queijo](https://en.wikipedia.org/wiki/Cheese_bun)). Together, we managed to achieve top17, the best position achieved by a Brazilian team until now. Let's keep training to get qualified to the Finals next year :D |
| 14 | + |
| 15 | +Unfortunately, the new Cloudlabs students did not manage to participate, since they had just joined the group. However, we had the pleasure to play with Vinicius, a long time Cloudlabs student, and Daniel and Miguel, two former students who now work at [Magalu Cloud](https://magalu.cloud). |
| 16 | + |
| 17 | + |
| 18 | + |
| 19 | +We are grateful to [Flipside](https://flipside.com.br) for providing such a nice space for us to gather together. |
| 20 | + |
| 21 | +Now let's get to the write-ups. |
| 22 | + |
| 23 | +## rainbow mountain |
| 24 | + |
| 25 | +We were given the [rbm](./rbm/rbm) binary. |
| 26 | + |
| 27 | +Since the binary is statically linked, and since it had strings referencing `GCC: (Debian 12.2.0-14) 12.2.0`, which is a version of gcc shipped with Debian bookworm, we started by generating FLIRT signatures for libc and libstdc++ from that distro, and loaded them into IDA Pro. |
| 28 | + |
| 29 | +Then, we spent a couple of hours reversing the main program, since it had a lot of C++ indirection going on and we thought some important behavior could be hidden there. However, it turns out the program simply called 0x4E8440 (which we named `fill_func_arr`) to load a big array of functions, then asked the user which of these functions they wanted to call, and finally asked for some base64 encoded data to pass as an argument to the chosen function. Lesson learned: start thinking simple; only if that does not work, carry out a deep analysis. |
| 30 | + |
| 31 | +We noticed the first two functions of the array (0x404450 and 0x404780) did the same thing, only dimensions of the buffers where different. They copied the provided string into a grid (9x8 or 7x5, respectively) called `arr` in boustrophedonic order (i.e. consecutive rows alternated between left-to-right and right-to-left). In the stack, just after `arr`, there was a small buffer called `target`. After copying the provided string to `arr`, the function checked if `target` contained some `wanted` string. |
| 32 | + |
| 33 | +In short, we needed to find which function had `arr` dimensions shorter than the grid size. Then, it would be possible to overflow `arr` and overwrite `target` with the desired string. |
| 34 | + |
| 35 | +Since there were a lot of functions, we needed to automate the search. We used IDA Pro menu `File -> Produce File -> Create C File` to decompile the entire program into [rbm.c](https://github.com/cloudlabs-ufscar/blog/blob/main/content/sec/defcon-quals-2025/rbm/rbm.c). |
| 36 | + |
| 37 | +We noticed one of the functions we still didn't manually analyze had the following structure: |
| 38 | + |
| 39 | +```c |
| 40 | + _BYTE v27[64]; // [rsp+60h] [rbp-60h] BYREF |
| 41 | + __int64 v28; // [rsp+A0h] [rbp-20h] |
| 42 | + __int64 v29; // [rsp+A8h] [rbp-18h] |
| 43 | + __int64 v30; // [rsp+B0h] [rbp-10h] |
| 44 | + __int64 v31; // [rsp+B8h] [rbp-8h] |
| 45 | + |
| 46 | + v31 = a1; |
| 47 | + v30 = 9; |
| 48 | + v29 = 7; |
| 49 | + v28 = 0; |
| 50 | +``` |
| 51 | + |
| 52 | +where `v27` is `arr`, `v28` is `target`, and `v30`x`v29` is the grid size. Then, we wrote a Python script to look for a function allowing to overflow `arr` at least by 8 bytes (size of `target`). |
| 53 | + |
| 54 | +```python |
| 55 | +import re |
| 56 | +import sys |
| 57 | + |
| 58 | +with open('rbm.c') as f: |
| 59 | + data = f.read() |
| 60 | + |
| 61 | +for m in re.finditer(r'_BYTE v27\[(\d+)\];(.*?) v30 = (\d+);\n v29 = (\d+);\n', data, re.DOTALL): |
| 62 | + arr_size, garbage, len1, len2 = m.groups() |
| 63 | + arr_size = int(arr_size) |
| 64 | + garbage = len(garbage) |
| 65 | + len1 = int(len1) |
| 66 | + len2 = int(len2) |
| 67 | + assert garbage == 194 |
| 68 | + if len1 * len2 >= arr_size + 8: |
| 69 | + print('line', data[:m.start()].count('\n')) |
| 70 | + print(len1, len2, arr_size) |
| 71 | + sys.exit(1337) |
| 72 | +``` |
| 73 | + |
| 74 | +Unfortunately, there was no such function. |
| 75 | + |
| 76 | +We got back at the decompiled code and realized not all of the functions from the big array followed the template we were looking for. |
| 77 | + |
| 78 | +We spotted the first function we found (0x405730) which was different from the template we previously analyzed. It simply copied the input string to `arr`. It was much simpler since it did not reorder the string contents. The following excerpt was extracted from that function: |
| 79 | + |
| 80 | +```c |
| 81 | + _BYTE v12[80]; // [rsp+28h] [rbp-68h] BYREF |
| 82 | + __int64 v13; // [rsp+78h] [rbp-18h] |
| 83 | + __int64 v14; // [rsp+80h] [rbp-10h] |
| 84 | + __int64 v15; // [rsp+88h] [rbp-8h] |
| 85 | + |
| 86 | + v15 = a1; |
| 87 | + v14 = 79; |
| 88 | + v13 = 0; |
| 89 | + v11 = 0x5A43474C43613378LL; |
| 90 | + v9 = string_len_(a1); |
| 91 | + v8 = 83; |
| 92 | +``` |
| 93 | + |
| 94 | +where `v12` is `arr`, `v13` is `target`, `v11` is `wanted`, and `v8` is the maximum amount of bytes copied from the input string to `arr`. |
| 95 | + |
| 96 | +Notice the function above allows to overflow `arr` by 3 bytes, but that is not enough to replace `target` with the `wanted` value. Once again, we need to find a function allowing to overflow at least 8 bytes. |
| 97 | + |
| 98 | +Now we complement our Python script with a regex for the newly discovered function template. |
| 99 | + |
| 100 | +```python |
| 101 | +for m in re.finditer(r'_BYTE v12\[(\d+)\];(.*?) v8 = (\d+);', data, re.DOTALL): |
| 102 | + arr_size, garbage, can_copy = m.groups() |
| 103 | + arr_size = int(arr_size) |
| 104 | + #garbage = len(garbage) |
| 105 | + can_copy = int(can_copy) |
| 106 | + if can_copy >= arr_size + 8: |
| 107 | + print('line', data[:m.start()].count('\n')) |
| 108 | + print(can_copy, arr_size) |
| 109 | + sys.exit(1337) |
| 110 | +``` |
| 111 | + |
| 112 | +The search finally returned something interesting: |
| 113 | + |
| 114 | +```text |
| 115 | +line 153797 |
| 116 | +72 64 |
| 117 | +``` |
| 118 | + |
| 119 | +Around that line in [rbm.c](https://github.com/cloudlabs-ufscar/blog/blob/main/content/sec/defcon-quals-2025/rbm/rbm.c), we had the following function: |
| 120 | + |
| 121 | +```c |
| 122 | +_BOOL8 __fastcall sub_4B82B0(__int64 a1) |
| 123 | +{ |
| 124 | + __int64 v1; // rax |
| 125 | + _QWORD *v2; // rax |
| 126 | + __int64 v3; // rax |
| 127 | + _QWORD *v4; // rax |
| 128 | + __int64 v5; // rax |
| 129 | + __int64 v7; // [rsp+8h] [rbp-88h] |
| 130 | + __int64 v8; // [rsp+10h] [rbp-80h] BYREF |
| 131 | + __int64 v9; // [rsp+18h] [rbp-78h] BYREF |
| 132 | + __int64 v10; // [rsp+20h] [rbp-70h] |
| 133 | + __int64 v11; // [rsp+28h] [rbp-68h] |
| 134 | + _BYTE v12[64]; // [rsp+30h] [rbp-60h] BYREF |
| 135 | + __int64 v13; // [rsp+70h] [rbp-20h] |
| 136 | + __int64 v14; // [rsp+78h] [rbp-18h] |
| 137 | + __int64 v15; // [rsp+80h] [rbp-10h] |
| 138 | + |
| 139 | + v15 = a1; |
| 140 | + if ( (sub_4042E0(a1) & 1) != 0 ) |
| 141 | + { |
| 142 | + v14 = 58; |
| 143 | + v13 = 0; |
| 144 | + v11 = 0x3447784339354946LL; |
| 145 | + v9 = string_len_(v15); |
| 146 | + v8 = 72; |
| 147 | + v10 = *min(&v9, &v8); |
| 148 | + v7 = sub_4F5810((__int64)v12); |
| 149 | + v1 = sub_4F52B0(v15); |
| 150 | + j_ifunc_5CC6D0(v7, v1, v10); |
| 151 | + v2 = operator_write_stream(std_cerr, (__int64)"target: "); |
| 152 | + unknown_libname_550(v2, sub_4F50B0); |
| 153 | + v3 = std::ostream::_M_insert<unsigned long>(); |
| 154 | + operator_write(v3, std::endl<char,std::char_traits<char>>); |
| 155 | + v4 = operator_write_stream(std_cerr, (__int64)"wanted: "); |
| 156 | + unknown_libname_550(v4, sub_4F50B0); |
| 157 | + v5 = std::ostream::_M_insert<unsigned long>(); |
| 158 | + operator_write(v5, std::endl<char,std::char_traits<char>>); |
| 159 | + return v13 == v11; |
| 160 | + } |
| 161 | + else |
| 162 | + { |
| 163 | + return 0; |
| 164 | + } |
| 165 | +} |
| 166 | +``` |
| 167 | +
|
| 168 | +It did not follow exactly our template, since it checked the input string with `sub_4042E0` before copying it to `arr`. We analyzed `sub_4042E0` and discovered it checked if the string was a palindrome. |
| 169 | +
|
| 170 | +So far so good, now we have everything we need to set up an overflow. |
| 171 | +
|
| 172 | +We got back to `fill_func_array` to see `sub_4B82B0` is in position `1576` of the function array. |
| 173 | +
|
| 174 | +We fired ipython to mount a palindrome input ending with the `wanted` value and to encode it to base64. |
| 175 | +
|
| 176 | +```python |
| 177 | +In [1]: from pwn import * |
| 178 | +
|
| 179 | +In [2]: wanted = p64(0x3447784339354946) |
| 180 | +
|
| 181 | +In [3]: wanted |
| 182 | +Out[3]: b'FI59CxG4' |
| 183 | +
|
| 184 | +In [4]: base64.b64encode(wanted[::-1] + (64-8)*b'A' + wanted) |
| 185 | +Out[4]: b'NEd4Qzk1SUZBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUZJNTlDeEc0' |
| 186 | +``` |
| 187 | + |
| 188 | +Finally, we tested it locally and it worked! |
| 189 | + |
| 190 | +```text |
| 191 | +$ ./rbm |
| 192 | +rainbow mountain |
| 193 | +function index: |
| 194 | +1576 |
| 195 | +picked 7fffc58e0560 |
| 196 | +base64'd input: NEd4Qzk1SUZBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUZJNTlDeEc0 |
| 197 | +decoded 72 bytes |
| 198 | +target: 3447784339354946 |
| 199 | +wanted: 3447784339354946 |
| 200 | +correct! |
| 201 | +the flag is no flag configured! contact orga |
| 202 | +``` |
| 203 | + |
| 204 | +And that's it. No point in showing the flag we received from the server, since they provided a different random flag for each team (as part of their infrastructure for *flag sharing prevention*). |
| 205 | + |
| 206 | +## dialects |
| 207 | + |
| 208 | +TBW |
| 209 | + |
| 210 | + |
0 commit comments