Ocean SSTool (anticheat.site) writeup.

Ocean SSTool (anticheat.site) writeup.
Sorry Notch and Jeb.

Background

Ocean is another Minecraft "Screensharing tool", which executes intrusive scans on your computer to try and figure out if you're cheating. Ocean's client is written in Python, and their API and program security is about as secure as a Cheeto door deadbolt.

Ocean's front page marketing slogan as of writing.

Ocean markets themselves as "A first choice solution to all Screensharing complications". Apart from this making no sense as a slogan, their client and API are very terribly written, and I will demonstrate why.

Ocean's client supports both Windows and Linux, as it is written in Python and then (badly) obfuscated and then packed into a PyInstaller executable, which asks for Administrator permissions on Windows. Yikes.
Ocean's Windows client triage report (7/10): https://tria.ge/230630-q1rv1sda98

Decompilation and Initial Analysis

The python source code is first obfuscated using this (really crappy) obfuscator: https://github.com/billythegoat356/Specter, before being fed into PyInstaller to be converted into a single binary. Anyone with an intermediate knowledge of python can simply work backwards from the obfuscator source code, and write your own deobfuscator.

Firstly, unpacking the binary from it's PyInstaller shell is trivial, one can simply use https://github.com/extremecoders-re/pyinstxtractor a wonderful tool known as PyInstxtractor.

This returns all the guts of the program, but what we're curious in is Ocean.pyc, which is the main bytecode for their program.

The first stage is mostly just a million characters of pure gibberish, as it's a massive bytearray of the executable's second stage.

Absolute gibberish.

This isn't very difficult though, and one can quickly move onto the third stage, which is more readable.

The obfuscator credits itself.

The only sneaky part about this stage, is that it uses the colon character to separate the lines and hides them off screen, trying to confuse a very sleepy reverse engineer.

Notice the spacing.

This is the last real step before getting the actual source code, which looks like this.

def temp ():#line:1029
	O00OO0000O0OO0000 =os .environ ['TEMP']#line:1030
	for O0O0O000OOO00OOOO ,O0000O0O0OOO0O00O ,O0O00OOOO0O0OO0O0 in os .walk (O00OO0000O0OO0000 ):#line:1032
		try :#line:1033
			for OOOO000000OO0OOO0 in O0O00OOOO0O0OO0O0 :#line:1034
				if 'jnativehook'in OOOO000000OO0OOO0 .lower ():#line:1035
					detect .add (f"JNativeHook Library:::Out of instance",json_data )#line:1036
					debug .add (f"Detected JNativeHook in {OOOO000000OO0OOO0}",json_data )#line:1037
		except :#line:1038
			continue #line:1039

This is very much readable code, but needs a bit of refactoring...

I have painstakingly gone through and refactored all the variables we care about, and now I will go through and explain the various issues in the code.

The first step is a quick API call to validate the support pin given by the Admin.

The displayed window once you execute the program.

This is done with this function:

def pinverify():  # line:1130
    global r  # line:1131
    global pin  # line:1132
    randomHash = ''.join(random.choices(string.ascii_letters + string.digits, k=545))  # line:1134
    headers = {"programHash": randomHash}  # line:1138
    pin = str(dpg.get_value(pin_box))  # line:1140
    r = requests.post(f'https://anticheat.site/api/pins/check/{pin}/', headers=headers)  # line:1142
    if r.status_code != 200:  # line:1144
        return  # line:1145
    if True == r.json()['success']:  # line:1147
        scan(r, pin)  # line:1148

The program generates a completely random string of length 545, and sends that via a POST request to the API (https://anticheat.site/api/pins/check/{pin}) with the "hash" included in a header, and the pin in the path.

An example response

The server then responds with a hash of it's own that's remains the same, no matter what you send as a random "hash", due to the fact that it simply sha256 hashes the pin code and uses that as the hash. How secure.

crackstation.net showing us the original value.

If that succeeds, the program then begins it's series of scans, which are shown in order here:

How creepy!

Don't worry, I will be going through them soon. They are terribly written.

After executing these scans, it POST requests the findings to their API, in a very insecure and abusable manner.

    durtext = scandur(timeElapsed)  # line:1194
    debug.add(f"Scan time: {durtext}", json_data)  # line:1195
    scanResultData = {'authorization': pinVerifyRequest.json()['hash'], 'vpn': vpn,
                      'country': country_data[f'{ip_address}'][
                          'country'] if country_data and f'{ip_address}' in country_data and 'country' in
                                        country_data[f'{ip_address}'] else 'unknown', 'logon': boottext,
                      'javaw': javawstart, 'recyclebin': datares, 'alts': str(uuids), 'cheating': '0',
                      'prefetch': durtext, 'hosts': gameurl, 'threads': OSystem, 'journal': '1', 'pcasvc': '1',
                      'detects': json.dumps(sorted(list(set(json_data.get('detect', []))))),
                      'warning': json.dumps(sorted(list(set(json_data.get('warning', []))))),
                      'debug': json.dumps(sorted(list(set(json_data.get('debug', [])))))}  # line:1214
    isCheating = False  # line:1216
    if "detect" in json_data:  # line:1217
        isCheating = True  # line:1218
    if isCheating:  # line:1220
        scanResultData["cheating"] = "2"  # line:1221
    elif "warning" in json_data:  # line:1223
        scanResultData["cheating"] = "1"  # line:1224
    currentUTCTime = requests.get('https://worldtimeapi.org/api/timezone/Etc/UTC')  # line:1226
    UTCTimeAsJson = currentUTCTime.json()  # line:1227
    DateAndTimeAsISOString = UTCTimeAsJson['datetime']  # line:1228
    datetime = xd.strptime(DateAndTimeAsISOString, "%Y-%m-%dT%H:%M:%S.%f%z")  # line:1229
    AESKey = datetime.strftime("%H:%M:%S").encode()  # line:1230
    if len(AESKey) not in [16, 24, 32]:  # line:1232
        AESKey += b' ' * (32 - len(AESKey))  # line:1233
    cipher = AES.new(AESKey, AES.MODE_CBC, b'0123456789ABCDEF')  # line:1235
    scanDataEncoded = str(scanResultData).encode()  # line:1236
    if len(scanDataEncoded) % 16 != 0:  # line:1237
        scanDataEncoded += b' ' * (16 - len(scanDataEncoded) % 16)  # line:1238
    cipherText = cipher.encrypt(scanDataEncoded)  # line:1240
    resultHeaders = {'Content-Type': 'application/json'}  # line:1242
    data = {'ciphertext': base64.b64encode(cipherText).decode('utf-8')}  # line:1243
    try:  # line:1244
        req = requests.post(f"https://anticheat.site/api/pins/set_result/{pin}",
                            headers=resultHeaders, data=json.dumps(data))

The server will happily accept anything you send it, which is how I was able to get Notch and Jeb_ marked as cheaters with this obviously fake data:

Really?

The vectors it detects

The client fetches a list of signatures to scan for from the API, using these endpoints:

However, attempting to read the files results in more gibberish, so we must look at the code for answers, and this is where the shockingly bad programming comes into play!

Attempting to directly read the files results in gibberish.

The code responsible for de-encrypting the text back into a readable form is as follows:

# Probably *the* worst AES encryption i've seen in my life
dllName = "Kernel32.dll".encode()
if len(dllName) not in [16, 24, 32]:
    dllName += b' ' * (32 - len(dllName))
aesKey = dllName
javaDetectionsURL = "https://anticheat.site/detections/jar"
req = requests.get(javaDetectionsURL)
IV = req.content[:16]
cipher = AES.new(aesKey, AES.MODE_CBC, IV)
cipherText = req.content[16:]
decodedCipherText = cipher.decrypt(cipherText)
slicedCipherText = decodedCipherText[-1]
decodedCipherText = decodedCipherText[:-slicedCipherText]
strippedAndDecodedText = decodedCipherText.decode().strip()
jsonCipher = json.loads(strippedAndDecodedText)

You read that right. It uses b'Kernel32.dll                    ' as the AES key. Wow. The absolute pinnacle of security. Fort Knox cowers in the presence of anticheat.site!

In any case, I used this script I wrote to dump the detection signatures into their own text files, which I will also attach here, for the curious.

The program then downloads and drops strings64.exe by SysInternals and simply starts running it on all programs and compares it to the above files. Truly advanced anticheat software! RIOT should hire them for Vanguard!

Conclusion

Overall, there's not much to say about this infograbber tool. I implore you to download the refactored sample and have a look at it, you will be amazed by what you'll find.

This file isn't really malicious, it's just really terrible at it's job, and you could easily just send a fake result to the API if someone attempts to use this tool on you, clearing you of any wrongdoings.

You could also mark random users as Cheaters and possibly get them banned from servers permanently, which really means people shouldn't trust this software's judgements in the slightest.

Credits:

  • nope, for being one of the smartest reverse engineers I know and helping with initial decompilation.
  • Zach for helping me trawl through this terrible code.

Bonus!

One of the developers of this tool replied to a tweet of mine recently with the following message:

Sorry, I quite like my name.

Hmmm.... Something about people who live in glass houses shouldn't throw stones? Not sure how that goes.

Thanks for reading.