许多人都知道利用dnSpy单步调试+Dump+CodeCracker的一系列工具可以脱去ConfuserEx壳,这些在网上都有教程,但是并没有文章说明过背后的原理。本文讲尽可能详细解说ConfuserEx的Anti Tamper与Anti Dump。
(有耐心并且了解一点点的PE结构完全可以看懂)
ConfuserEx整个项目结构
在开始讲解之前,我们大概了解一下ConfuserEx项目的结构。
我们用Visual Studio打开ConfuserEx,项目大概是这样的:
- Confuser.CLI的是命令行版本,类似de4dot的操作方式。
- Confuser.Core是核心,把所有部分Protection组合到一起。
- Confuser.DynCipher可以动态生成加密算法。
- Confuser.Protections里面包含了所有Protection,这是需要研究的部分。
- Confuser.Renamer可以对类名、方法名等重命名,包括多种重命名方式,比如可逆的重命名,这些没有在ConfuserEx的GUI里面显示就是了。
- Confuser.Runtime是运行时,比如Anti Dump的实现,其实就在这个项目里面。上面提到的Confuser.Protections会把Confuser.Runtime中的Anti Dump的实现注入到目标程序集。
- ConfuserEx是GUI,没必要多说。
整个项目几乎没什么注释,下面的中文注释均为我添加的。
Anti Dump
Anti Dump比起Anti Tamper简单不少,所以我们先来了解一下Anti Dump。
Anti Dump的实现只有一个方法,非常简洁。
我们找到Confuser.Protections项目的AntiDumpProtection.cs。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| protected override void Execute(ConfuserContext context, ProtectionParameters parameters) { TypeDef rtType = context.Registry.GetService<IRuntimeService>().GetRuntimeType("Confuser.Runtime.AntiDump");
var marker = context.Registry.GetService<IMarkerService>(); var name = context.Registry.GetService<INameService>();
foreach (ModuleDef module in parameters.Targets.OfType<ModuleDef>()) { IEnumerable<IDnlibDef> members = InjectHelper.Inject(rtType, module.GlobalType, module);
MethodDef cctor = module.GlobalType.FindStaticConstructor(); var init = (MethodDef)members.Single(method => method.Name == "Initialize"); cctor.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Call, init));
foreach (IDnlibDef member in members) name.MarkHelper(member, marker, (Protection)Parent); } }
|
AntiDumpProtection做的只是注入,所以我们转到Confuser.Runtime中的AntiDump.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
| static unsafe void Initialize() { uint old; Module module = typeof(AntiDump).Module; var bas = (byte*)Marshal.GetHINSTANCE(module); byte* ptr = bas + 0x3c; byte* ptr2; ptr = ptr2 = bas + *(uint*)ptr; ptr += 0x6; ushort sectNum = *(ushort*)ptr; ptr += 14; ushort optSize = *(ushort*)ptr; ptr = ptr2 = ptr + 0x4 + optSize;
byte* @new = stackalloc byte[11]; if (module.FullyQualifiedName[0] != '<') { byte* mdDir = bas + *(uint*)(ptr - 16);
if (*(uint*)(ptr - 0x78) != 0) { byte* importDir = bas + *(uint*)(ptr - 0x78); byte* oftMod = bas + *(uint*)importDir; byte* modName = bas + *(uint*)(importDir + 12); byte* funcName = bas + *(uint*)oftMod + 2; VirtualProtect(modName, 11, 0x40, out old);
*(uint*)@new = 0x6c64746e; *((uint*)@new + 1) = 0x6c642e6c; *((ushort*)@new + 4) = 0x006c; *(@new + 10) = 0;
for (int i = 0; i < 11; i++) *(modName + i) = *(@new + i);
VirtualProtect(funcName, 11, 0x40, out old);
*(uint*)@new = 0x6f43744e; *((uint*)@new + 1) = 0x6e69746e; *((ushort*)@new + 4) = 0x6575; *(@new + 10) = 0;
for (int i = 0; i < 11; i++) *(funcName + i) = *(@new + i); }
for (int i = 0; i < sectNum; i++) { VirtualProtect(ptr, 8, 0x40, out old); Marshal.Copy(new byte[8], 0, (IntPtr)ptr, 8); ptr += 0x28; } VirtualProtect(mdDir, 0x48, 0x40, out old); byte* mdHdr = bas + *(uint*)(mdDir + 8); *(uint*)mdDir = 0; *((uint*)mdDir + 1) = 0; *((uint*)mdDir + 2) = 0; *((uint*)mdDir + 3) = 0;
VirtualProtect(mdHdr, 4, 0x40, out old); *(uint*)mdHdr = 0; mdHdr += 12; mdHdr += *(uint*)mdHdr; mdHdr = (byte*)(((ulong)mdHdr + 7) & ~3UL); mdHdr += 2; ushort numOfStream = *mdHdr; mdHdr += 2; for (int i = 0; i < numOfStream; i++) { VirtualProtect(mdHdr, 8, 0x40, out old); mdHdr += 4; mdHdr += 4; for (int ii = 0; ii < 8; ii++) { VirtualProtect(mdHdr, 4, 0x40, out old); *mdHdr = 0; mdHdr++; if (*mdHdr == 0) { mdHdr += 3; break; } *mdHdr = 0; mdHdr++; if (*mdHdr == 0) { mdHdr += 2; break; } *mdHdr = 0; mdHdr++; if (*mdHdr == 0) { mdHdr += 1; break; } *mdHdr = 0; mdHdr++; } } } else { uint mdDir = *(uint*)(ptr - 16); uint importDir = *(uint*)(ptr - 0x78);
var vAdrs = new uint[sectNum]; var vSizes = new uint[sectNum]; var rAdrs = new uint[sectNum]; for (int i = 0; i < sectNum; i++) { VirtualProtect(ptr, 8, 0x40, out old); Marshal.Copy(new byte[8], 0, (IntPtr)ptr, 8); vAdrs[i] = *(uint*)(ptr + 12); vSizes[i] = *(uint*)(ptr + 8); rAdrs[i] = *(uint*)(ptr + 20); ptr += 0x28; }
if (importDir != 0) { for (int i = 0; i < sectNum; i++) if (vAdrs[i] <= importDir && importDir < vAdrs[i] + vSizes[i]) { importDir = importDir - vAdrs[i] + rAdrs[i]; break; } byte* importDirPtr = bas + importDir; uint oftMod = *(uint*)importDirPtr; for (int i = 0; i < sectNum; i++) if (vAdrs[i] <= oftMod && oftMod < vAdrs[i] + vSizes[i]) { oftMod = oftMod - vAdrs[i] + rAdrs[i]; break; } byte* oftModPtr = bas + oftMod; uint modName = *(uint*)(importDirPtr + 12); for (int i = 0; i < sectNum; i++) if (vAdrs[i] <= modName && modName < vAdrs[i] + vSizes[i]) { modName = modName - vAdrs[i] + rAdrs[i]; break; } uint funcName = *(uint*)oftModPtr + 2; for (int i = 0; i < sectNum; i++) if (vAdrs[i] <= funcName && funcName < vAdrs[i] + vSizes[i]) { funcName = funcName - vAdrs[i] + rAdrs[i]; break; } VirtualProtect(bas + modName, 11, 0x40, out old);
*(uint*)@new = 0x6c64746e; *((uint*)@new + 1) = 0x6c642e6c; *((ushort*)@new + 4) = 0x006c; *(@new + 10) = 0;
for (int i = 0; i < 11; i++) *(bas + modName + i) = *(@new + i);
VirtualProtect(bas + funcName, 11, 0x40, out old);
*(uint*)@new = 0x6f43744e; *((uint*)@new + 1) = 0x6e69746e; *((ushort*)@new + 4) = 0x6575; *(@new + 10) = 0;
for (int i = 0; i < 11; i++) *(bas + funcName + i) = *(@new + i); }
for (int i = 0; i < sectNum; i++) if (vAdrs[i] <= mdDir && mdDir < vAdrs[i] + vSizes[i]) { mdDir = mdDir - vAdrs[i] + rAdrs[i]; break; } byte* mdDirPtr = bas + mdDir; VirtualProtect(mdDirPtr, 0x48, 0x40, out old); uint mdHdr = *(uint*)(mdDirPtr + 8); for (int i = 0; i < sectNum; i++) if (vAdrs[i] <= mdHdr && mdHdr < vAdrs[i] + vSizes[i]) { mdHdr = mdHdr - vAdrs[i] + rAdrs[i]; break; } *(uint*)mdDirPtr = 0; *((uint*)mdDirPtr + 1) = 0; *((uint*)mdDirPtr + 2) = 0; *((uint*)mdDirPtr + 3) = 0;
byte* mdHdrPtr = bas + mdHdr; VirtualProtect(mdHdrPtr, 4, 0x40, out old); *(uint*)mdHdrPtr = 0; mdHdrPtr += 12; mdHdrPtr += *(uint*)mdHdrPtr; mdHdrPtr = (byte*)(((ulong)mdHdrPtr + 7) & ~3UL); mdHdrPtr += 2; ushort numOfStream = *mdHdrPtr; mdHdrPtr += 2; for (int i = 0; i < numOfStream; i++) { VirtualProtect(mdHdrPtr, 8, 0x40, out old); mdHdrPtr += 4; mdHdrPtr += 4; for (int ii = 0; ii < 8; ii++) { VirtualProtect(mdHdrPtr, 4, 0x40, out old); *mdHdrPtr = 0; mdHdrPtr++; if (*mdHdrPtr == 0) { mdHdrPtr += 3; break; } *mdHdrPtr = 0; mdHdrPtr++; if (*mdHdrPtr == 0) { mdHdrPtr += 2; break; } *mdHdrPtr = 0; mdHdrPtr++; if (*mdHdrPtr == 0) { mdHdrPtr += 1; break; } *mdHdrPtr = 0; mdHdrPtr++; } } } }
|
这里面修改导入表的部分其实是可有可无的,这个是可逆的,清空节名称也是是可选的。
其中非常重点的是将IMAGE_COR20_HEADER.MetaData清零,CLR已经完成了元数据的定位,并且保存了有关数据(可以使用CE搜索内存验证,搜索ImageBase+MetaData.VirtualAddress),不再需要这个字段,是可以清零的,但是我们读取元数据,是需要这个字段的。
接下来Anti Dump会删除BSJB标志,这样就无法搜索到STORAGESIGNATURE了。还有元数据流头的rcName字段,一并清零,这样也会让我们无法定位到元数据结构体,但是CLR不再需要这些了。
解决这个的办法很简单,把<Module>::.cctor()的call void Confuser.Runtime.AntiDump::Initialize()这条指令nop掉。我们要如何定位到这条指令呢?
这里有个投机取巧的办法,解决Anti Tamper之后,在dnSpy里面找出现了
1 2 3 4 5
| Module module = typeof(AntiDump).Module; byte* bas = (byte*)Marshal.GetHINSTANCE(module); ...... if (module.FullyQualifiedName[0] != '<'){ }
|
这样的方法,并且这个方法还多次调用了VirtualProtect,原版ConfuserEx是调用了14次。
把call 这个方法的地方nop掉,注意显示模式切换到IL,然后点一下IL所在的FileOffset,用十六进制编辑器改成0,不然容易出问题。
Anti Tamper
Anti Tamper稍微麻烦一些,看不懂的地方实际操作一下,到ConfuserEx项目里面调试一下!!!!!!
分析
ConfuserEx里面有2种AntiTamper模式,一种的Hook JIT,另一种是原地解密。Hook JIT算是半成品,还没法正常使用,所以我们实际上看到的是原地解密模式,强度不是特别高。
我们转到Confuser.Protections项目的AntiTamper\NormalMode.cs
这里我就不注释了,因为这里也是一个注入器,和AntiDumpProtection.cs是差不多的,看不懂也没关系,看我后面分析实际实现就能明白了。
找到AntiTamper的实现AntiTamper.Normal.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| static unsafe void Initialize() { Module m = typeof(AntiTamperNormal).Module; string n = m.FullyQualifiedName; bool f = n.Length > 0 && n[0] == '<'; var b = (byte*)Marshal.GetHINSTANCE(m); byte* p = b + *(uint*)(b + 0x3c); ushort s = *(ushort*)(p + 0x6); ushort o = *(ushort*)(p + 0x14);
uint* e = null; uint l = 0; var r = (uint*)(p + 0x18 + o); uint z = (uint)Mutation.KeyI1, x = (uint)Mutation.KeyI2, c = (uint)Mutation.KeyI3, v = (uint)Mutation.KeyI4; for (int i = 0; i < s; i++) { uint g = (*r++) * (*r++); if (g == (uint)Mutation.KeyI0) { e = (uint*)(b + (f ? *(r + 3) : *(r + 1))); l = (f ? *(r + 2) : *(r + 0)) >> 2; } else if (g != 0) { var q = (uint*)(b + (f ? *(r + 3) : *(r + 1))); uint j = *(r + 2) >> 2; for (uint k = 0; k < j; k++) { uint t = (z ^ (*q++)) + x + c * v; z = x; x = c; x = v; v = t; } } r += 8; }
uint[] y = new uint[0x10], d = new uint[0x10]; for (int i = 0; i < 0x10; i++) { y[i] = v; d[i] = x; z = (x >> 5) | (x << 27); x = (c >> 3) | (c << 29); c = (v >> 7) | (v << 25); v = (z >> 11) | (z << 21); } Mutation.Crypt(y, d);
uint w = 0x40; VirtualProtect((IntPtr)e, l << 2, w, out w);
if (w == 0x40) return;
uint h = 0; for (uint i = 0; i < l; i++) { *e ^= y[h & 0xf]; y[h & 0xf] = (y[h & 0xf] ^ (*e++)) + 0x3dbb2819; h++; } }
|
上面是我注释的,实际上的解密写在了最末尾”*e ^= y[h & 0xf];”,前面一大坨代码都是计算出key和要解密数据的位置。
为什么可以解密?因为xor 2次相同的值,等于xor 0,比如123 ^ 456 ^ 456 == 123。
那么这段代码究竟解密了什么呢?
我们先了解一下元数据表的Method表
我用红框标记的RVA指向了方法体的数据,方法体里面存放了ILHeader ILCode LocalVar EH。
ConfuserEx会修改RVA,让RVA指向另一个红框”章节 #0: 乱码”,这个Section专门存放了方法体(模块静态构造器和Anti Tamper本身的方法体不在这个节里面,否则都没法运行了)。
ConfuserEx会加密这一个节的内容。因为模块静态构造器是比程序集入口点更优先执行的,所以模块静态构造器的第一条IL指令就是call void AntiTamper::Initialize()。
在程序集运行时会首先执行这一条IL指令,其它方法都会被解密,程序就可以正常的运行下去了。
这种方法比Hook JIT的兼容性好非常多,几乎不可能出现无法运行的问题。但是这种方法的强度也是远不如Hook JIT的。
AntiTamperKiller成品
刚才我们已经分析完了Anti Tamper,如果你看懂了,你也能写出一个Anti Tamper的静态脱壳机(dnSpy Dump法是有可能损坏数据的,静态脱壳仅仅解密了一个节的数据)
Anti Tamper脱壳机下载:AntiTamperKiller.7z
de4dot怎么用的这个就怎么用,支持ConfuserEx最大保护。