2022年8月30日0時至24時,哈爾濱市報告12例新冠病毒陽性感染者,其中10例確診病例,2例無癥狀感染者。根據流調結果,現將其主要活動軌跡涉及場所發布如下:
新增確診病例主要情況:
確診病例21:新冠病毒肺炎確診病例(普通型),現住香坊區公濱路307-1號。
確診病例22-23:新冠病毒肺炎確診病例(普通型),現住香坊區新松茂樾山一期。
確診病例24:新冠病毒肺炎確診病例(輕型),現住道里區鄉里街140號。
確診病例25-27:新冠病毒肺炎確診病例(輕型),現住南崗區凡爾賽詩城小區。
確診病例28:新冠病毒肺炎確診病例(普通型),現住道里區海富禧園小區。
確診病例29:新冠病毒肺炎確診病例(普通型),現住道里區美晨家園小區。
確診病例30:新冠病毒肺炎確診病例(普通型),現住道里區共樂小區。
新增無癥狀感染者主要情況:
無癥狀感染者6:新冠病毒無癥狀感染者,現住道里區新城小區。
無癥狀感染者7:新冠病毒無癥狀感染者,現住巴彥縣天增鎮慶勝村。
一、巴彥縣
8月25日-28日
8月25日07:00-11:00、13:00-17:00、18:00-20:00
8月26日07:00-11:00、13:00-17:00
8月27日07:00-10:30、18:00-21:30
8月28日10:00-10:30
巴彥縣維珍食雜店(天增鎮慶勝村徐家崗屯)
8月27日、28日
8月27日10:30-11:30
8月28日08:00-10:00、10:30-11:00
巴彥縣宴席廳(天增鎮慶勝村半步道屯)
二、哈市其他城區
8月24日
06:30 道里區新發鎮五星村
09:00-10:00 香坊區哈平路189-1號
10:00 電信加油站(香坊區哈平路81號)
8月26日
00:00-09:50 哈爾濱太平國際機場T2航站樓
06:45 網約車(黑AK03C2)
07:20網約車(黑A7C7C7)
09:00 84路公交車(民安街站至雨陽公園公交首末站)
12:29 南崗區比優特超市(博物館店)
12:40南崗區超麥蛋糕(建設街與花園街交口店)
13:09-17:18 道里區金色海洋浴館(民安小區)
14:00 84路公交車(雨陽公園公交首末站至民安街站)
15:30 元申廣電(哈藥路店)
16:41 地鐵1號線轉3號線(哈爾濱南站至工農大街站,醫大二院站換乘),129路公交車(工農大街麗江路口站至武警指揮學校站)
17:00 地鐵2號線轉3號線(工人文化宮站至勞動公園站,珠江路站換乘)
17:32-17:43 道里區耐克專賣店(凱德廣場埃德蒙頓路店)
18:00 75路公交車(經緯三道街站至民安街站)
18:00-21:30 香坊區黃牛村小鋁鍋烤肉(通鄉街與松木街交口)
18:21 南崗區奈思母嬰館(凡爾賽詩城)
20:43 南崗區可優生鮮超市(凡爾賽詩城小區)
21:34-21:57 道里區烽禾影城(群力遠大店)
8月26日、27日
08:00 115路公交車(市第六醫院站至大方里小區站)
09:00-16:00 九昱醫藥有限責任公司(道外區大方里街99號)
8月27日
08:00 84路公交車(民安街站至雨陽公園公交首末站)
09:03 南崗區二子生鮮(凡爾賽詩城小區)
10:00 南崗區盟科視界小區
10:40 杭點局點心蔥油面(紅博中央公園店)
11:30 金藝名剪燙染專業店(學院路濱才城尚品陽光H棟7號)
12:50 妙記辣鴨(濱才店)
12:59 得π超市(利民西六大街與學院路交叉口西240米)
13:00 松北區阿三生煎(融創茂店)
13:56-16:35 道里區碧水華清池(康安路108號)
14:02-17:27 哈爾濱融創樂園(松北區世茂大道)
14:49 道里區許大廚菜館(消防救援大隊民生尚都小型消防站東南側80米)
15:44-17:27 松北區嗨嗨小豬兒童成長樂園(融創茂二樓)
16:00-19:45 南崗區上和置地廣場銀座23樓(燎原街附58-2)
19:45 網約車(黑AQ6C70)
21:30 南崗區二子生鮮(凡爾賽詩城小區)
21:32 南崗區貪吃鴨貨(凡爾賽詩城小區)
對上述場所已嚴格落實了消毒和管控等措施。
哈爾濱市應對新冠肺炎疫情工作指揮部要求廣大市民,在此期間與上述新冠病毒陽性感染者有過接觸史或活動軌跡有交集、重合的人員,迅速做好個人防護,立即撥打社區或屬地疫情防控部門電話,不要外出,配合工作人員開展流調排查、核酸檢測等疫情防控措施。近期所有外地市抵(返)哈人員,抵返前需提前2天向所在屬地社區(村屯) 和單位報備,抵哈后第一時間向所在屬地社區(村屯)和單位(或所住賓館)報告。高、中風險區人員建議暫緩來哈。同時請市民關注官方權威發布,進一步提高防范意識,不聚集、不聚會、不聚餐,養成出門佩戴口罩、勤洗手、常通風、保持社交距離等文明健康生活習慣。
哈爾濱市及各區縣(市)疾控中心咨詢電話
道里區疾控中心 51669665
道外區疾控中心 57672096
南崗區疾控中心 86243577
香坊區疾控中心 55694618
平房區疾控中心 86520920
阿城區疾控中心 53722510
松北區疾控中心 88105166
呼蘭區疾控中心 57321893
雙城區疾控中心 53163181
依蘭縣疾控中心 57222573
巴彥縣疾控中心 57502636
賓 縣疾控中心 57914255
方正縣疾控中心 57112343
木蘭縣疾控中心 57083497
尚志市疾控中心 53324184
通河縣疾控中心 57435030
五常市疾控中心 55802434
延壽縣疾控中心 53067333
哈爾濱市疾控中心 51012320
哈爾濱市疾病預防控制中心
2022年8月31日
源:海口發布
海口市新型冠狀病毒肺炎疫情防控工作指揮部關于新增確診病例的通報
2022年11月8日0-16時,我市新增1例確診病例,在外省來瓊重點人群篩查中發現。11月7日以來,我市累計報告確診病例2例。
一、基本情況
病例2 居住于海樂花園4棟,11月5日21時由長沙乘坐AQ1148航班抵達海口美蘭機場,落地檢結果為陰性。
二、活動軌跡
11月5日
21:00-21:30 海口美蘭機場;
21:30-22:00 乘出租車返回住處。
11月6日
11:30-12:30 金墾路21號水箱王維修店;
14:00-19:00 金墾路21號水箱王維修店;
19:00-19:20 坡博農貿市場。
11月7日
11:30-12:30 金墾路21號水箱王維修店;
14:00-16:30 金墾路21號水箱王維修店;
16:35-17:00 金宇街道大規模采樣點;
17:00-20:17 金墾路21號水箱王維修店;
20:27-20:57 海墾路81號八一夜市。
請在上述時間段、地點與該人員有接觸或有軌跡交集的人員第一時間主動向單位、社區(村委會)或所住宿賓館報告,配合做好流調及相關健康管理措施,如瞞報、遲報、謊報將依法追究相關責任。
目前我市已迅速組織流調排查、場所管控、消毒、人員轉運等工作,并已對相關人員實施管控。請廣大市民不信謠、不傳謠,涉疫消息以官方發布為準,繼續增強防范意識,科學佩戴口罩、勤洗手、常通風、保持安全社交距離、不扎堆、不聚集,配合落實掃地點碼工作,盡早全程接種新冠病毒疫苗。
海口市新型冠狀病毒肺炎
疫情防控工作指揮部
2022年11月8日
文預警!!!
UWP 程序有 .NET Native 可以將程序集編譯為本機代碼,逆向的難度會大很多;而基于 .NET Framework 和 .NET Core 的程序卻沒有 .NET Native 的支持。雖然有 Ngen.exe 可以編譯為本機代碼,但那只是在用戶計算機上編譯完后放入了緩存中,而不是在開發者端編譯。
于是有很多款混淆工具來幫助混淆基于 .NET 的程序集,使其稍微難以逆向。本文介紹 Smart Assembly 各項混淆參數的作用以及其實際對程序集的影響。
本文不會講 SmartAssembly 的用法,因為你只需打開它就能明白其基本的使用。
感興趣可以先下載:.NET Obfuscator, Error Reporting, DLL Merging - SmartAssembly。
我們先需要準備程序集來進行混淆試驗。這里,我使用 Whitman 來試驗。它在 GitHub 上開源,并且有兩個程序集可以試驗它們之間的相互影響。
額外想吐槽一下,SmartAssembly 的公司 Red Gate 一定不喜歡這款軟件,因為界面做成下面這樣竟然還長期不更新:
而且,如果要成功編譯,還得用上同為 Red Gate 家出品的 SQL Server,如果不裝,軟件到處彈窗報錯。只是報告錯誤而已,干嘛還要開發者裝一個那么重量級的 SQL Server 啊!詳見:Why is SQL Server required — Redgate forums。
SmartAssembly 本質上是保護應用程序不被逆向或惡意篡改。目前我使用的版本是 6,它提供了對 .NET Framework 程序的多種保護方式:
以上所有 SmartAssembly 對程序集的修改中,我標為 粗體 的是真的在做混淆,而標為 斜體 的是一些輔助功能。
后面我只會說明其混淆功能。
我故意在 Whitman.Core 中寫了一個沒有被用到的 internal 類 UnusedClass,如果我們開啟了裁剪,那么這個類將消失。
▲ 沒用到的類將消失
特別注意,如果標記了 InternalsVisibleTo,尤其注意不要不小心被誤刪了。
名稱混淆中,類名和方法名的混淆有三個不同級別:
需要注意:對于部分程序集,類與方法名(NameMangling)的等級只能選為 3,否則混淆程序會無法完成編譯。
字段名的混淆有三個不同級別:
需要注意:對于部分程序集,字段名(FieldsNameMangling)的等級只能選為 2 或 3,否則混淆程序會無法完成編譯。
實際試驗中,以上各種組合經常會出現無法編譯的情況。
下面是 Whitman 中 RandomIdentifier 類中的部分字段在混淆后的效果:
// Token: 0x04000001 RID: 1 [CompilerGenerated] [DebuggerBrowsable(DebuggerBrowsableState.Never)] private int \u0001; // Token: 0x04000002 RID: 2 private readonly Random \u0001=new Random(); // Token: 0x04000003 RID: 3 private static readonly Dictionary<int, int> \u0001=new Dictionary<int, int>(); 1 2 3 4 5 6 7 8 9 10
這部分的原始代碼可以在 冷算法:自動生成代碼標識符(類名、方法名、變量名) 找到。
如果你需要在混淆時使用名稱混淆,你只需要在以上兩者的組合中找到一個能夠編譯通過的組合即可,不需要特別在意等級 1~3 的區別,因為實際上都做了混淆,1~3 的差異對逆向來說難度差異非常小的。
需要 特別小心如果有 InternalsVisibleTo 或者依據名稱的反射調用,這種混淆下極有可能掛掉!!!請充分測試你的軟件,切記!!!
如果開啟了 ChangeMethodParent,那么混淆可能會將一個類中的方法轉移到另一個類中,這使得逆向時對類型含義的解讀更加匪夷所思。
如果你的程序集中確實存在需要被按照名稱反射調用的類型,或者有 internal 的類/方法需要被友元程序集調用,請排除這些命名空間。
列舉我在 Whitman.Core 中的方法:
public string Generate(bool pascal) { var builder=new StringBuilder(); var wordCount=WordCount <=0 ? 4 - (int) Math.Sqrt(_random.Next(0, 9)) : WordCount; for (var i=0; i < wordCount; i++) { var syllableCount=4 - (int) Math.Sqrt(_random.Next(0, 16)); syllableCount=SyllableMapping[syllableCount]; for (var j=0; j < syllableCount; j++) { var consonant=Consonants[_random.Next(Consonants.Count)]; var vowel=Vowels[_random.Next(Vowels.Count)]; if ((pascal || i !=0) && j==0) { consonant=CultureInfo.CurrentCulture.TextInfo.ToTitleCase(consonant); } builder.Append(consonant); builder.Append(vowel); } } return builder.ToString(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
▲ 這個方法可以在 冷算法:自動生成代碼標識符(類名、方法名、變量名) 找到。
流程混淆修改方法內部的實現。為了了解各種不同的流程混淆級別對代碼的影響,我為每一個混淆級別都進行反編譯查看。
▲ 沒有混淆
▲ 0 級流程混淆
▲ 1 級流程混淆
可以發現 0 和 1 其實完全一樣。又被 SmartAssembly 耍了。
2 級流程混淆代碼很長,所以我沒有貼圖:
// Token: 0x06000004 RID: 4 RVA: 0x00002070 File Offset: 0x00000270 public string Generate(bool pascal) { StringBuilder stringBuilder=new StringBuilder(); StringBuilder stringBuilder2; if (-1 !=0) { stringBuilder2=stringBuilder; } int num2; int num=num2=this.WordCount; int num4; int num3=num4=0; int num6; int num8; if (num3==0) { int num5=(num <=num3) ? (4 - (int)Math.Sqrt((double)this._random.Next(0, 9))) : this.WordCount; if (true) { num6=num5; } int num7=0; if (!false) { num8=num7; } if (false) { goto IL_10E; } if (7 !=0) { goto IL_134; } goto IL_8E; } IL_6C: int num9=num2 - num4; int num10; if (!false) { num10=num9; } int num11=RandomIdentifier.SyllableMapping[num10]; if (6 !=0) { num10=num11; } IL_86: int num12=0; int num13; if (!false) { num13=num12; } IL_8E: goto IL_11E; IL_10E: string value; stringBuilder2.Append(value); num13++; IL_11E: string text; bool flag; if (!false) { if (num13 >=num10) { num8++; goto IL_134; } text=RandomIdentifier.Consonants[this._random.Next(RandomIdentifier.Consonants.Count)]; value=RandomIdentifier.Vowels[this._random.Next(RandomIdentifier.Vowels.Count)]; flag=((pascal || num8 !=0) && num13==0); } if (flag) { text=CultureInfo.CurrentCulture.TextInfo.ToTitleCase(text); } if (!false) { stringBuilder2.Append(text); goto IL_10E; } goto IL_86; IL_134: if (num8 >=num6) { return stringBuilder2.ToString(); } num2=4; num4=(int)Math.Sqrt((double)this._random.Next(0, 16)); goto IL_6C; } 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
▲ 2 級流程混淆
這時就發現代碼的可讀性降低了,需要耐心才能解讀其含義。
以下是 3 級流程混淆:
// Token: 0x06000004 RID: 4 RVA: 0x0000207C File Offset: 0x0000027C public string Generate(bool pascal) { StringBuilder stringBuilder=new StringBuilder(); int num2; int num=num2=this.WordCount; int num4; int num3=num4=0; int num7; int num8; string result; if (num3==0) { int num5; if (num > num3) { num5=this.WordCount; } else { int num6=num5=4; if (num6 !=0) { num5=num6 - (int)Math.Sqrt((double)this._random.Next(0, 9)); } } num7=num5; num8=0; if (false) { goto IL_104; } if (7==0) { goto IL_84; } if (!false) { goto IL_12A; } return result; } IL_73: int num9=num2 - num4; num9=RandomIdentifier.SyllableMapping[num9]; IL_81: int num10=0; IL_84: goto IL_114; IL_104: string value; stringBuilder.Append(value); num10++; IL_114: string text; bool flag; if (!false) { if (num10 >=num9) { num8++; goto IL_12A; } text=RandomIdentifier.Consonants[this._random.Next(RandomIdentifier.Consonants.Count)]; value=RandomIdentifier.Vowels[this._random.Next(RandomIdentifier.Vowels.Count)]; flag=((pascal || num8 !=0) && num10==0); } if (flag) { text=CultureInfo.CurrentCulture.TextInfo.ToTitleCase(text); } if (!false) { stringBuilder.Append(text); goto IL_104; } goto IL_81; IL_12A: if (num8 < num7) { num2=4; num4=(int)Math.Sqrt((double)this._random.Next(0, 16)); goto IL_73; } result=stringBuilder.ToString(); return result; } 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
▲ 3 級流程混淆
3 級流程混淆并沒有比 2 級高多少,可讀性差不多。不過需要注意的是,這些差異并不是隨機差異,因為重復生成得到的流程結果是相同的。
以下是 4 級流程混淆:
// Token: 0x06000004 RID: 4 RVA: 0x0000207C File Offset: 0x0000027C public unsafe string Generate(bool pascal) { void* ptr=stackalloc byte[14]; StringBuilder stringBuilder=new StringBuilder(); StringBuilder stringBuilder2; if (!false) { stringBuilder2=stringBuilder; } int num=(this.WordCount <=0) ? (4 - (int)Math.Sqrt((double)this._random.Next(0, 9))) : this.WordCount; *(int*)ptr=0; for (;;) { ((byte*)ptr)[13]=((*(int*)ptr < num) ? 1 : 0); if (*(sbyte*)((byte*)ptr + 13)==0) { break; } *(int*)((byte*)ptr + 4)=4 - (int)Math.Sqrt((double)this._random.Next(0, 16)); *(int*)((byte*)ptr + 4)=RandomIdentifier.SyllableMapping[*(int*)((byte*)ptr + 4)]; *(int*)((byte*)ptr + 8)=0; for (;;) { ((byte*)ptr)[12]=((*(int*)((byte*)ptr + 8) < *(int*)((byte*)ptr + 4)) ? 1 : 0); if (*(sbyte*)((byte*)ptr + 12)==0) { break; } string text=RandomIdentifier.Consonants[this._random.Next(RandomIdentifier.Consonants.Count)]; string value=RandomIdentifier.Vowels[this._random.Next(RandomIdentifier.Vowels.Count)]; bool flag=(pascal || *(int*)ptr !=0) && *(int*)((byte*)ptr + 8)==0; if (flag) { text=CultureInfo.CurrentCulture.TextInfo.ToTitleCase(text); } stringBuilder2.Append(text); stringBuilder2.Append(value); *(int*)((byte*)ptr + 8)=*(int*)((byte*)ptr + 8) + 1; } *(int*)ptr=*(int*)ptr + 1; } return stringBuilder2.ToString(); } 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
▲ 4 級流程混淆
我們發現,4 級已經開始使用沒有含義的指針來轉換我們的內部實現了。這時除了外部調用以外,代碼基本已無法解讀其含義了。
還是以上一節中我們 Generate 方法作為示例,在開啟了動態代理之后(僅開啟動態代理,其他都關掉),方法變成了下面這樣:
// Token: 0x06000004 RID: 4 RVA: 0x0000206C File Offset: 0x0000026C public string Generate(bool pascal) { StringBuilder stringBuilder=new StringBuilder(); int num=(this.WordCount <=0) ? (4 - (int)\u0002.\u0002((double)\u0001.~\u0001(this._random, 0, 9))) : this.WordCount; for (int i=0; i < num; i++) { int num2=4 - (int)\u0002.\u0002((double)\u0001.~\u0001(this._random, 0, 16)); num2=RandomIdentifier.SyllableMapping[num2]; for (int j=0; j < num2; j++) { string text=RandomIdentifier.Consonants[\u0003.~\u0003(this._random, RandomIdentifier.Consonants.Count)]; string text2=RandomIdentifier.Vowels[\u0003.~\u0003(this._random, RandomIdentifier.Vowels.Count)]; bool flag=(pascal || i !=0) && j==0; if (flag) { text=\u0006.~\u0006(\u0005.~\u0005(\u0004.\u0004()), text); } \u0007.~\u0007(stringBuilder, text); \u0007.~\u0007(stringBuilder, text2); } } return \u0008.~\u0008(stringBuilder); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
▲ 動態代理
注意到 _random.Next(0, 9) 變成了 \u0001.~\u0001(this._random, 0, 9),Math.Sqrt(num) 變成了 \u0002.\u0002(num)。
也就是說,一些常規方法的調用被替換成了一個代理類的調用。那么代理類在哪里呢?
▲ 生成的代理類
生成的代理類都在根命名空間下。比如剛剛的 \u0001.~\u0001 調用,就是下面這個代理類:
// Token: 0x0200001A RID: 26 internal sealed class \u0001 : MulticastDelegate { // Token: 0x06000030 RID: 48 public extern \u0001(object, IntPtr); // Token: 0x06000031 RID: 49 public extern int Invoke(object, int, int); // Token: 0x06000032 RID: 50 RVA: 0x000030A8 File Offset: 0x000012A8 static \u0001() { MemberRefsProxy.CreateMemberRefsDelegates(25); } // Token: 0x04000016 RID: 22 internal static \u0001 \u0001; // Token: 0x04000017 RID: 23 internal static \u0001 ~\u0001; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
字符串編碼將程序集中的字符串都統一收集起來,存為一個資源;然后提供一個輔助類統一獲取這些字符串。
比如 Whitman.Core 中的字符串現在被統一收集了:
▲ 統一收集的字符串和解密輔助類
在我的項目中,統一收集的字符串可以形成下面這份字符串(也即是上圖中 Resources 文件夾中的那個文件內容):
cQ==dw==cg==dA==eQ==cA==cw==ZA==Zg==Zw==aA==ag==aw==bA==eg==eA==Yw==dg==Yg==bg==bQ==dHI=ZHI=Y2g=d2g=c3Q=YQ==ZQ==aQ==bw==dQ==YXI=YXM=YWk=YWlyYXk=YWw=YWxsYXc=ZWU=ZWE=ZWFyZW0=ZXI=ZWw=ZXJlaXM=aXI=b3U=b3I=b28=b3c=dXI=MjAxOC0wOC0yNlQxODoxMDo0Mw==`VGhpcyBhc3NlbWJseSBoYXMgY mVlbiBidWlsdCB3aXRoIFNtYXJ0QXNzZW1ibHkgezB9LCB3aGljaCBoYXMgZXhwaXJlZC4=RXZhbHVh dGlvbiBWZXJzaW9uxVGhpcyBhc3NlbWJseSBoYXMgYmVlbiBidWlsdCB3aXRoIFNtYXJ0QXNzZW1ibHk gezB9LCBhbmQgdGhlcmVmb3JlIGNhbm5vdCBiZSBkaXN0cmlidXRlZC4=IA==Ni4xMi41Ljc5OQ==U21hcnRBc3NlbWJseQ==UGF0aA==U29mdHdhcmVcUmVkIEdhdGVc(U29mdHdhcmVcV293NjQzMk5vZ GVcUmVkIEdhdGVc 1 2 3 4 5 6 7 8 9
雖然字符串難以讀懂,但其實我原本就是這么寫的;給你看看我的原始代碼就知道了(來自 冷算法:自動生成代碼標識符(類名、方法名、變量名)):
private static readonly List<string> Consonants=new List<string> { "q","w","r","t","y","p","s","d","f","g","h","j","k","l","z","x","c","v","b","n","m", "w","r","t","p","s","d","f","g","h","j","k","l","c","b","n","m", "r","t","p","s","d","h","j","k","l","c","b","n","m", "r","t","s","j","c","n","m", "tr","dr","ch","wh","st", "s","s" }; 1 2 3 4 5 6 7 8 9
生成的字符串獲取輔助類就像下面這樣不太容易讀懂:
// Token: 0x0200000A RID: 10 public class Strings { // Token: 0x0600001C RID: 28 RVA: 0x00002B94 File Offset: 0x00000D94 public static string Get(int stringID) { stringID -=Strings.offset; if (Strings.cacheStrings) { object obj=Strings.hashtableLock; lock (obj) { string text; Strings.hashtable.TryGetValue(stringID, out text); if (text !=null) { return text; } } } int index=stringID; int num=(int)Strings.bytes[index++]; int num2; if ((num & 128)==0) { num2=num; if (num2==0) { return string.Empty; } } else if ((num & 64)==0) { num2=((num & 63) << 8) + (int)Strings.bytes[index++]; } else { num2=((num & 31) << 24) + ((int)Strings.bytes[index++] << 16) + ((int)Strings.bytes[index++] << 8) + (int)Strings.bytes[index++]; } string result; try { byte[] array=Convert.FromBase64String(Encoding.UTF8.GetString(Strings.bytes, index, num2)); string text2=string.Intern(Encoding.UTF8.GetString(array, 0, array.Length)); if (Strings.cacheStrings) { try { object obj=Strings.hashtableLock; lock (obj) { Strings.hashtable.Add(stringID, text2); } } catch { } } result=text2; } catch { result=null; } return result; } // Token: 0x0600001D RID: 29 RVA: 0x00002CF4 File Offset: 0x00000EF4 static Strings() { if (Strings.MustUseCache=="1") { Strings.cacheStrings=true; Strings.hashtable=new Dictionary<int, string>(); } Strings.offset=Convert.ToInt32(Strings.OffsetValue); using (Stream manifestResourceStream=Assembly.GetExecutingAssembly().GetManifestResourceStream("{f6b5a51a-b2fb-4143-af01-e2295062799f}")) { int num=Convert.ToInt32(manifestResourceStream.Length); Strings.bytes=new byte[num]; manifestResourceStream.Read(Strings.bytes, 0, num); manifestResourceStream.Close(); } } // Token: 0x0400000C RID: 12 private static readonly string MustUseCache="0"; // Token: 0x0400000D RID: 13 private static readonly string OffsetValue="203"; // Token: 0x0400000E RID: 14 private static readonly byte[] bytes=null; // Token: 0x0400000F RID: 15 private static readonly Dictionary<int, string> hashtable; // Token: 0x04000010 RID: 16 private static readonly object hashtableLock=new object(); // Token: 0x04000011 RID: 17 private static readonly bool cacheStrings=false; // Token: 0x04000012 RID: 18 private static readonly int offset=0; } 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
生成字符串獲取輔助類后,原本寫著字符串的地方就會被替換為 Strings.Get(int) 方法的調用。
前面那份統一收集的字符串依然還是明文存儲為資源,但還可以進行壓縮。這時,Resources 中的那份字符串資源現在是二進制文件(截取前 256 字節):
00000000: 7b7a 7d02 efbf bdef bfbd 4def bfbd efbf 00000010: bd7e 6416 efbf bd6a efbf bd22 efbf bd08 00000020: efbf bdef bfbd 4c42 7138 72ef bfbd efbf 00000030: bd54 1337 efbf bd0e 22ef bfbd 69ef bfbd 00000040: 613d efbf bd6e efbf bd35 efbf bd0a efbf 00000050: bd33 6043 efbf bd26 59ef bfbd 5471 efbf 00000060: bdef bfbd 2cef bfbd 18ef bfbd 6def bfbd 00000070: efbf bdef bfbd 64ef bfbd c9af efbf bdef 00000080: bfbd efbf bd4b efbf bdef bfbd 66ef bfbd 00000090: 1e70 efbf bdef bfbd ce91 71ef bfbd 1d5e 000000a0: 1863 efbf bd16 0473 25ef bfbd 2204 efbf 000000b0: bdef bfbd 11ef bfbd 4fef bfbd 265a 375f 000000c0: 7bef bfbd 19ef bfbd d5bd efbf bdef bfbd 000000d0: efbf bd70 71ef bfbd efbf bd05 c789 efbf 000000e0: bd51 eaae beef bfbd ee97 adef bfbd 0a33 000000f0: d986 141c 2bef bfbd efbf bdef bfbd 1fef 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
這份壓縮的字符串在程序啟動的時候會進行一次解壓,隨后就直接讀取解壓后的字符串了。所以會占用啟動時間(雖然不長),但不會占用太多運行時時間。
為了能夠解壓出這些壓縮的字符串,Strings 類相比于之前會在讀取后進行一次解壓縮(解密)。可以看下面我額外標注出的 Strings 類新增的一行。
using (Stream manifestResourceStream=Assembly.GetExecutingAssembly().GetManifestResourceStream("{4f639d09-ce0f-4092-b0c7-b56c205d48fd}")) { int num=Convert.ToInt32(manifestResourceStream.Length); byte[] buffer=new byte[num]; manifestResourceStream.Read(buffer, 0, num); ++ Strings.bytes=SimpleZip.Unzip(buffer); manifestResourceStream.Close(); } 1 2 3 4 5 6 7 8
至于嵌入其中的解壓與解密類 SimpleZip,我就不能貼出來了,因為反編譯出來有 3000+ 行:
與其他的緩存策略一樣,每次獲取字符串都太消耗計算資源的話,就可以拿內存空間進行緩存。
在實際混淆中,我發現無論我是否開啟了字符串緩存,實際 Strings.Get 方法都會緩存字符串。你可以回到上面去重新閱讀 Strings.Get 方法的代碼,發現其本來就已帶緩存。這可能是 SmartAssembly 的 Bug。
之前的混淆都會在原來有字符串地方使用 Strings.Get 來獲取字符串。而如果開啟了這一選項,那么 Strings.Get 就不是全局調用的了,而是在類的內部調用一個委托字段。
比如從 Strings.Get 調用修改為 \u0010(),,而 \u0010 是我們自己的類 RandomIdentifier 內部的被額外加進去的一個字段 internal static GetString \u0010;。
這其實是個沒啥用的選項,因為我們程序集只會多出一個全局的特性:
[assembly: SuppressIldasm] 1
只有 MSIL Disassembler 和基于 MSIL Disassembler 的工具認這個特性。真正想逆向程序集的,根本不會在乎 MSIL Disassembler 被禁掉。
dnSpy 和 dotPeek 實際上都忽略了這個特性,依然能毫無障礙地反編譯。
dnSpy 可以做挺多事兒的,比如:
在 OtherOptimizations 選項中,有一項 SealClasses 可以將所有可以密封的類進行密封(當然,此操作不會修改 API)。
在上面的例子中,由于 RandomIdentifier 是公有類,可能被繼承,所以只有預先寫的內部的 UnusedClass 被其標記為密封了。
// Token: 0x02000003 RID: 3 internal sealed class UnusedClass { // Token: 0x06000007 RID: 7 RVA: 0x000026D0 File Offset: 0x000008D0 internal void Run() { } // Token: 0x06000008 RID: 8 RVA: 0x000026D4 File Offset: 0x000008D4 internal async Task RunAsync() { } } 1 2 3 4 5 6 7 8 9 10 11 12 13
既然你希望選擇“混淆”,那么你肯定是希望能進行最大程度的保護。在保證你沒有額外產生 Bug,性能沒有明顯損失的情況下,能混淆得多厲害就混淆得多厲害。
基于這一原則,我推薦的混淆方案有(按推薦順序排序):
以上四種混淆方式從四個不同的維度對你類與方法的實現進行了混淆,使得你寫的類的任何地方都變得無法辨認。流程混淆修改方法內實現的邏輯,名稱混淆修改類/屬性/方法的名稱,動態代理將方法內對其他方法的調用變得不再直接,字符串壓縮加密將使得字符串不再具有可讀的含義。對逆向閱讀影響最大的就是以上 4 種混淆了,如果可能,建議都選擇開啟。
如果你的程序中有需要保護的“嵌入的資源”,在沒有自己的保護手段的情況下,可以使用“資源壓縮加密”。不過,我更加推薦你自己進行加密。
至于 SmartAssembly 推薦的其他選項,都是噱頭重于實際效果:
SmartAssembly 的官方文檔寫得還是太簡單了,很難得到每一個設置項的含義和實際效果。
以上這些信息的得出,離不開 dnSpy 的反編譯。