본문
가짜 alg.exe로 배우는 c#.NET 프로그래밍(치료법 포함)
[그림 1] Microsoft .NET Framework오류.
어느순간부터 컴퓨터 부팅시에 다음과 같은 오류메시지가 떴다. 이후에 계속 버튼을 누르면 이상한 웹페이지도 뜨고.. 해서 무슨일인가 알아보기로 결심했다.
"
응용 프로그램에서 처리되지 않은 예외가 발생했습니다. [계속]을 클릭하면 응용 프로그램에서 이 오류를 무시하고 계속합니다. [끝내기]를 클릭하면 응용 프로그램이 즉시 닫힙니다.
"<!DOCTYPE HTML PUBLIC "-//W3C//D" 문자열에서 'Double'형식으로 변환할 수 없습니다
윈도우 시작시에 이 오류가 발생하는것으로 보아 서비스나 시작프로그램 내에 존재할것이라 생각하고 확인해 본 결과 서비스는 별 문제가 없는거 같고 시작 레지스트리에 CPAX20이라는 놈이 존재하는것을 확인할 수 있었다. 처음에 alg.exe라는 낮익은 파일명을 가지고 있기에(Application Layer Gateway) 갸우뚱 했지만 역시나 정상적인 파일이 아니었다. 특히 원래 alg.exe는 C:\WINDOWS\System32 에 있어야 하는데 이건 C:\Program Files\Common Files에 있지 않은가!
[그림 2] msconfig으로 확인결과 SornSoft에서 만든 CPAX20이라는 프로그램을 발견할 수 있었다.
이 오류를 그만보고 싶다면 여기서 [그림 2]와 같이 CPAX20 항목의 체크를 해제해주고 확인을 누르면 끝이다... 너무 간단한가.. 해서 아래에 이 프로그램을 분석한 결과를 올려본다. 그냥 c#.net 프로그래밍의 입문정도의 난이도?
그럼 .net으로 컴파일 되었음이 오류창을 통하여 확인이 되었으니, 이제는 소스코드를 봐야겠다. obfuscator를 사용했었으면 좀 더 귀찮았겠지만 다행이도 전혀 작업을 거치지 않아 소스를 확인할 수 있었다. (그간 잘 사용하던 .net reflector가 유료화 됨에 따라서 어쩔수 없이 dotPeek을 사용했는데 무거워서 좋지않았다. 물론 여러 기능이 있었던건 좋았지만)
그럼 위의 오류창에 있었던 대로 오류 추적을 시작해본다. CPAX20.Form1.tmrLink_Tick(Object sender, EventArgs e)에서 발생하는 CPAX20.utilities.Dec(String strCode)이 오류를 발생하기에 우선 상위 메소드를 확인해본다. 참고로 메소드 명에서 알다시피 위 메소드는 Tick(타이머)을 하는데, 이 메소드 말고도 tmrOffer_Tick라는 메소드도 있으며, 각각 1000, 15000ms의 시간 후에 호출된다.(즉, 1초, 15초 후에 발생) 이후에는 각 메소드에서 RandomNumber()을 호출함으로서 다음에 발생할 시간을 랜덤으로 받아온다.
private void tmrLink_Tick(object sender, EventArgs e)
{
this.tmrLink.Interval = Conversions.ToInteger(this.RandomNumber());
this.bolLink = true;
string str = Conversions.ToString(this.WebC(this.svr + this.User + this.lLink + this.Code + this.Ver));
if (Operators.CompareString(str, (string) null, false) == 0) return;
this.strSwitch = "initial";
helper.ParsePage(Conversions.ToString(utilities.Dec(str)));
}
우선 tmrLink.Interval을 RandomNumber()을 호출하여 다음에 이 메소드가 실행될 시간을 설정해준다. 그리고 str string변수에 WebC()에서 얻어온 Http본문을 읽어와서 저장한다. 만약 얻어온 string이 비어있다면 함수를 종료한다. 그렇지 않으면 utilities.Dec(str)으로 문자열을 처리한 후에 helper.ParsePage()에 처리된 문자열을 넣어버린다. 그럼 차근차근 연관메소드를 열거해보자.
private object RandomNumber()
{
return (object) checked (new Random().Next(1, 75) * 60 * 1000);
}
이 프로그램이 visual basic에서 만들어져서 그런가 약간 갸우뚱하게 하는 면이 있다. 굳이 object로 변환한다음 Conversions.ToInteger()을 호출할 일이 있나? 아무튼 위의 메소드에서 Random().Next(1, 75)는 1과 75사이에서 랜덤한 수를 받아온다는것이다. 그리고 뒤에 60*1000을 하는것은 60*1초, 즉 랜덤한 수 n분에 한번씩 호출된다는 의미이다. 그리고 앞에 붙은 checked는 컴파일러가 자동으로 집어넣어준거 같은데(코드 꼬라지를 보면 코더는 분명 무려 이걸 넣을 머리는 안되는것 같다) 산술 계산에 있어서 오버플로우가 발생하는지 검출해 주는 기능을 한다. 그리고 object로 반환된 랜덤한 숫자를 Conventions.ToInteger()을 통해서 Integer로 변환해 주어 interval 값으로 돌려받는다.
그리고 세번째줄의 this.svr + this.User + this.lLink + this.Code + this.Ver에는 어떤 값이 들어갈까?? 이들값은 utilities.settings변수에 배열형태로 존재하는데, 이는 utilities.GetUser()에서 다음과 같이 존재한다.
public static void GetUser()
{
FileSystem.FileOpen(1, Application.ExecutablePath, OpenMode.Binary, OpenAccess.Read, OpenShare.Default, -1);
string Expression = Microsoft.VisualBasic.Strings.Space(checked ((int) FileSystem.LOF(1)));
FileSystem.FileGet(1, ref Expression, -1L, false);
FileSystem.FileClose(1);
utilities.settings = Microsoft.VisualBasic.Strings.Split(Expression, "][", -1, CompareMethod.Binary);
utilities.settings[1] = utilities.settings[1];
utilities.settings[2] = utilities.settings[2];
utilities.settings[3] = utilities.settings[3];
utilities.settings[4] = utilities.settings[4];
utilities.settings[5] = utilities.settings[5];
utilities.settings[6] = utilities.settings[6];
}
Application.ExecutablePath는 현재 실행하는 파일의 파일명을 포함한 경로를 나타내므로 FileSystem.FileOpen으로 C:\Program Files\Common Files\alg.exe 자신을 읽어온다는것을 알 수 있다. 그리고 Expression 문자열변수는 이 파일의 길이만큼을 공백으로 채워지게 된다.(왜 이러는지.. 역시 똑똑한 컴파일러 떄문인가) 그다음 FileSystem.FileGet으로 Expression을 파일 내용으로 채운다. 하고나서 utillities.settings 변수는 "]["으로 분리되는 문자열 배열으로 채워지게 된다. 어떻게 해놨을까 하고 파일을 메모장++으로 열어보았다.
[그림 3] 실행파일 뒤에 붙어버린 문자열
위 그림과 같이 치사하게 실행파일 맨 뒤에 문자열을 붙여서 사용하고 있었다. 뭐 유지보수에 편하겠지만 이건 너무하잖아. 그리고 차라리 이 문자열을 뽑아내는 과정에 모두 split으로 하고 있으니 기가막힐 따름이다. 아무튼 결과적으로 utillities.settings[0]은 여태까지의 모든 문자, 그리고 utillities.settings[1]은 fadetoblack, [2]는 http://www.sorn=soft.com/products/c2/user/ 이런식으로 가게 된다. 그리고 이것들은 Form1_Load()에서 다시 할당이 된다.(이 메소드를 미리 써놓았어야 했는데..)
private void Form1_Load(object sender, EventArgs e)
{
this.Opacity = 0.0;
utilities.GetUser();
this.User = utilities.settings[1];
this.svr = utilities.settings[2];
this.iLink = utilities.settings[3];
this.gLink = utilities.settings[4];
this.lLink = utilities.settings[5];
this.Ver = utilities.settings[6];
this.bolLink = false;
this.Web.Navigate(this.svr + this.User + this.iLink + this.Code + this.Ver);
this.tmrOffer.Enabled = true;
this.tmrLink.Enabled = true;
}
다른 변수들은 해결이 됐고, 이젠 Code라는 놈의 정체를 알아야 한다. 이놈은 너무나 위대해서 무려 Form1 생성자님 밑에서 값을 할당받는다. 가만보니 utillities.GetVolumeSerialNumber()을 호출한 값을 Utillities.Enc()하여 얻는데 메소드들을 들여다보자,,,
public Form1()
{
this.Load += new EventHandler(this.Form1_Load);
this.Code = Conversions.ToString(utilities.Enc(utilities.GetVolumeSerialNumber()));
this.strOffer = new string[2];
this.strSettings = new string[4];
this.InitializeComponent();
}
public static string GetVolumeSerialNumber()
{
string str = (string) null;
ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher("SELECT * FROM Win32_LogicalDisk WHERE Name = '" + Interaction.Environ("SystemDrive") + "'");
ManagementObjectCollection.ManagementObjectEnumerator enumerator;
try
{
enumerator = managementObjectSearcher.Get().GetEnumerator();
while (enumerator.MoveNext())
str = Conversions.ToString(enumerator.Current["VolumeSerialNumber"]);
}
finally
{
if (enumerator != null)
enumerator.Dispose();
}
return str;
}
public static object Enc(string strCode)
{
string str = (string) null;
int num = 1;
int length = strCode.Length;
int Start = num;
while (Start <= length)
{
str = Start == Microsoft.VisualBasic.Strings.Len(strCode) ? str + Conversions.ToString(checked (Microsoft.VisualBasic.Strings.Asc(Microsoft.VisualBasic.Strings.Mid(strCode, Start, 1)) * 33)) : str + Conversions.ToString(checked (Microsoft.VisualBasic.Strings.Asc(Microsoft.VisualBasic.Strings.Mid(strCode, Start, 1)) * 33)) + ".";
checked { ++Start; }
}
return (object) str;
}
차근차근 가보자. Interaction.Environ("SystemDrive")으로 SystemDrive 환경변수를 불러오는데, 이는 윈도우 시스템 폴더가 있는 드라이브를 지칭하며 대개 C:를 가리킨다. 그리고 나서 얻는 VolumeSerialNumber, 볼륨 일련 번호로서 cmd에서 dir를 침으로서 확인할 수 있다. /w 스위치는 그냥 보기 편하라고. 본인의 경우에는 720F-99A1가 나왔으며, 포맷시마다 이 값은 변경이 된다.(그리고 임의로 이 값을 바꿀수도 있다.)
[그림 4] VolumeSerialNumber, 볼륨 일련 번호는 dir으로 간단히 확인 가능하다
이 값을 Enc() 한다는데. 이게 그나마 이 포스팅에서 의미가 있지않나 싶다. 우선 알아야 할점은 Microsoft.VisualBasic.Strings.Asc()는 문자에 해당하는 문자 코드를 나타내는 Integer 값을 반환하고. Microsoft.VisualBasic.Strings.Mid()는 문자열에서 지정한 문자 수가 포함된 문자열을 반환한다는 것이다. 그리고 while내의 식은 3항연산자로서, 조건 ? 참일때의결과 : 거짓일때의결과 를 나타낸다. 즉, 위는 다음과 같이 풀어쓸 수 있다.
if(Start==Microsoft.VisualBasic.Strings.Len(strCode))
str = str + Conversions.ToString(checked (Microsoft.VisualBasic.Strings.Asc(Microsoft.VisualBasic.Strings.Mid(strCode, Start, 1)) * 33))
else
str = str + Conversions.ToString(checked (Microsoft.VisualBasic.Strings.Asc(Microsoft.VisualBasic.Strings.Mid(strCode, Start, 1)) * 33)) + "."
즉, 만약 현재 Start의 값이 strCode의 문자열 길이와 일치하지 않는다면 뒤에 .을 붙여서 계속 문자열을 잇고, 문자열 길이와 일치할 경우 (끝에 도달했을 경우). 뒤에 아무것도 붙이지 않는다.. 라는 허무한 결론. 이런 코드를 보고 바로 '더럽고 쓸데없는 코드'라고 부른다. 아무튼 계속 진행해서, 위에서 알아본 720F-99A1이 Enc()를 통해서 어떻게 변화되는지 보자. http://www.asciitable.com/ 에서 아스키 코드 테이블을 확인하면서 진행을 한다. 우선 7에 해당하는 Integer(정수)는 10진수로 55, 여기에 33을 곱하면 1815. 그리고 네번째 문자인 F는 10진수로 70이므로 여기에 33을 곱하면 2310.. 이런식으로 나간다.
결국 이 모든것에 의해서 this.Web.Navigate(this.svr + this.User + this.iLink + this.Code + this.Ver); 가 실행되면
http://www.sorn-soft.com/products/c2/user/fadetoblack/a.php?c=1815.1650.1584.2310.1881.1881.2145.1617&v=1.0.0.6 에 접속하는 것이다. (프로그램 처음 실행시 뜨는 창). 마찬가지로 tmrLink_Tick()에서도 동일한 페이지에 들어가서 해당 http 스트림을 읽어온다. 아래는 그 스트림의 예제를 보여준다.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">
<!-- turing_cluster_prod -->
<html>
<head>
<title>sorn-soft.com</title>
<meta name="keywords" content="sorn-soft.com">
<meta name="description" content="sorn-soft.com">
<meta name="robots" content="INDEX, FOLLOW">
<meta name="revisit-after" content="10">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<frameset rows="100%,*" frameborder="no" border="0" framespacing="0">
<frame src="http://www.sorn-soft.com?epl=zchdT9HdfIUdkgggyWZcR8fldjuQUDhFchf_LEpzXkafEnCxSCpayoAYmdcAUtguheRgZIGeVrS1yjwYlSSzwWi-QSZ7jzPlGQi1Q9FgREDMc1BKRYFtU3qiFSaJ-S7J1SoKGUgeyKA8aZOeIqSnaQBkIkNjmiJ0ACAQ3Ke_AADgfwdAAECAWwkAAMfldjtZUyZZQTE2aFpCewAAAPA" name="sorn-soft.com">
</frameset>
<noframes>
<body>
<a href="http://www.sorn-soft.com?epl=zchdT9HdfIUdkgggyWZcR8fldjuQUDhFchf_LEpzXkafEnCxSCpayoAYmdcAUtguheRgZIGeVrS1yjwYlSSzwWi-QSZ7jzPlGQi1Q9FgREDMc1BKRYFtU3qiFSaJ-S7J1SoKGUgeyKA8aZOeIqSnaQBkIkNjmiJ0ACAQ3Ke_AADgfwdAAECAWwkAAMfldjtZUyZZQTE2aFpCewAAAPA">
Click here to go to sorn-soft.com </a>.
</body>
</noframes>
</html>
아하, 오류에서 보았던 문자열이 보인다. 이 문자열이 utilities.Dec()으로 들어간 후에 오류가 생기는데 최종적으로 utilities.Dec()을 확인해보고 글을 종료하려 한다,
public static object Dec(string strCode)
{
string[] strArray = strCode.Split('.');
string str = (string) null;
int num1 = 0;
int num2 = checked (Enumerable.Count<string>((IEnumerable<string>) strArray) - 1);
int index = num1;
while (index <= num2)
{
str = str + Conversions.ToString(Microsoft.VisualBasic.Strings.Chr(checked ((int) Math.Round(unchecked (Conversions.ToDouble(strArray[index]) / 33.0)))));
checked { ++index; }
}
return (object) str;
}
추측컨데 원래 이 프로그램을 계획했을 경우에 위 URL로 접근하면 아무내용없이 그냥 1815.1650.1584.2310.1881.1881.2145.1617같은 문자열만 입력이 되어있었을거다. 그런데 상황이 바뀌었는지 페이지 내용이 변경되었고, 그래서 잡다한 html코드가 섞여버린것이다. 가장 처음줄에 위 html코드를 .으로 split하여 배열로 저장하고, 배열내의 항목 하나하나를 33.0으로 나누는 과정에서 (다시 원래 볼륨 일련번호로 돌려보내려는) 문제가 생긴것이다. html 소스의 가장 첫줄만 .단위로 나누어봐도
strArray[0] = <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4
strArray[1] = 01 Frameset//EN" "http://www
strArray[2] = w3
strArray[3] = org/TR/html4/frameset
strArray[4] = dtd">
이렇게 되는데 이 각각을 어떻게 33.0으로 나누겠는가!! 결국 개발자(이런 말 쓰는것조차도 아깝다!)의 예외처리를 안한 실수와 서버상의 변경으로 인해 자신의 신분을 노출하는 불상사가 일어난것이다. 즉 "예외처리를 잘하자"가 이 프로그램의 교훈이라고나 할까?? 처음에는 뭔가 복잡한게 있곘지 하고 분석을 시작했는데 너무 허무하다-_- 아래에는 혹시 필요한 사람이 있을지 몰라(없을듯) 이 파일을 첨부한다 파일이 바이러스로 진료되는것을 막기위하여 암호를 걸어놓았으며 암호는 frontjang이다.
그냥 적기 아쉬워서 조금 더 뒤적여봤더니.. http://www.blackhatworld.com/를 참조. 이런 분야가 있네....
댓글