- 積分
- 34
注冊時間2018-7-21
閱讀權限10
最后登錄1970-1-1
周游歷練

TA的每日心情 | 開心 2018-9-21 22:58 |
---|
簽到天數: 2 天 [LV.1]初來乍到
|
# [.NET]詳解ConfuserEx的Anti Tamper與Anti Dump by Wwh
許多人都知道利用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。
![]()
[C#] 純文本查看 復制代碼 protected override void Execute(ConfuserContext context, ProtectionParameters parameters) {
TypeDef rtType = context.Registry.GetService<IRuntimeService>().GetRuntimeType("Confuser.Runtime.AntiDump");
// 獲取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);
// 將Confuser.Runtime.AntiDump類注入到目標程序集,返回目標程序集中的所有IDnlibDef
MethodDef cctor = module.GlobalType.FindStaticConstructor();
// 找到<Module>::.cctor
var init = (MethodDef)members.Single(method => method.Name == "Initialize");
cctor.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Call, init));
// 插入call void Confuser.Runtime.AntiDump::Initialize()這條IL指令
foreach (IDnlibDef member in members)
name.MarkHelper(member, marker, (Protection)Parent);
// 將這些IDnlibDef標記為需要重命名的
}
}
AntiDumpProtection做的只是注入,所以我們轉到Confuser.Runtime中的AntiDump.cs
![]()
[C#] 純文本查看 復制代碼 static unsafe void Initialize() {
uint old;
Module module = typeof(AntiDump).Module;
var bas = (byte*)Marshal.GetHINSTANCE(module);
byte* ptr = bas + 0x3c;
// 存放NT頭偏移的地址
byte* ptr2;
ptr = ptr2 = bas + *(uint*)ptr;
// ptr指向NT頭
ptr += 0x6;
// ptr指向文件頭的NumberOfSections
ushort sectNum = *(ushort*)ptr;
// 獲取節的數量
ptr += 14;
// ptr指向文件頭的SizeOfOptionalHeader
ushort optSize = *(ushort*)ptr;
// 獲取可選頭的大小
ptr = ptr2 = ptr + 0x4 + optSize;
// ptr指向第一個節頭
byte* @new = stackalloc byte[11];
if (module.FullyQualifiedName[0] != '<') //Mapped
{
// 這里判斷是否為內存加載的模塊(dnSpy里面顯示InMemory的),比如Assembly.Load(byte[] rawAssembly)
// 如果是內存加載的模塊,module.FullyQualifiedName[0]會返回"<未知>"
//VirtualProtect(ptr - 16, 8, 0x40, out old);
//*(uint*)(ptr - 12) = 0;
byte* mdDir = bas + *(uint*)(ptr - 16);
// ptr指向IMAGE_COR20_HEADER
//*(uint*)(ptr - 16) = 0;
if (*(uint*)(ptr - 0x78) != 0) {
// 如果導入表RVA不為0
byte* importDir = bas + *(uint*)(ptr - 0x78);
byte* oftMod = bas + *(uint*)importDir;
// OriginalFirstThunk
byte* modName = bas + *(uint*)(importDir + 12);
// 導入DLL的名稱
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;
// ntdll.dll
for (int i = 0; i < 11; i++)
*(modName + i) = *(@new + i);
// 把mscoree.dll改成ntdll.dll
VirtualProtect(funcName, 11, 0x40, out old);
*(uint*)@new = 0x6f43744e;
*((uint*)@new + 1) = 0x6e69746e;
*((ushort*)@new + 4) = 0x6575;
*(@new + 10) = 0;
// NtContinue
for (int i = 0; i < 11; i++)
*(funcName + i) = *(@new + i);
// 把_CorExeMain改成NtContinue
}
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);
// mdHdr指向STORAGESIGNATURE(開頭是BSJ**B的那個)
*(uint*)mdDir = 0;
*((uint*)mdDir + 1) = 0;
*((uint*)mdDir + 2) = 0;
*((uint*)mdDir + 3) = 0;
// 將IMAGE_COR20_HEADER的cb MajorRuntimeVersion MinorRuntimeVersion MetaData清零
VirtualProtect(mdHdr, 4, 0x40, out old);
*(uint*)mdHdr = 0;
// 刪除BSJ**B標志,這樣就無法搜索到STORAGESIGNATURE了
mdHdr += 12;
// mdHdr指向iVersionString
mdHdr += *(uint*)mdHdr;
mdHdr = (byte*)(((ulong)mdHdr + 7) & ~3UL);
mdHdr += 2;
// mdHdr指向STORAGEHEADER的iStreams
ushort numOfStream = *mdHdr;
// 獲取元數據流的數量
mdHdr += 2;
// mdHdr指向第一個元數據流頭
for (int i = 0; i < numOfStream; i++) {
VirtualProtect(mdHdr, 8, 0x40, out old);
//*(uint*)mdHdr = 0;
mdHdr += 4;
// mdHdr指向STORAGESTREAM.iSize
//*(uint*)mdHdr = 0;
mdHdr += 4;
// mdHdr指向STORAGESTREAM.rcName
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++;
}
// 清零STORAGESTREAM.rcName,因為這個是4字節對齊的,所以代碼長一些
}
}
else //Flat
{
// 這里就是內存加載程序集的情況了,和上面是差不多的,我就不再具體分析了
//VirtualProtect(ptr - 16, 8, 0x40, out old);
//*(uint*)(ptr - 12) = 0;
uint mdDir = *(uint*)(ptr - 16);
//*(uint*)(ptr - 16) = 0;
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);
//*(uint*)mdHdrPtr = 0;
mdHdrPtr += 4;
//*(uint*)mdHdrPtr = 0;
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會刪除BSJ**B標志,這樣就無法搜索到STORAGESIGNATURE了。還有元數據流頭的rcName字段,一并清零,這樣也會讓我們無法定位到元數據結構體,但是CLR不再需要這些了。
解決這個的辦法很簡單,把<Module>::.cctor()的call void Confuser.Runtime.AntiDump::Initialize()這條指令nop掉。我們要如何定位到這條指令呢?
這里有個投機取巧的辦法,解決Anti Tamper之后,在dnSpy里面找出現了
[C#] 純文本查看 復制代碼 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
![]()
[C#] 純文本查看 復制代碼 static unsafe void Initialize() {
Module m = typeof(AntiTamperNormal).Module;
string n = m.FullyQualifiedName;
bool f = n.Length > 0 && n[0] == '<';
// f為true代表這是內存加載的程序集
var b = (byte*)Marshal.GetHINSTANCE(m);
byte* p = b + *(uint*)(b + 0x3c);
// pNtHeader
ushort s = *(ushort*)(p + 0x6);
// Machine
ushort o = *(ushort*)(p + 0x14);
// SizeOfOptHdr
uint* e = null;
uint l = 0;
var r = (uint*)(p + 0x18 + o);
// pFirstSectHdr
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++);
// SectionHeader.Name => nameHash
// 此時r指向SectionHeader.VirtualSize
if (g == (uint)Mutation.KeyI0) {
// 查看Confuser.Protections.AntiTamper.NormalMode
// 這里的Mutation.KeyI0是nameHash
// 這個if的意思是判斷是否為ConfuserEx用來存放加密后方法體的節
e = (uint*)(b + (f ? *(r + 3) : *(r + 1)));
// f為true,e指向RawAddres指向的內容,反之指向VirtualAddress指向的內容
l = (f ? *(r + 2) : *(r + 0)) >> 2;
// f為true,l等于RawSize >> 2,反之等于VirtualSize >> 2
// 不用關心為什么>> 2了,這個到了后面還會<< 2回去
}
else if (g != 0) {
var q = (uint*)(b + (f ? *(r + 3) : *(r + 1)));
// f為true,q指向RawAddres指向的內容,反之指向VirtualAddress指向的內容
uint j = *(r + 2) >> 2;
// l等于VirtualSize >> 2
for (uint k = 0; k < j; k++) {
// 比如VirtualSize=0x200,那這里就循環0x20次
uint t = (z ^ (*q++)) + x + c * v;
z = x;
x = c;
x = v;
v = t;
// 加密運算本身,不需要做分析
}
}
r += 8;
// 讓下一次循環時r依然指向SectionHeader的開頭
}
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);
// 這里會ConfuserEx替換成真正的加密算法,大概是這樣:
// data[0] = data[0] ^ key[0];
// data[1] = data[1] * key[1];
// data[2] = data[2] + key[2];
// data[3] = data[3] ^ key[3];
// data[4] = data[4] * key[4];
// data[5] = data[5] + key[5];
// 然后這樣循環下去
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的,尤其是那種用一個非托管DLL來Hook JIT,還給非托管DLL加個vmp殼的(說的哪幾個殼應該都清楚)。
### AntiTamperKiller成品
剛才我們已經分析完了Anti Tamper,如果你看懂了,你也能寫出一個Anti Tamper的靜態脫殼機(dnSpy Dump法是有可能損壞數據的,靜態脫殼僅僅解密了一個節的數據)
Anti Tamper脫殼機下載:
鏈接: [https://pan.baidu.com/s/1IMWk7BywjVX1O2AsJ2qIrA](https://pan.baidu.com/s/1IMWk7BywjVX1O2AsJ2qIrA)密碼: 9ywx
de4dot怎么用的這個就怎么用,支持ConfuserEx最大保護。
|
評分
-
查看全部評分
|