大內高手專欄:

To De or Not to De?

作者:蔡學鏞

2005 年 2 月

莎士比亞(Shakespeare)劇作中的哈姆雷特(Hamlet)對於該不該殺了叔父感到猶豫不決,To be or not to be?慾望城市(Sex and the City)影集中的凱莉(Carrie Bradshaw)對於男友要求 pee 在他身上的動作猶豫不決(Season 3, Episode 2),To pee or not to pee?身為程式員的我們對於該不該反編譯(decompile)別人的 .NET 與 Java 程式感到猶豫不決,To de or not to de?

.NET 與 Java 程式的諸多特質,使得程式特別容易被反編譯。本文章中,我會介紹反編譯相關的技術與議題,包括反組譯(disassembler)、反編譯器、混淆器(obfuscator)、以及法律。看完這篇文章後,To de or not to de,答案就由你自己決定了。

反組譯

.NET Framework SDK 提供了 ILDASM.exe,可以把 Managed PE 檔反組譯成 MSIL 組合語言。.NET Framework SDK 同時也提供了 ILASM.exe,可以把 MSIL 組合語言組譯(assemble)成 Managed PE 檔。你可以在 Serge Lidin 所著的《Inside Microsoft .NET IL Assembler》(Microsoft Press)一書中看到 MSIL 組合語言的定義。

我比較不喜歡使用 ILDASM,而偏好使用 RemoteSoft 推出的 .NET Explorer,如圖 1 所示,我建議讀者去下載 .NET Explorer 試用版回來使用。.NET Explorer 試用版可以反組譯,但是反編譯功能被 disable(需要花錢購買)。

圖 1
圖 1:RemoteSoft .NET Explorer 可以用來進行 .NET 反組譯。

Java 2 SDK 也提供了 javap,以為 Java Class File 的反組譯工具。但是 Sun 並未定義 Java 的組合語言,所以當然也不會提供 Java 的組譯工具。除了 javap 之外,還有許多其他的 Java 反組譯工具:

  • ClassfileToXML
  • IceBreaker
  • ClassNavigator
  • JavaDump
  • Jasmin

我自己也寫了兩套 Java 反組譯工具:

  • Vitamin J(我用 Java Swing 寫的一套 GUI-mode 反組譯工具,曾在 JavaTwo 2002 研討會上展示過)
  • Jasm4Jvm5(我用 REBOL 語言寫的一套 Console-mode 反組譯工具,支援最新的 Java Class File 格式)

為何 .NET 與 Java 的程式容易被反編譯?

簡而言之,.NET 與 Java 的程式容易被反編譯,是因為:

  1. 採用兩階段式編譯(2-phase compilation),中間碼(Intermediate Language)和源碼(Source Code)之間非常接近,所以容易反編譯
  2. Java Class File 與 .NET Managed PE 檔保留相當多符號(Symbol)資訊以及 metadata。
  3. 使用的指令 Opcode 不多,且重複性太高。Java 一直以來只有約 202 個 Opcode,.NET 1.0/1.1 只有 213 個 Opcode(.NET 2.0 估計會多出幾個)。
  4. Java 和 .NET的Opcode 都相當高階(High-Level)。
  5. 兩者都是使用 Stack-Based 的架構,使得指令之間的關係簡單,容易反向推導。
  6. 許多程式沒有使用混淆器。
  7. 現代程式的架構良好,模組切割得當。

反編譯工具

Java 的反編譯工具包括了:

  • JAD
  • JODE
  • Mocha
  • SourceAgain
  • Jive
  • WingDis
  • HomeBrew
  • JReveal
  • Decafe
  • JReverse
  • jAscii

.NET 的反編譯工具包括了:

  • Reflector for .NET
  • Anakrino
  • RemoteSoft Salamander

因應之道

如何保護你的 Java 或 .NET 程式,可能的方法包括了:

  • 法律。後面會有更詳細的說明。
  • 將程式放在 Server 上執行,不要讓公司以外的人接觸到。並非所有的程式都適合使用這種方式。
  • 混淆(obfuscation)。後面會有更詳細的說明。
  • 編譯成原生碼(Native Code),但是會因此失去跨平台的優點,且這種作法並不普遍,銷售 Java 原生譯器的廠商似乎都不再繼續此項業務。我也似乎沒有看到真正廣為使用的 .NET 原生編譯器。(RemoteSoft 的Salamander .NET Native Compiler 雖然號稱是 .NET Native Compiler,但其實是類似 Thinstall 的作法,並不是真正的 Native Compiler。)
  • 包裝並加密,但是會失去跨平台的優點。
  • 程式關鍵處使用 native code,以提高反編譯的難度,但是會失去跨平台的優點。.NET 這方便比 Java 簡單,特別是,如果使用 Managed C++,會更簡單。Java 呼叫 native code 必須透過 JNI(Java Native Interface)。我覺得『JNI 超難用!』
  • 基於反編譯很不容易阻擋,有些公司乾脆賣起源碼了,許多 Java 和 .NET 元件廠商在賣元件的同時,會有另一個版本連源碼一起賣,並允許修改。

混淆器

混淆器(Obfuscator)的目的是,將原來的執行檔經過處理之後,轉成另一個執行檔,程式依然可以執行,但是不容易被反編譯。混淆器的設計方針應該是:

  • 必須符合 VM 的規格書,以免造成程式無法執行
  • 不能讓程式執行結果有出入
  • 被混淆過的程式,效率通常會變差,但是不可以受到太大的影響
  • 讓程式盡量無法被現有的反編譯器反編譯成功
  • 即使被反編譯成功,也會造成程式不容易被程式員閱讀與修改

Java 的混淆器(obfuscator)包括了:

  • Crema(奇怪的是,當初 Borland 從 Hanpeter Van Vliet 買進 Crema 版權,後來似乎棄之不顧了。)
  • DashO,這是最多人使用的產品
  • SourceGuard
  • Zelix KlassMaster

Microsoft Visual Studio .NET 2003/2005 附上 Preemptive Dotfuscator。Borland C# Builder 附上 Wise Owl 的Demeanor。C# Builder『似乎』因為銷售狀況不佳,已經不再推出新版本了,而是被整合進 Delphi 2005 中。目前 Delphi 2005 沒有內建混淆器。

各種混淆的方法

混淆的方法包括了:

  • Scramble Identifiers:把有意義的名稱(類別名稱、變數名稱…)移除,改用沒有意義的名稱取代,如此一來,就算被反編譯成功,也不容易閱讀理解。通常的作法是讓名稱縮短,以節省檔案空間。
  • Insert dead code or irrelevant code:程式中穿插一些永遠不會被執行的程式碼,以為欺敵之計。
  • Extend loop condition:在迴圈條件中加入一些沒有實際效果的條件。
  • Reducible to non-reducible:Java 語言只能表達出一部份的 Java Bytecode 語法。C# 語言也只能表達出一部份的 MSIL。利用這個特點,將程式的 Bytecode 與 PE 改成無法被反編譯的語法。
  • Add redundant operands:加入一些冗贅的運算。例如:「int a = 10;」改成「int b = 2; int c = 5; int a = b * c;」
  • 我在清華大學碩士論文中用的方法,利用 Exception 製造出跳躍。

我認為讀者應該對這些混淆器實作上的瑣碎細節不感興趣,所以以下只列出名稱,不作說明:

  • Parallelize code
  • Inline and Outline methods
  • Interleave methods
  • Clone methods
  • Loop transformations
  • Reorder statements
  • Reorder loops
  • Reorder expressions
  • Change encoding
  • Split variables
  • Convert static to procedure data
  • Aggregation
  • Merge Scalar variables
  • Factor Class
  • Insert Bogus class
  • Refactor Class
  • Split Array
  • Merge Arrays
  • Fold Array
  • Flatten Array
  • Ordering
  • Reorder methods and Instance variables
  • Reorder Arrays

關於這些混淆方式,有一些值得注意的地方:

  • 如果你看過 Martin Fowler 的《Refactoring》一書,你可能發現上面列出的某些名稱似曾相識。基本上,refactoring 一書中所介紹的方法都可以用來混淆,只不過是『反方向使用』。
  • 字串的混淆,可以讓程式碼變小。流程的混淆,會讓程式碼變大。
  • 上面列出來的方法雖然多,但是大多數的混淆器只有使用其中一小部分的方法。
  • 許多 VM 實作上會基於某些假設,因此,混淆過的程式很有可能會造成某些 VM 當機。所以,有些人不願意使用混淆器,免得不但反編譯器被混淆而無法反編譯,連 VM 也被混淆而無法執行。
  • 打開大門,表示歡迎大家進來;鎖了門,不代表小偷就不會進門,但是不鎖門,就會增加被偷的風險。同樣的道理:開放源碼,表示歡迎大家閱讀並利用源碼;把程式混淆過,不代表程式就無法被反編譯,但是不將程式混淆過,就會增加被反編譯的風險。

To De or Not to De? That’s a Legal Question.

軟體可以利用下面的法律來保護,禁止被反編譯。

  • 專利法:申請專利來保護程式內的特殊技巧。
  • 版權法:抄襲程式(雖然作了修改)仍然會侵犯版權。
  • 授權協議:可以在軟體合約內容中加上禁止逆向工程(Reverse Engineering)條款
  • 其他法律:例如美國的 Digital Millennium Copyright Act

逆向工程(反編譯與反組譯)並非一律被禁止,有些逆向工程的動作屬於該軟體的「合理使用」範圍內。大多數的法律認定,利用反編譯來取得未公開的 API 是合法的,其他用途則屬非法。所以,如果你購買了某個 .NET 或 Java 軟體(或控件),且該軟體 / 控件沒有足夠的使用說明,你可以利用逆向工程技術來取得該軟體的操作介面(User Interface)與 API。以前相當流行挖掘 Windows OS 的 Undocumented API,這正是屬於「合理使用」的範圍。但是,不同的國家與區域對此可能有不同的規範,請特別注意。

許多銷售反編譯器的廠商,往往宣稱他們的產品可以幫助你「將遺失的源碼找回來」,他們絕對不會宣稱該產品可以幫助你用來「偷竊別人的源碼」。提供反編譯工具並不具有法律上的疑義,但是這些廠商仍然對此做出道德上的宣示,可見反編譯是一個多麼敏感的話題。我想,這或許是為什麼 Godfrey Nolan 早在 1999 或 2000 年就該出版的《Decompiling Java》一書,一直拖延到 2004 年才出版的,而出版社也從 McGraw Hill 換成了 APress。

最後,我歸納的結論是:想要反編譯別人的程式,必須考慮到法律、道德、和技術三個層面。