JustTK_ - 去控制流平坦化学习

控制流平坦化是OLLVM中使用到的一种代码保护方式,它还有2个兄弟-虚假控制流和指令替换,这3种保护方式可以累加,对于静态分析来说混淆后代码非常复杂。
控制流平坦化的主要思想就是以基本块为单位,通过一个主分发器来控制程序的执行流程。
例如一个常见的if-else分支结构的程序可以是这样:
image.png
经过控制流平坦化之后,得到了一个相当规整的流程图:
反静态分析 - 控制流平坦化 - 图2
控制流平坦化在代码上体现出来可以简要地理解为是while+switch的结构,其中的switch可以理解为主分发器。这一点在IDA的反汇编里面可以很明显地体现出来。
混淆前:

  1. int __cdecl main(int argc, const char **argv, const char **envp)
  2. {
  3. int result; // eax
  4. if ( argc == 2 )
  5. {
  6. if ( check_password(argv[1]) )
  7. puts("Congratulation!");
  8. else
  9. puts("error");
  10. result = 0;
  11. }
  12. else
  13. {
  14. puts("error");
  15. result = 1;
  16. }
  17. return result;
  18. }

混淆后:

  1. int __cdecl main(int argc, const char **argv, const char **envp)
  2. {
  3. signed int v3; // eax
  4. int v4; // eax
  5. signed int v5; // ecx
  6. signed int v7; // [rsp+44h] [rbp-1Ch]
  7. int v8; // [rsp+58h] [rbp-8h]
  8. v8 = 0;
  9. v7 = 1633153878;
  10. do
  11. {
  12. while ( v7 > -1424773165 )
  13. {
  14. if ( v7 > -608328430 )
  15. {
  16. if ( v7 > 1948349962 )
  17. {
  18. if ( v7 == 1948349963 )
  19. {
  20. v8 = 1;
  21. v7 = -1883523171;
  22. puts("error");
  23. }
  24. }
  25. else if ( v7 > 1633153877 )
  26. {
  27. if ( v7 == 1633153878 )
  28. {
  29. v3 = 1228678806;
  30. if ( argc != 2 )
  31. v3 = 1948349963;
  32. v7 = v3;
  33. }
  34. }
  35. else
  36. {
  37. switch ( v7 )
  38. {
  39. case -608328429:
  40. v8 = 0;
  41. v7 = -1883523171;
  42. break;
  43. case -549365528:
  44. v7 = -608328429;
  45. puts("Congratulation!");
  46. break;
  47. case 1228678806:
  48. v4 = check_password(argv[1]);
  49. v5 = -1424773164;
  50. if ( v4 )
  51. v5 = -549365528;
  52. v7 = v5;
  53. break;
  54. }
  55. }
  56. }
  57. else if ( v7 == -1424773164 )
  58. {
  59. v7 = -608328429;
  60. puts("error");
  61. }
  62. }
  63. }
  64. while ( v7 != -1883523171 );
  65. return v8;
  66. }

从IDA的流程图中也可以很容易地识别出经过控制流平坦化的程序:
反静态分析 - 控制流平坦化 - 图3
可以看到,经过平坦化之后的代码块大部分整齐地堆积在程序流图的下方,而这部分代码就是真正有意义的代码块,称之为相关块。
而去平坦化要做的就是在茫茫人海中识别出相关块,然后理清楚这些相关块之间的关系。
目前去控制流平坦化最有效的办法是利用符号执行。具体的脚本和使用我参考了:

而控制流平坦化的理论和去平坦化的算法参考了:

在用腾讯安全应急响应中心提供的工具时因为angr和barf版本不同报了很多错,我在https://github.com/SnowGirls/deflat下载了这个工具的更新版,遇到部分报错如下:

  1. File "default.py", line 32, in statement_inspect
  2. if len(expressions) != 0 and isinstance(expressions[0], pyvex.expr.ITE):
  3. TypeError: object of type 'generator' has no len()

谷歌了一下,发现了和我遇到同样问题的师傅:

这位师傅罗列了很多的报错和修改方法,在参考并修改后执行成功:
反静态分析 - 控制流平坦化 - 图4反静态分析 - 控制流平坦化 - 图5
可以发现经过去平坦化之后的代码已经恢复了大部分的可读性。
上面的反混淆脚本在处理控制流平坦化时只能解决简单的while+switch套娃。如果输入的数据能够影响循环的走向,我们就不得不处理已经经过的分支,否则因为分支过多而爆内存…个人猜想可以通过手动patch的方法对程序执行的路径树进行“剪枝”,或者把一个控制流平坦化流程图分成2个。目前还没有实际遇到过。以后遇到的时候再具体分析。

参考

反混淆:恢复被OLLVM保护的程序

利用符号执行去除控制流平坦化

修复去除控制流平坦化工具deflat.py