안녕하세요~ 오늘은 DiceLoader라는 악성코드를 분석하면서 제가 주로 악성코드 분석을 공부하는 유튜브 채널에 대해 얘기하고 싶습니다.
정보보안 강의들은 (OSCP, OSEP, SanS 강의 등) 비싸지만 악성코드 분석이 더욱 좁은 분야라서 악성코드 분석에 대한 강의들이 더 비싸겠지? 라는 생각을 했으나 유튜브에서 OALabs라는 채널을 발견했습니다.
악성코드 분석에 대해 아시는 분들은 알고 계시겠지만 OALabs는 훌륭한 unpacme하고 hashdb라는 IDA pro plugin을 만드신 분이십니다. 그것뿐만 아니라 Twitch에서 일주일에 2번 정도 악성코드 분석을 스트리밍도 합니다. 스트림의 vod을 다시 보기 위해 patreon에 올리는데 patreon는 구독하여 한 달에 한 3만원 내야 하긴 하지만!!! patreon 채널에 얼마나 많은 고급질 정보가 있는지를 고려했을 때 너무나 합리적인 가격이라고 생각합니다. 관심이 있으신 분들은 꼭 한번 확인해보세요!
https://www.patreon.com/oalabs
오늘은 OALabs가 스트림에서 분석했던 DiceLoader를 같이 한번 봐보겠습니다.
IDA로 바이너리를 열어 "uHGopURQTJIYueOa"라는 이름을 가지고 있는 function이 보이길래 들어가서 봐봤더니 PE header가 보입니다.
흥미로운 value도 있어서 구글 검색을 돌려봤는데...
RefelctiveDLL Injection이더라고요. https://otx.alienvault.com/pulse/5ff4d7362e0ef3694be746f4/related에 따르면 Cobalt Strike Beacon을 로딩해서 실행시키기 위한 것이라고 합니다.
다양한 API들을 활용하여 피해자에 대한 정보를 얻을 수 있는 것을 확인할 수 있습니다.
GetAdaptersInfo: 로컬 컴퓨터에 대한 어댑터 정보 검색
GetUserNameA: 현재 스레드와 연결된 사용자의 이름 검색
GetComputerNameEx: 로컬 컴퓨터와 연결된 NetBIOS 또는 DNS 이름을 검색
GetCurrentProcessId: 현재 PID 검색
바이너리 안에 암호화되어 있는 데이터 "blop" 2개하고 암호 key도 있습니다. encryption는 단순한 XOR라서 cyberchef로 쉽게 unencrypt된 데이터를 받을 수 있습니다.
쓰레기 데이티가 많은 것 같지만 이 샘플이 사용하는 C2 서버의 아이피주소들을 받을 수 있습니다.
46.17.107.5
185.250.151.33
다른 데이터 blob은 아무것도 없는 것 같은데 Hexdump를 보면 맨 위에 있는 "01 bb" 443포트 말하는 것입니다! Cyberchef를 돌려 실용적인 결과가 없을 때 한번 Hex 결과를 보는 것이 큰 도움이 됩니다.
이번 DiceLoader는 그렇게 기능이 많은 샘플이 아니기 때문에 스트림에서 했던 Yara Rule하고 Config Extractor로 넘어가겠습니다.
Yara Rule를 쓸때 지금 보고 있는 샘플의 특이성을 알아내야 yara를 돌렸을 때 네트워크 안에 숨어 있는 다른 샘플들을 발견할 수 있습니다. 오늘 보고 있는 DiceLoader의 특이성은?
1) "Global"이라는 Stackstring이 존재하는 것이 확인됩니다. (RegretLocker에 대한 블로그에서 본 샘플도 비슷한 string들이 많습니다) 이러한 Stackstring들이 큰 도움이 됩니다
2) 샘플을 검색해보니 fnv1 hash 생성되는 것이 보입니다.
3) 위에서 언급했던 PELoader도 특이하죠
스트림에서 만들었던 Yara Rule를 보겠습니다.
오른쪽에 어샘블리 instruction들이 보이고, 왼쪽에 그 instruction들의 opcode가 보입니다. Disassembler가 만들어준 코드를 opcode와 함께 그대로 copy/paste 하면 되고, $로 변수를 만듭니다.
첫번째 변수: 어떤 악성코드의 샘플마다 살짝 다를 수도 있지만 encrypt하고 decrypt를 위한 key는 높은 확률로 자주 비슷하므로 첫 번째 변수는 암호 key입니다
두 번째 변수: 특이한 ReflectiveLoader를 사용해 보겠습니다. (1번 사진 참고)
셋 번째 변수: fnv1 hash
import "pe"
rule diceloader {
meta:
description = "Identifies diceloader"
strings:
// Mod 31 for key length
// C1 FA 04 sar edx, 4
// 8B C2 mov eax, edx
// C1 E8 1F shr eax, 1Fh
// 03 D0 add edx, eax
// 6B C2 1F imul eax, edx, 1Fh
$mod = { C1 FA 04 8B C2 C1 E8 1F 03 D0 6B C2 1F }
// Reflective loader - not in all versions
// B8 4D 5A 00 00 mov eax, 'ZM'
// 66 41 39 07 cmp [r15], ax
// 75 1B jnz short loc_18000106D
// 49 63 57 3C movsxd rdx, dword ptr [r15+3Ch]
// 48 8D 4A C0 lea rcx, [rdx-40h]
// 48 81 F9 BF 03 00 00 cmp rcx, 3BFh
// 77 0A ja short loc_18000106D
// 42 81 3C 3A 50 45 00 00 cmp dword ptr [rdx+r15], 'EP'
// 74 05 jz short loc_180001072
$reflective = { B8 4D 5A 00 00 66 41 39 07 75 ?? 49 63 57 3C 48 8D 4A C0 48 81 F9 BF 03 00 00 77 ?? 42 81 3C 3A 50 45 00 00 }
// Fnv1 Algrithm - only in new versions
$fnv1 = {33 ?? 69 ?? 93 01 00 01}
condition:
pe.is_64bit() and
$mod and
($reflective or $fnv1)
}
위에서는 Cyberchef로 샘플의 config을 받을 수 있지만, 똑같은 악성코드인 샘플을 볼 때마다 IDA를 열고, 암호 루틴을 찾아내고, 데이트를 copy/paste 등 이 모든 단계들을 걸쳐 config을 받는 것은 너무 번거롭습니다.
그래서 바로 Config Extractor를 파이썬으로 만들면 되죠!
코드를 천천히 봐보겠습니다.
Config Extraction을 위한 필수 library.
pefile는 바이너리 분석, struct는 파이썬 값과 파이썬 bytes 객체로 표현되는 C 구조체 간의 변환을 수행, re는 모든 사람들이 빠짐없이 싫어하는 regex...
import pefile
import struct
import re
위에서 언급했던 것처럼 이 샘플은 간단한 XOR 암호라서 쉽게 파이썬으로 쓰자면은...
def xor_decrypt(data, key):
out = []
for i in range(len(data)):
out.append(data[i] ^ key[i%len(key)])
return bytes(out)
위에서 import했던 pe로 바이너리를 엽니다.
바이너리에 다양한 section이 존재하는데 (data, text, r.data 등) 암호화되어 있는 데이터하고 key는 ".data"라는 section에 저장되어 있습니다. (--> get_data_section_bounds() )
egg이라는 변수가 보이는데, buffer overflow 같은 것을 공부하신 적 있으신 분들은 egghunter라고 들어보셨겠죠? 쉽게 말하자면 우리가 필요한 데이터를 찾기 위한 사냥개라고 생각하면 됩니다.
저는 아직 파이썬 코딩이 좀 약해서 100% 다 이해 못했지만, 다시 공부하고 이 블로그를 좀 업데이트해야겠네요 :)
import pefile
import struct
import re
def xor_decrypt(data, key):
out = []
for i in range(len(data)):
out.append(data[i] ^ key[i%len(key)])
return bytes(out)
def get_key_offset(pe, file_data):
# Try new method first -- key is passed as an argument
#
# BD 1F 00 00 00 mov ebp, 1Fh
# 4C 8D 05 68 1E 00 00 lea r8, key
# 44 8B CD mov r9d, ebp
# 49 8B CE mov rcx, r14
# 8D 75 F7 lea esi, [rbp-9]
# 8B D6 mov edx, esi
# E8 50 FE FF FF call sub_7FFA3BA420B8
egg = rb'\xBD\x1F\x00\x00\x00\x4C\x8D\x05(....)'
for m in re.finditer(egg, file_data):
try:
rel_offset = struct.unpack('<I',m.group(1))[0]
# Remember to add 7 for the 7 bytes in the instruction
rel_offset += 7
# Remember that this is in the .text section which is loaded before the .data
# so the relative will be postive displacement
match_offset = m.start() + 5
match_rva = pe.get_rva_from_offset(match_offset)
key_rva = match_rva + rel_offset
key_offset = pe.get_offset_from_rva(key_rva)
# Return if we have something
return key_offset
except Exception:
continue
# If we are here the new method didn't work let's try the old method
# The key is directly referenced in a crypto routine
# 48 8D 15 80 23 00 00 lea rdx, key
# 8A 04 10 mov al, [rax+rdx]
# 41 30 01 xor [r9], al
egg = rb'\x48\x8D\x15(....)\x8A\x04\x10\x41\x30\x01'
for m in re.finditer(egg, file_data):
try:
rel_offset = struct.unpack('<I',m.group(1))[0]
# Remember to add 7 for the 7 bytes in the instruction
rel_offset += 7
# Remember that this is in the .text section which is loaded before the .data
# so the relative will be postive displacement
match_offset = m.start()
match_rva = pe.get_rva_from_offset(match_offset)
key_rva = match_rva + rel_offset
key_offset = pe.get_offset_from_rva(key_rva)
# Return if we have something
return key_offset
except Exception:
continue
# If we got here we failed
return None
def get_data_section_bounds(pe):
data_section_offset= None
data_section_end_offset = None
for s in pe.sections:
if b'.data\x00' in s.Name:
data_section_offset = s.PointerToRawData
data_section_end_offset = s.PointerToRawData + s.SizeOfRawData
break
return data_section_offset,data_section_end_offset
def parse_ports(data):
out_ports = []
for ptr in range(0,len(data)-3,3):
if data[ptr] == 0:
break
tmp_port = struct.unpack('<H',data[ptr+1:ptr+3])[0]
out_ports.append(tmp_port)
return out_ports
def get_config(data):
c2s = []
pe = pefile.PE(data=file_data)
key_offset = get_key_offset(pe, file_data)
if key_offset is None:
print("Error - no key offset found!")
return c2s
key = file_data[key_offset:key_offset+31]
data_section_offset,data_section_end_offset = get_data_section_bounds(pe)
if data_section_offset is None:
print("Error - no .data section")
return c2s
ports_data = file_data[data_section_offset:key_offset]
ips_data = file_data[key_offset+31:data_section_end_offset]
ports_ptxt_data = xor_decrypt(ports_data, key)
ports = parse_ports(ports_ptxt_data)
ports = set(ports)
ips_ptxt_data = xor_decrypt(ips_data.lstrip(b'\x00'), key)
c2_ips = ips_ptxt_data.split(b'\x00')[0].split(b'|')
# Sometimes the port data is messed up
# In this case just skip it
if len(ports) == 0 or len(ports) > 5:
return [ip.decode('ascii') for ip in c2_ips]
for ip in c2_ips:
for port in ports:
c2s.append(ip.decode('ascii')+":"+str(port))
return c2s
IcedID 로더 악성코드 새로운 TTP 분석 (0) | 2023.01.11 |
---|---|
SquirrelWaffle 로더 악성코드 분석 (0) | 2022.08.02 |
Hancitor DLL 악성코드 분석 (0) | 2022.05.19 |
IcedID Loader 악성코드 분석 (0) | 2022.05.19 |