REV, MSC (500 points, 11 solves)
Here we explain some solutions for the reversing challenge HaRlEm ShAkE from our recent CTF.
It is a Rust binary using x11rb to move around windows on the screen and display a flag in plain sight.
Find the source code for this reversing challenge on GitHub.
°(not actually related)
We’ll go directly into the main method and see some setup until we arrive at a long list of function calls to sub_942E0
with changing parameters.
Analyzing sub_942E0
, we first see a hardcoded key 91b16ca0da5d332cb9909596be5f6918f0e11956f55685f111d105c4a5ec5f58
as well as a string Stop patching pls :(
. This is triggered if we patch the given blobs of data passed to the function or the key itself so the decryption fails.
Starting the program changes the position and size of all X11 windows. I’m running on Sway, so that doesn’t affect me, but we can also skip that call in the debugger.
We then set a breakpoint in sub_942E0
and let the program decrypt the image for us. The first image a {
character.
Well, this seems tedious, why don’t we just decrypt the images ourselves?
We know all offsets as seen in the screenshot above. We also know the key, and the constant expand 32-byte k
gives us a hint we might be using using Salsa20/ChaCha20.
We see that the decrypt function is called with a 12 byte offset.
12 bytes nonce length are default for ChaCha20, so let’s just try ChaCha20-Poly13051 and it works out of the box. The first image decodes correctly.
Now we automate it for all character-images and place them to the wanted positions (which are also passed into sub_942E0
).
We also found this string in the binary: Screen too small :(, min size 1900 x 900
. These values we use to create our image.
Solve Script:
from Crypto.Cipher import ChaCha20_Poly1305
from iced_x86 import *
import re
from PIL import Image
from io import BytesIO
image = Image.new("RGB", (1900, 900), "black")
with open("harlem_shake", "rb") as f:
executable = f.read()
key = executable[0x19DCC6:0x19DCE6]
def decrypt(img_off, img_len, x, y):
nonce = executable[img_off:img_off + 12]
img = executable[img_off + 12: img_off + img_len]
cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
char_img = Image.open(BytesIO(cipher.decrypt(img)))
image.paste(char_img, (x, y))
img_off = 0
img_len = 0
img_pos_x = 0
img_pos_y = 0
code_offset = 0x95609
code_end = 0x95D43
for instr in Decoder(64, executable[code_offset:code_end], ip=0):
# yolo regex matching just works
if m := re.match(r"lea rcx,\[(?P<off>\w+)h\]", str(instr)):
img_off = int(m.group("off"), 16) + code_offset
if m := re.match(r"mov r8d,(?P<len>\w+)h", str(instr)):
img_len = int(m.group("len"), 16)
if m := re.match(r"mov esi,(?P<x>\w+)h", str(instr)):
img_pos_x = int(m.group("x"), 16)
if m := re.match(r"mov edx,(?P<y>\w+)h", str(instr)):
img_pos_y = int(m.group("y"), 16)
if m := re.match(r"call 0FFFFFFFFFFFFECD7h", str(instr)):
decrypt(img_off, img_len, img_pos_x, img_pos_y)
if instr.ip > (code_end - code_offset):
break
image.save("flag.png")
hxp{Alw4ys_ha5_b3en_oN_y0ur_ScrE3n}
Alternative solution and what is actually going on:
What the program actually does is putting all character images together into one big image and then sending this image to the XServer creating a clickable region.
Meaning: You can click through everything except the characters itself. You might have seen this when hovering over certain areas that “randomly” your mouse changes when harlem_shake
is running.
Using tools like XScope, or adding logging in XServer, you can just read out the requests and get the flag.