Skip to content

Commit a13ee2a

Browse files
committed
defcon-quals-2015: add rbm
1 parent 32f4a84 commit a13ee2a

File tree

6 files changed

+239907
-0
lines changed

6 files changed

+239907
-0
lines changed
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
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+
![Cloudlabs alumni](./photos/Hackaflag-46.jpg)
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+
![The (Relentless) Crypto Mage](./photos/IMG_5008.jpg)
5.59 MB
Loading
857 KB
Loading
2.77 MB
Binary file not shown.

0 commit comments

Comments
 (0)