.., .??; ????. .??' .??' .???????????????????, .??; ?????????????.; .?????????????. ????.?????????????? .??????????????.???. ????????????., .?' ,?.; ;'.???????????. .??..????????., .????. .?????????.???, ,?????????.; '?????????? .???????' .???????, ,??????????. .?????????? .??????????................................., ;???????? .???????????????????????????????????????????????' .??????, .?????????????????????????????????????????????????????' ??????????' .?????????????????????????????????????????????????????. .?????????' ;???' ;??' .????????????, ;.????????????? '?' ,???. '.???? .?. ;???????????. ,???????????? ;?? .????., .??????.; .?.; ;???????????. '??????????? .?. .???????; .??????., ;????????????.'''''''''''.???????????' ,.???????' .?????. ;??????????????????????????????????; ??????. '????????' ;??????????????????????????????????, ????????.; ;.??????' ;?????????????........??????????????? ;???????. .?????. ;???????????? .???????????? ,?????????, '??????? ;???????????. .???????????. .??????????; .????????. .???????????? ????????????. .????????????. .??????????????????????????????????? .???????????????????????????, ,??????????????????????????????????? ??????????????????????????. .???????????????????????????????????? ,???????????????????????????; ,'.????????????????????????????????. .?????????????????????..,; '????????; '???????? ?????????. ;,' ;,; '?????????. ....????????.,;.?? .?.,,.????????..... ,??????. ?. ;?' ,??????? .??????. '??; .?. .???????; ;, .???..?????.'; ,.?????..????, ;, ?????????????????..........?????????????????. ?.' '?????????????????????????????????; ;.?. .???..????????????????????...???' ;.; ????, .?????' '???. ,. ;.; '??? '. ;'
This was a reversing challenge rated with 494 points in GrabCON CTF.
The instructions said:
Login to this binary and you will get a flag.
As the name suggests, it was compiled with rust which makes the reversing a more interesting task.
Running the binary and debugging in Docker
When I first ran the program, I got the following error:
$ ./rusty
./rusty: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.33’ not found (required by ./rusty)
./rusty: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.32’ not found (required by ./rusty)
After some research, I found out that the problem was that the binary was compiled on Ubuntu 21.04 and that there wasn’t any quick way to install GLIBC 2.32 on Ubuntu 20.04.
My first thought was Docker. So I pulled the image ubuntu:21.04 and it ran smoothly.
$ sudo docker run -v $PWD:/Data -it ubuntu:21.04 bash
root@a62fe0f60097:/# ./Data/rusty
Username : test
Password : test
Go away you fool
root@a62fe0f60097:/#
One trick I learned a year ago is that you can actually debug code running on a Docker container by avoiding the creation of a new PID namespace and using the one from the host. This can be accomplished by using --pid=host
.
This way you can see it from the debugger and attach it to it.
There are probably more ways to accomplish it but this one works for me. Let me know if you know others!
First look
When we first open it, we can see it is a large file of 7.69MB written in Rust.
We look for the main
function and we can see it calls dbg.main
so let’s open that one.
At first sight, it looks intimidating. We can try to spot where the user and password are being read and try to move forward from there.
When approaching the first conditional jump we can see that a new vector is created with the string “01DU53r” and it is compared to the username we send.
If we debug it and send AAAAAAAAAA as the username and BBBBBBBBBBB as the password we can see that the username is being compared with 01DU53r.
Now let’s move to the password. After the username check, we can see that an iterator is being created and a conditional jump is right after it. So we know that there is some for loop which in the graph is the big chunk of code from the left.
Let’s take a look at this part.
Weird loop
After looking a bit at the blocks we can notice a pattern: a variable amount of times a sum and division occurs and then a call is made to one of the following functions: rusty::x::RANDOM, rusty::y::RANDOM and rusty::z::RANDOM. After the call, a push is being made to a different vector each time.
We can also notice that, in the end, the loop is broken but if we debug we can see that it finally returns to the beginning.
Let’s debug it to check what is happening. We will send the alphabet as the password so we can quickly spot the values.
So in the x
call, we have 1 add
block and 2 div
blocks. When we check the arguments being passed to the function call we can see that 0x41 and 0x43 are loaded into the registers.
It looks like the add and div are just the calculations of an index of our password. The div
is just a way to achieve modular calculation so the index doesn’t overflow. Let’s reverse the x, y and z call to see what they are doing.
X looks very simple. Just and add nothing else.
Let’s check the arguments that y receives.
We can see that we are using 0x41, 0x44 and 0x46 in our case which are [0], [+3] and [+5] which match the add
blocks we can see above the function.
Y has a little bit more operations. The result would be (al ^ dl) | cl
which is ([0] ^ [3]) | [4])
And finally z, which doesn’t show in the graph (Cutter bug I guess) but we can get there in the assembly view.
We can see that we are using 0x41, 0x42, 0x43 and 0x44 in our case which are [0], [+1], [+2] and [+3] which match the add
blocks we can see above the function.
In this case, this is the most complex of all of them. The calculation is (al + cl) ^ (cl | dl)
which is ([0] + [1]) ^ ([2] | [3])
Now that we know what it’s happening here let’s see what the program does with the results.
Password check
When the previous loop ends, a vector with a lot of data is created. The length is 36.
After that, we can see that some vector manipulation calls happen and finally a filter call to compare vectors.
This behavior is repeated 3 times so let’s check the vector comparisons.
When we take a look into the vector manipulation calls we can see that one of the components is one of the vectors that were previously built in the loop. The other one is a slice of the weird vector generated before of length 0xc.
To pass the following filter call jump the two vectors must be the same. So now we know the length of the password is 12.
If all filter checks are good, the password is printed out. If we try to go to the end bypassing the checks manually, the password printed is garbage.
So let’s find out which password generates the 3 vectors that compose the weird huge vector.
Let’s crack this up.
When I solved this challenge I had 30 minutes left on the clock so I used the fastest and dirtiest solution.
The approach is the following. The results from the x
, y
and z
functions must match the weird vector.
We can pick the z
function and generate all possible values which match the results from the weird vector. From there, we can generate all the possible values in the y
function with the fixed ones from the z
function and check which ones are valid. Finally, do the same with the x
function.
This give us vectors with the [0], [1], [2], [3] and [5] positions. So we only need to do the calculations three times to get all pieces of the password and use the [5] value to check which is the valid next vector from all the possible ones.
So the python code would be something like:
weird = [0x66,0x81,0x78,0x88,0xb5,0x88,0xa9,0x66,0x8f,0x6a,0x89,0x85,0x57,0x7f,0x73,0x77,0x73,0x7a,0x77,0x7b,0x4e,0x37,0x37,0x43,0x48,0xf0,0x3f,0x01,0xeb,0xf0,0x92,0x11,0x15,0xbc,0xf0,0x17]
part_x = [0x66,0x81,0x78,0x88,0xb5,0x88,0xa9,0x66,0x8f,0x6a,0x89,0x85]
part_y = [0x57,0x7f,0x73,0x77,0x73,0x7a,0x77,0x7b,0x4e,0x37,0x37,0x43]
part_z = [0x48,0xf0,0x3f,0x01,0xeb,0xf0,0x92,0x11,0x15,0xbc,0xf0,0x17]
def calc1(x,t,s, check):
result1 = (((x ^ t) & 0xff) | s) & 0xff
if result1 == check:
return True
else:
return False
def calc2(x,y,z,t, check):
result = (((x+y) & 0xff) ^ ((z | t) & 0xff)) & 0xff
if result == check:
return True
else:
return False
final = []
possibles = [[],[],[]]
for i in range(int(len(part_x)/4)):
total = []
print(f'Generating possibles {i}')
for x in range(32, 127):
for y in range(32, 127):
for z in range(32, 127):
for t in range(32, 127):
result = calc2(x,y, z, t, part_z[i*4])
if result:
total.append([x,y, z, t])
finals = []
for item in total:
for s in range(32,127):
if calc1(item[0], item[3], s, part_y[i*4]):
finals.append([item[0], item[1], item[2], item[3], s])
for item in finals:
if ((item[0] + item[2]) & 0xff) == part_x[i*4]:
if ((item[1] + item[3]) & 0xff) == part_x[i*4 + 1]:
if ((item[3] + item[4]) & 0xff) == part_x[i*4 + 3]:
possibles[i].append(item)
print('[1] Possibles')
print(possibles[0])
print('[2] Possibles')
print(possibles[1])
print('[3] Possibles')
print(possibles[2])
The result is:
The fact that the 2nd one only returns one makes the job easier. The first valid vector is the one whose last value is the same as the second value from the 2nd vector. That is the 3rd vector.
Finally, the last one works the same. The valid one is the first from the 3rd vector list. And as we can see the fifth element (78) matches the second element from the 1st vector.
So the result is: [49, 78, 53, 51, 67, 85, 114, 51, 55, 51, 88, 55]
If we get the string version: 1N53CUr373X7
Let’s run the program again with this password:
We got it!