静的分析→脆弱性→利益

PVS-Studioに関する記事では、静的分析を使用して発見できる脆弱性とセキュリティの欠陥についての話題が増えています。 これらの記事の著者は、(私を含めて )すべての間違いがセキュリティ上の欠陥ではないと批判しています。 ただし、静的アナライザーのメッセージから、見つかった問題の動作に至るまで、あらゆる種類のメリットを得ることができるかどうか、興味深い疑問が生じます。 私の場合、利益はまだ理論的なままでしたが、プロジェクトコードを実際に掘り下げることなくエラーを悪用することができました。







Javaクラスの難読化ツールを開発していると想像してください。 あなたのビジネスは、.classファイルからソースコードを抽出することを難しくしています。これには、市場で入手可能な逆コンパイラの使用も含まれます。 標準の難読化手法に加えて、有名な逆コンパイラのバグを探して悪用することは非常に合理的です。 人気のある逆コンパイラが生成したコードでクラッシュするだけであれば、顧客は非常に満足しています。







人気のある逆コンパイラの1つは、IntelliJ IDEAの一部であるJetBrains Fernflowerです。 JetBrainsはそれを個別に配布することを本当に気にしませんが IntelliJ Community Edition リポジトリからダウンロードすることでソースからビルドできます 。 非公式のミラーを作成するのはさらに簡単です。IDEA全体を使い果たす必要はありません。 最近のコミットd706718取り上げます。 antを実行することでFernflowerによって組み立てられ、外部依存関係を必要とせず、コマンドラインアプリケーションとして使用できるfernflower.jarを生成します。







$ java -jar fernflower.jar Usage: java -jar fernflower.jar [-<option>=<value>]* [<source>]+ <destination> Example: java -jar fernflower.jar -dgs=true c:\my\source\ c:\my.jar d:\decompiled\
      
      





新たな改善の後、IDEA静的アナライザーはより賢くなり、 ConverterHelper :: getNextClassNameメソッドで警告を発行し始めました。







 int index = 0; while (Character.isDigit(shortName.charAt(index))) { index++; } if (index == 0 || index == shortName.length()) { // <<== return "class_" + (classCounter++); } else { ... }
      
      





警告は次のように聞こえます。







条件「index == shortName.length()」に達すると、常に「false」になります

常に真または常に偽の状態に関するこのような警告は非常に興味深いものです。 多くの場合、それらはこの状態ではなく、上記の他の場所のバグを示しています。 この結論が出された理由を理解することさえ困難です。 ここで、条件の前にwhileループがあり、その中にshortName.charAt(index)



を含む終了条件があります:インデックスによって文字列の文字を取得します。 本質的なことは、インデックスを文字列の長さ以上にすることはできないということです。そうしないと、 charAt



IndexOutOfBoundsException



除いてドロップします。 したがって、ループがindex == shortName.length()



達すると、ループを正常に終了できなくなりますが、失敗します。 そしてindex == shortName.length()



ループを正常にindex == shortName.length()



、条件index == shortName.length()



実際には常にfalseです。







次に、例外が実際に発生する可能性があるのか​​、それとも追加条件だけであるのかを調べる必要があります。 このメソッドのフレームワーク内では、この状況は何にも矛盾しません。文字列shortName



全体が数字のみで構成されていれば十分です。 素晴らしい、それは本当のバグの匂いがする。 しかし、数字だけで構成される文字列はこのメソッドに入ることができますか? このメソッドの2つのコールポイント、 ClassesProcessor :: newIdentifierConverter :: renameClassを見てください 。 どちらの場合も、パッケージのないクラス名はshortName



として渡されshortName



。これは、Java仮想マシンの規則に従って、数字で構成される場合があります。 どちらの場合も、このコードはConverterHelper :: toBeRenamedの条件で実行されます。 条件は少し濁っていますが、クラス名が数字で始まる場合に機能することは明らかです。







どうやら、このコードは、クラスの名前が仮想マシンに対して有効であるが、Java言語に対しては無効である場合、クラスの名前を変更する責任があります。 それでは、数字から名前を付けて正しいクラスを生成しましょう。 お気に入りのASMを取りに行きます。 クラスがコンストラクタ持つことが望ましいです。 その中に何かを印刷しましょう:







 String className = "42"; ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); // public class 42 extends Object { cw.visit(Opcodes.V1_6, ACC_PUBLIC | ACC_SUPER, className, null, "java/lang/Object", new String[0]); // private 42() { MethodVisitor ctor = cw.visitMethod(ACC_PRIVATE, "<init>", "()V", null, null); // super(); ctor.visitIntInsn(ALOAD, 0); ctor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); // System.out.println("In constructor!"); callPrintln(ctor, "In constructor!"); // return; ctor.visitInsn(RETURN); ctor.visitMaxs(-1, -1); ctor.visitEnd(); // }
      
      





さて、クラスが本当に正常であることを確認するために、正直なHello World



main



しましょう:







 // public static void main(String[] args) { MethodVisitor main = cw.visitMethod(ACC_PUBLIC|ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); // System.out.println("Hello World!"); callPrintln(main, "Hello World!"); // return; main.visitInsn(RETURN); main.visitMaxs(-1, -1); main.visitEnd(); // } cw.visitEnd(); // }
      
      





callPrintln



メソッドcallPrintln



簡単です。ここにあります:







 private static void callPrintln(MethodVisitor mv, String string) { mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn(string); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); }
      
      





クラスをファイルに保存します。







 Files.write(Paths.get(className+".class"), cw.toByteArray());
      
      





素晴らしい、クラスが生成され、正常に実行されます:







 $ java 42 Hello World!
      
      





今、それを逆コンパイルしてみてください:







 $ java -jar fernflower.jar 42.class dest INFO: Decompiling class 42 INFO: ... done
      
      





不運、落ちなかった。 結果のファイルの内容を見てみましょう:







 public class 42 { private _2/* $FF was: 42*/() { System.out.println("In constructor!"); } public static void main(String[] var0) { System.out.println("Hello World!"); } }
      
      





名前の変更はまったく機能しないようです。 クラスはまだ42と呼ばれ、もちろん有効なJavaクラスではありません。 さらに、コンストラクターの名前が変更され、通常はコンストラクターでなくなりました。 もちろん、逆コンパイラが有効なJavaファイルを作成できなかったのは良いことですが、もっと欲しいのです。







どういうわけか、名前の変更を有効にできますか? README.mdで直接説明されているいくつかのオプションがあります。 そして、それらの間で、 ren



オプション:









さて、試してみましょう:







 $ java -jar fernflower.jar -ren=1 42.class dest Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 2 at java.lang.String.charAt(Unknown Source) at ...renamer.ConverterHelper.getNextClassName(ConverterHelper.java:58) at ...renamer.IdentifierConverter.renameClass(IdentifierConverter.java:187) at ...renamer.IdentifierConverter.renameAllClasses(IdentifierConverter.java:169) at ...renamer.IdentifierConverter.rename(IdentifierConverter.java:63) at ...main.Fernflower.decompileContext(Fernflower.java:46) at ...main.decompiler.ConsoleDecompiler.decompileContext(ConsoleDecompiler.java:135) at ...main.decompiler.ConsoleDecompiler.main(ConsoleDecompiler.java:96)
      
      





なんでも! まあ、彼らは必要に応じて正確に落ちました。 そして、逆コンパイラのソースコードを掘り下げることすらありません。 興味深いことに、ユーザーがそのようなクラスを含むjarファイル全体を逆コンパイルすると、少なくとも1つのファイルが逆コンパイルされる前に逆コンパイル全体がクラッシュします。 メッセージによると、どのクラスがエラーの原因であるかは完全に不明です。 そのようなクラスを難読化されたjarのどこかに深く詰め込み、そのようなjarを逆コンパイルしないで十分です。 はい、残念ながら、デフォルトで無効になっているオプションで開始する必要がありますが、他の難読化メカニズムにより、このオプションの使用が非常に望ましい場合があります。







もちろん、私は難読化ツールではなく逆コンパイラーを作成する会社で働いているため、脆弱性を悪用する代わりに、それを報告して終了しました。 また、更新されたIDEA静的アナライザーを使用してコード内の同様のエラーを見つけるには、 ソースからIntelliJ Community Editionをコンパイルするか、2017.2 EAPプログラムを待つことができます。 また、静的分析を過小評価しないでください。 コードを分析しないと、競合他社やサイバー犯罪者がコードを分析し、あなたの人生を台無しにする何かを見つけます。








All Articles