【文章标题】: cryxenet0.02unpackme完全分析
【文章作者】: 峰回路转
【作者邮箱】: killbug2004@gmail.com
【软件名称】: cryxenet0.02unpackme
【下载地址】: http://www.crackmes.de/users/tfb/cryxenet_0.02_unpackme/
【操作平台】: .net
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教! 如果我的代码你看着很眼熟,那绝对不是偶然^V^
--------------------------------------------------------------------------------
【详细过程】
这个unpackme是作者的.net保护壳保护的,所以继续阅读之前需要对.net原理和.net PE文件结构有一定了解。程序由三个
文件组成,只有unpackme.exe是可以运行的。
一:unpackme.exe的分析
PE工具察看unpackme.exe为.net PE,用.net Reflector载入unpackme进行分析。程序入口函数
main无法以C#等高级语言查看,只能查看il代码,方法开始代码如下:
.method public static void main() cil managed
{
.custom instance void [mscorlib]System.STAThreadAttribute::.ctor()
.entrypoint
.maxstack 4
.locals init (
[0] int64 num,
[1] class [mscorlib]System.IO.FileInfo info,
[2] class [mscorlib]System.Reflection.Assembly 'assembly',
[3] object[] objArray,
[4] string[] strArray,
[5] int64 num2,
[6] int64 num3,
[7] int64 num4,
[8] int64 num5,
[9] int64 num6,
[10] class [mscorlib]System.IO.FileStream stream,
[11] native int ptr,
[12] uint8[] buffer,
[13] class Project1.Program/obfuscation8 obfuscation,
[14] uint8[] buffer2,
[15] int64 num7,
[16] int64 num8,
[17] int64 num9)
L_0000: nop
/*
L_0001: ldc.i4.s 0x63
L_0003: conv.i8
L_0004: stloc.s num2
L_0006: ldc.i4.s 0x4b
L_0008: conv.i8
L_0009: stloc.s num3
L_000b: ldc.i4 0x38d
L_0010: conv.i8
L_0011: stloc.s num4
L_0013: call int64 Project1.Program::IsDebuggerPresent()
L_0018: pop
L_0019: ldloc.s num3
L_001b: br.s L_003d // 无条件跳转,所以reflector无法反编译为高级语言
L_001d: add.ovf
L_001e: stloc.s num2
L_0020: ldloc.s num4
L_0022: ldc.i4.7
L_0023: conv.i8
L_0024: sub.ovf
L_0025: stloc.s num4
L_0027: ldloc.s num4
L_0029: ldloc.s num2
L_002b: add.ovf
L_002c: stloc.s num3
L_002e: ldloc.s num4
L_0030: ldc.i4.7
L_0031: conv.i8
L_0032: add.ovf
L_0033: stloc.s num4
L_0035: call int64 Project1.Program::IsDebuggerPresent()
L_003a: pop
L_003b: ldloc.s num3
L_003d: ldloc.s num4
L_003f: add.ovf
L_0040: stloc.s num2
L_0042: ldloc.s num4
L_0044: ldc.i4.7
L_0045: conv.i8
L_0046: sub.ovf
L_0047: stloc.s num4
L_0049: ldloc.s num4
L_004b: ldloc.s num2
L_004d: add.ovf
L_004e: stloc.s num3
L_0050: ldloc.s num4
L_0052: ldc.i4.7
L_0053: conv.i8
L_0054: add.ovf
L_0055: stloc.s num4
L_0057: call int64 Project1.Program::IsDebuggerPresent()
L_005c: pop
L_005d: ldloc.s num3
L_005f: ldloc.s num4
L_0061: add.ovf
L_0062: stloc.s num2
L_0064: ldloc.s num4
L_0066: ldc.i4.7
L_0067: conv.i8
L_0068: sub.ovf
L_0069: stloc.s num4
L_006b: ldloc.s num4
L_006d: ldloc.s num2
L_006f: add.ovf
L_0070: stloc.s num3
L_0072: ldloc.s num4
L_0074: ldc.i4.7
L_0075: conv.i8
L_0076: add.ovf
L_0077: stloc.s num4
L_0079: ldloc.s num3
L_007b: ldloc.s num4
L_007d: add.ovf
L_007e: stloc.s num2
L_0080: ldloc.s num4
L_0082: ldc.i4.7
L_0083: conv.i8
L_0084: sub.ovf
L_0085: stloc.s num4
L_0087: ldloc.s num4
L_0089: ldloc.s num2
L_008b: add.ovf
L_008c: stloc.s num3
L_008e: ldloc.s num4
L_0090: ldc.i4.7
L_0091: conv.i8
L_0092: add.ovf
L_0093: stloc.s num4
*/
//创建Fileinfo对象,关联"native.dll"
L_0095: ldstr "native.dll" //将"native.dll"压入堆栈
L_009a: newobj instance void [mscorlib]System.IO.FileInfo::.ctor(string)
//创建一个FileInfo对象
L_009f: stloc.1 //将堆栈顶的数据保存到索引为1的局部变量中
//即将FileInfo对象保存到info中
对il代码进行简单分析可以知道,L_001b: 标签处的无条件跳转指令导致reflector无法反编译成高级语言,这里和汇编中的无条件
跳转的花指令类似。这个函数里面有大量对num4,num3等变量的操作,代码都是重复,且这些变量没有参与别的操作,是垃圾代码。
用ildasm将unpackme反编译成il,将垃圾代码注释掉,再用ilasm将修改后的il代码编译成exe,用reflector载入生成的程序,现在
代码可以反编译成C#代码:
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Reflection;
namespace Project1
{
class Program
{
private delegate int obfuscation8();
public static void Main()
{
long num6;
FileStream stream = new FileInfo("native.dll").OpenRead();
long length = stream.Length;
byte[] array = new byte[((int)length) + 1];
stream.Read(array, 0, (int)length);
stream.Close();
long num7 = length;
for (num6 = 0L; num6 <= num7; num6 += 1L)
{
array[(int)num6] = (byte)(array[(int)num6] ^ 0x37);
}
IntPtr destination = new IntPtr();
destination = Marshal.AllocCoTaskMem((int)length);
Marshal.Copy(array, 0, destination, (int)length);
obfuscation8 delegateForFunctionPointer = (obfuscation8)Marshal.GetDelegateForFunctionPointer(destination, typeof(obfuscation8));
File.WriteAllBytes("dump.bin", array);
long num = new long();
num = delegateForFunctionPointer();
System.Console.WriteLine(" num = " + num);
stream = new FileInfo("cryxed.dll").OpenRead();
length = stream.Length;
byte[] buffer2 = new byte[((int)length) + 1];
stream.Read(buffer2, 0, (int)length);
stream.Close();
long num8 = length;
for (num6 = 0L; num6 <= num8; num6 += 1L)
{
buffer2[(int)num6] = (byte)(buffer2[(int)num6] ^ 0x37);
}
File.WriteAllBytes("decrypted_assembly.exe", buffer2);
Assembly assembly = Assembly.Load(buffer2);
object[] parameters = new object[1];
string[] strArray = new string[] { "" };
parameters[0] = strArray;
assembly.EntryPoint.Invoke(null, parameters);
}
}
}
代码功能很明了:
1、读取"native.dll"中的数据,异或0x37解密,然后用Marshal提供的方法以函数的形式直接运行解密的数据,可以猜想这个代码是需要自己重定位的。
2、读入"cryxed.dll"文件,异或0x37解密,以Assembly的形式执行,可以判断解密后的数据为一个assembly。
由于unpackme的功能简单,可以完全反编译成C#等高级代码,上面代码中的两行注释的代码是对反编译后C#代码的修改,将解密后的native.dll和cryxed.dll
分别写入dump.bin和decrypted_assembly.exe,都是简单的异或加密,当然可以用工具或代码自己处理这两个原始文件,接下来就是对这两个解密后的文件分析。
二、dump.bin 壳loader的分析
知道dump.bin是可以直接在内存中运行的代码,类似shellcode和普通壳的loader,所以用IDA以二进制形式载入,选择32位模式在文件开始处进行反汇编分析。
代码中有一些花指令:
call sub_10AC5
seg000:00000AC5 JunkFun proc near ; CODE XREF: sub_10151+Ap
seg000:00000AC5 ; sub_10151+22p
seg000:00000AC5 inc dword ptr [esp]
seg000:00000AC8 retn
seg000:00000AC8
seg000:00000AC8 JunkFun endp
由于这个垃圾函数的调用比较多,所以利用IDC脚本处理一下便于分析:
#include <idc.idc>
static main(void)
{
auto EA,Counter;
auto Address;
EA = MinEA() + 3;
Counter = 0;
while(EA != BADADDR)
{
Address = FindBinary(EA, SEARCH_DOWN, "00 00 FF");
if(Address == BADADDR)
break;
if((Byte(Address - 3) == 0xE8) && (Byte(Address + 2) == 0xFF))
{
PatchDword(Address - 3,0x90909090);
PatchWord(Address + 1,0x9090);
Counter++;
EA = Address + 1;
}
EA ++;
}
Message("There are %d junk code blocks.\n",Counter);
Message("done.\n");
}
去花后的代码干净很多,代码不是很长,所以从头到尾分析,这是一个典型的loader,具体分析参考我的idb文件,执行流程如下:
1、重定位代码以便正确访问变量
2、利用PEB获取kernel32的基址,然后利用PE文件结构的导出表和函数名hash,得到LoadLibrayA、GetProcAddress函数地址
再利用这两个函数获取代码要用到的一些API地址,这里利用模块mscorjit.dll的getJit得到.net中非常重要的函数compileMethod地址
3、获取Strongnamesignatureverificationfromimage函数,通过inline hook挂钩该函数,在函数开始处跳转到代码Code_1函数内执行
我们知道.net镜像启动后载入内存,系统通常都是调用Strongnamesignatureverificationfromimage对其进行强命名校验,loader挂钩该函数
可以实现对后面载入的assembly执行前进行处理。
接下来分析挂钩Strongnamesignatureverificationfromimage函数代码的功能:
1、函数利用Strongnamesignatureverificationfromimage的参数获取待校验镜像的基址
2、获取镜像基址后,利用PE文件结构获取镜像FileAlignment值
3、利用0x00002050这个特殊的值,搜索要解密assembly的MethodDef方法描述表
4、解密方法表的method header(分为tiny header和fat hader)
5、挂钩compileMethod函数,将该函数的调用指向到Code_2
6、解除对Strongnamesignatureverificationfromimage的hook,调用系统的Strongnamesignatureverificationfromimage继续运行
我们看到对Strongnamesignatureverificationfromimage挂钩函数的功能是对后面执行的assembly的方法描述表的表头进行解密,这样运行时系统就可以
获取要执行方法的正确信息,然后挂钩compileMethod实现运行时解密方法体的il代码,解密il是挂钩compileMethod的函数Code_2的主要功能,这里就不再讨论,
主要注意一下tiny和fat的il解密有一点区别。
现在已经完成这个unpackme的最关键部分的分析,作者的壳对pe文件进行处理,主要是加密文件的MethodDef方法描述表,处理后的程序,利用jit hook
挂钩compileMethod实现动态解密il代码,这样就无法利用reflector等工具静态分析,上面生成的decrypted_assembly.exe文件用reflector察看,所有
的方法都是无法处理的,应为方法表和il都是加密的。
三、脱壳机的编写
有了对loader的分析可以知道decrypted_assembly.exe要正确运行的话,只需要解密方法表就可以,不再依赖别的模块。
这里我就提取loader中简单循环移位的代码静态实现对cryxed.dll解密,脱壳的关键是解析.net PE文件定位到PE文件的MethodDef,加密算法很简单
具体代码可能有点乱,可以参考附件中的DecryptAssembly程序的具体代码。对于动态脱壳机的编写也是可以实现的,注入程序inline hook compileMethod函数
可以得到解密的il代码,重构PE文件的MethodDef是可以实现的,这里就不再讨论了。
四、注册机的编写
有了脱壳机对cryxed.dll脱壳,生成的文件后缀名改为exe,可以直接运行,程序是vb.net的编写,用reflector直接查看源码,直接程序中的解密函数
粘贴到自己的程序中就OK,调用系统加密模块,rc2加密,base64编码
public string TripleDESDecode(string value, string key)
{
TripleDESCryptoServiceProvider provider = new TripleDESCryptoServiceProvider();
provider.IV = new byte[8];
provider.Key = new PasswordDeriveBytes(key, new byte[0]).CryptDeriveKey("RC2", "MD5", 0x80, new byte[8]);
byte[] buffer = Convert.FromBase64String(value);
MemoryStream stream2 = new MemoryStream(value.Length);
CryptoStream stream = new CryptoStream(stream2, provider.CreateDecryptor(), CryptoStreamMode.Write);
stream.Write(buffer, 0, buffer.Length);
stream.FlushFinalBlock();
byte[] buffer2 = new byte[((int)(stream2.Length - 1L)) + 1];
stream2.Position = 0L;
stream2.Read(buffer2, 0, (int)stream2.Length);
stream.Close();
return Encoding.UTF8.GetString(buffer2);
}
注册机的代码参考附件的cryxenet002_keygen,至此基本完成的这个unpackme的全部分析,如果要很好的去理解,需要了解.net运行时的原理和.net PE文件的结构,
其实和传统的壳有些方面是相通的。
--------------------------------------------------------------------------------
【经验总结】
这个程序是jit hook实现的很简陋的.net保护壳,同当前流行的商业保护壳是有很大的差距,字符串加密、名称和流程混淆、复杂的分
块加密、anti-debug、anti-dump分析起来是很伤身体的,不过透过这个unpackme也可以小窥一下当前.net保护的壳基本原
理,大部分都是在compileMethod hook上下功夫。如果要写保护壳的话,还可以在许多许多方向上进行扩充的,传统的保
护壳是有许多可借鉴的地方。.net方面的资料可以参考Daniel Pistell老大的网站www.ntcore.com
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!
[培训]内核驱动高级班,冲击BAT一流互联网大厂工
作,每周日13:00-18:00直播授课