Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
From the previous challenge, we know that this file is a compiled binary for the Arduino.
We are given a .bin file but we need to convert it to an Intel Hex file so it can be flashed to the Arduino.
There are various tools to do this, such as Bin2Hex.py, which is linked.
After converting flash.bin, to flash.hex, I flashed it onto my Arduino Uno with:
"C:\Program Files (x86)\Arduino\hardware\tools\avr/bin/avrdude" -C "C:\Program Files (x86)\Arduino\hardware\tools\avr/etc/avrdude.conf" -v -patmega328p -carduino -PCOM3 -b115200 -D -Uflash:w:C:\Path\To\flash.hex:i
I got this command from uploading a sketch to the Arduino with the software.
Once flashed, I checked the Serial Monitor in the Arduino software, but it was only outputting invalid characters.
I thought that the baudrate could be the problem, so I tried turning it down.
There were less invalid characters as I went further down and at 19200, I got the flag:
When we download the file, we're presented with a file called flash.bin. Running file
on it, we get that it's just data. If we try and cat
the file, we get a whole load of junk. Great. Lets try adn see if we can enumerate any strings from it.
strings flash.bin
scroll up - yep, there's the flag.
Very similar to FIAS, so I won't explain the parts about a stack canary.
This time, there's PIE.
What changes? What changes is that the address of our ever-so-important flag function changes every time, as PIE randomises the base of the binary.
What we need to do is leak a value on the stack that we can calculate the binary base from.
We're in luck. When a function is called, the address of the instruction for it to ret to is placed on the top of the stack.
This means if the value isn't later overwritten, the addresses of some instructions, including instructions of functions of the binary, will remain on the stack.
Thus we can leak an instruction of a function in the binary, and calculate the base off of this.
I chose stack item 3, the address of the instruction in say_hi after it calls the pc_get_thunk function. Our exploit:
Leak canary and binary base in one go with format string Send junk + canary + junk + flag address Script below:
Let's run a checksec. Canary, no PIE.
On our first input, there's a format string vuln.
On the second input, there's a buffer overflow as gets is used.
There is also a flag function that seems to call system("cat flag.txt"). We just need to return to this function. There's a canary, so this isn't so easy.
We can, however, use our format string vulnerability to leak the canary value, as the canary is stored on the stack and format strings let us leak values stored on the stack. I used a simple fuzzing script to find the canary offset, which was 11. We can set a breakpoint at the instruction which checks the canary and paste in a cyclic pattern to get the offset to the canary, which is 24 by checking the loaded canary value from the stack against the pattern. Our payload will be:
junk + canary + 12 bytes of junk + address of flag function
Let's review the code.
There's two interesting functions - put_on_stack, which generates some interesting python bytecode to put a value onto the stack, and returns it.
Then, there's execute bytecode. It takes some bytecode, and, well, executes it. How though?
It loads 256 constants - 1,2,3, etc. matching up to 1,2,3,etc. As global variables, it loads the functions chr, ord, globals, locals, getattr and setattr. We have to use these functions alone to gain RCE. Impossible? No. But lets look into how we can do this.
We input a name. It takes the first 32 bytes of this, and generates some byte code that puts this name onto the stack. It then prepends said bytecode to all bytes afterwards, and executes it. If we put 0-32 chars, this is fine, but if we input anymore, then from the 33th character onwards is executed as python bytecode!
We must generate python bytecode that utilises the small set of functions and variables we have to gain RCE. My final payload in python code form was globals()[chr(101)+chr(118)+chr(97)+chr(108)](chr(95)+chr(95)+chr(98)+chr(117)+chr(105)+chr(108)+chr(116)+chr(105)+chr(110)+chr(115)+chr(95)+chr(95)+chr(46)+chr(95)+chr(95)+chr(105)+chr(109)+chr(112)+chr(111)+chr(114)+chr(116)+chr(95)+chr(95)+chr(40)+chr(39)+chr(111)+chr(115)+chr(39)+chr(41)+chr(46)+chr(112)+chr(111)+chr(112)+chr(101)+chr(110)+chr(40)+chr(39)+chr(99)+chr(97)+chr(116)+chr(32)+chr(47)+chr(104)+chr(111)+chr(109)+chr(101)+chr(47)+chr(114)+chr(97)+chr(99)+chr(116)+chr(102)+chr(47)+chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+chr(116)+chr(120)+chr(116)+chr(39)+chr(41)+chr(46)+chr(114)+chr(101)+chr(97)+chr(100)+chr(40)+chr(41))
Not very readable, but in normal python code,
How can we do this? We can't just go about compiling a .pyc or code using compile() - those files create constants and globals to suit them. We can't do this however, we must only use constants and variables given to us. We can analyse the put_on_stack function, and see what it does
b"t\x00"
- load the first global, chr. We can replace with \x02 to load the 3rd global, globals
b"d<number>"
- load constant for argument. When doing globals(), we don't need this.
b"\x83\x01"
- call current function with 1 argument
b"\x17\x00"
- binary add. does first value on stack + second value. note it's called binary but it can be used to concatenate strings just like adding numbers bcoz its python at the end of the day
we can use this to generate bytecode to call globals, b"t\x02\x83\x00"
. Then, we must dynamically construct the word "eval" using only chr via the same methods the put_on_stack function uses. After that, we use the BINARY_SUBSCR opcode, b"\x19\x00", which computes TOP1 = TOP1[TOP2]
Now what? Loaded on the stack is the function eval. Now, using the same method as put_on_stack again, we construct our python payload that cats the flag. Finally, we wrap it all up with a b"\x83\x01" - call the function eval, loaded from globals()['eval'], with one argument.
I ended up doing some enumeration, and finding flag.txt
in /home/ractf
, then catting it.
This results in the flag ractf{Puff3rf1sh??}. I will post the final bytecode below and my generation script and final exploit script as generator.py
and puffer.py
.
Bytecode:
OK so the binary got patched 4 times lol, I'll just pretend I solved it from the first one
The header reads: mCTZ, indicated the rest of the binary is compressed with 'zstandard'. We can use zstd to uncompress and read the image. It has 2 sections: scode and sin. SIN is ROM (i thought it was input, causing me to delay my solve by at least 3 hours lmao) and SCODE is the code of the program. Extracting the data from these blocks, we can use the specification at to break down the logic of the program.
Accept input
Call function at 0x3E
The functions reads input into SMAIN (general memory) one char at a time until it reaches a newline, and then returns the length of the string by counting the iterations.
If length of the input != 0x0C, print "Incorrect length!" and die
Else, loop through each char of the input with the corresponding char in SIN.
Take the 2s complement of the SIN char.
XOR ~SIN with the input char
Output the result & 0xFF
And with 0xFF causes no changes to a value, so that can be ignored. We can skip the twos complement bit by prematurely XORing every value in SIN with 0xFF. Therefore the program becomes INPUT ^ SIN = OUTPUT.
However, we dont have any information about input or output beyond their lengths, EXCEPT that OUTPUT is wrapped in flag format. Therefore we can express it as
A pwn challenge. Just a simple format string exploit.
Protections: Partial RELRO, no PIE, therefore we can overwrite the GOT as it is not read-only.
When our input is asked for, it is printed out using printf without proper format strings.
Our input is put on the stack, so the program is vulnerable to arbtirary writes via %n
.
There's no buffer overflow, and only one input, so we shouldn't be leaking anything.
We can send a simple format string overwrite payload to rewrite puts@GOT with the address of the function flaggy, and the flag will be yours. This works as puts@plt is called just after printf is called on our input, so we can overwrite the GOT entry with a different value.
Thus when the plt is called it will find a different GOT value and jump to that.
Here is the solve script:
Let's start from scratch - run the binary. It'll complain to us that we don't have some python library called memecrypt installed. That's odd.. why does it demand a python library if its an ELF file? More on this later. We can install with python3 -m pip install memecrypt
, and the binary runs smoothly, printing out what seems to be a python byte string.
Let's analyse this further. If we spam control c as the program is running, it gives us an error message about a KeyboardInterrupt, saying it's within a file owo.py. We can inspect further in classic reversing tools in ghidra, and find all sorts of python functions like __Pyx_PyObject_GetAttrStr
or __Pyx_PyObject_Call2Args
. This shows us it seems to be a python install zipped up with some python code/bytecode, or compiled cython. We decided to look into tools specialised in reversing things like this, and came across a program called REpdb which is part of the pyREtic library, made for reversing python that is put together in executable files like this as well as bytecode.
In order to use REpdb, we needed an inject point. That's where memecrypt comes in. REpdb has to be run by the process it's reversing - the easiest way to do this was to force the process to import malicious python code. We can accomplish this by editing our memecrypt.py install, or by creating a fake memecrypt.py file. I did the latter, tony did the former. Here is my fake memecrypt.py file where meme_orig is my renamed real memecrypt install.
I added those last two lines so that the program could use meme_cipher normally. With this setup, running the binary drops us into a REpdb prompt to inspect the program. What do we see? As we step through, we can see the ciphertext eDxTP2RoekN4KkVXeDpQQyU+Sy5cPidXZSR6QGRaLktkSycrITpQS1xXem5cV3pvZTpFb2NXekRcNkstZSRub2U6RW9qJGZH
being loaded into a meme_cipher object along with the key lmao. What does it decrypt to? It decrypts to "from secrets import token_bytes as hush;print(hush())"
A little research shows us that token_bytes generates some cryptographically secure random bytes. Since it prints the output of this, that seems to be what our byte string is. Tony also found this earlier through attaching tools like gdb into the process.
What's very interesting is that the program decrypts this, and seemingly executes it. This means that by tampering with our memecrypt install and changing things in the decrypt function, we can make it run whatever we want. I did lots of tampering to reap maximum information. In the meme cipher class, I added the following:
as well as this to decrypt:
and ttt = 0 to the top of the file , and print statements to always print the ciphertext and key being decrypted as well as the decrypted result. Such that on the first run of the decrypt function, it would just return our fake code. We can enumerate with this, calling globals() and similar things to look around the scope. We find lots and lots of functions. This includes flag, frag, rick_roll, grant, artemis, _, __, uwu_whats_this_nuzzles_rawr_xd
, and much more as well as meme cipher object "uwu" and the module webbrowser imported as communism.
I wrote a small script to call every function with a small delay.
Showing me the output of every function and it's respective function. We get a lot here. First of all, the flag function returns a fake flag, decrypting the ciphertext K3swTVxHOGtyWVclZmd0OGYueUZ5RzJYXForP1wwdHJbR1dhVGcrPytndFFUPzhMW0dcdA==
with the key 1337_revese_engineering.
From here, we moved on to static analysis in the decompiled C code.
There's a lot more unknown ciphertexts we can find in the strings of the binary, specifically small ones. Wait a second...
here, in the function owo, one of the small ciphertexts, T0YqVGBGJzZiLXYp
, is referenced. Right afterwards, so is the word apollo
. Why don't we attempt to decrypt T0YqVGBGJzZiLXYp
with apollo
?
We do, and it decrypts to lastfrag
. Last frag implies fragments, fragments of some form of larger ciphertext or key. We decided this had to be on track, and looked for other frags.
here! the ciphertext QVZHZUEqOnM=
is mentioned, and so is rain
! This decrypts to frag1
. Finally...
the ciphertext ZGBXYmRbfXU=
is mentioned along with the word champions, and this decrypts to frag2. No more small ciphertexts remainded, and linus confirmed it for us - these were all the frags we needed.
Key: rain
, CT: QVZHZUEqOnM=
, PT: frag1 Key: champions
, CT: ZGBXYmRbfXU=
, PT: frag2 Key: apollo
, CT: T0YqVGBGJzZiLXYp
, PT: lastfrag
From there, we tried to figure out how to put them together. Putting together the ciphertexts, the keys, the plaintexts, all sorts of combinations. We searched for larger ciphertexts that we hadn't cracked yet, and found b2o+LiRjVll0eHw8TXteVk1dVXdVYyZzJHlvOiQ+XnQ2Y3xmU3tvOm97XklTOlYxNmMkXg==
, QnxZNFtibWxPWD9udF5tcjgmd1ZbPG1XJw==
, Xjk6Y1N3PDBWdz0jNjlTOFp9bCJ5bWxPRFZu
and JUdMP2czQ3MuM1s0TkclbHlsZ20vRmZteXxMZC9sfnZDeWo1TkdIQVF5IkFTNiRxJT5PIlY7ZnAlPGo1Tm58cg==
eventually, will was able to decrypt JUdMP2czQ3MuM1s0TkclbHlsZ20vRmZteXxMZC9sfnZDeWo1TkdIQVF5IkFTNiRxJT5PIlY7ZnAlPGo1Tm58cg==
with the key frag1frag2frag3 to get the plaintext ractf{Thing5_Wh1ch_Ar3_Bey0nd_My_C0mpreh3nsi0n}, which wasn't actually the flag.
But, we were told it was incredibly close.
frag1frag2frag3 didnt make much sense, we only had frag1, frag2 and lastfrag. Woa came up with the idea to try encrypting our "fake" flag with the key frag1frag2lastfrag to see what it looked like, and what did we get? Xjk6Y1N3PDAsP35BYn1uPGJtVk9Wdz1DU1leV1M6bix5d344Wn1eV159bkhaVzx4eXdTbg==
This is remarkably close to the ciphertext we found in the binary, Xjk6Y1N3PDBWdz0jNjlTOFp9bCJ5bWxPRFZu
The fact the encryption of a close flag with the key frag1frag2lastfrag resembles a ciphertext we had already found must've meant something.
From there, we tried concatenating Xjk6Y1N3PDBWdz0jNjlTOFp9bCJ5bWxPRFZu
(the binary's ciphertext) with a bunch of other ciphertexts and throwing it into our related-key-bruteforce-script created by Tony. Eventually, Xjk6Y1N3PDBWdz0jNjlTOFp9bCJ5bWxPRFZu
+ QnxZNFtibWxPWD9udF5tcjgmd1ZbPG1XJw==
was concatenated to create the larger ciphertext Xjk6Y1N3PDBWdz0jNjlTOFp9bCJ5bWxPRFZuQnxZNFtibWxPWD9udF5tcjgmd1ZbPG1XJw==
,which decrypted with the key frag1frag2lastfrag to the real flag.
Various youtube links like , and are decrypted with the key yt and then used in webbrowser. We can't find too much else useful with this code injection, but it allows us to get a handle on what exactly is happening in this program.