├── LICENSE ├── README.md └── parseMFS.py /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Positive Technologies 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 6 | 7 | 8 | Copyright (c) 2017 Positive Technologies 9 | 10 | Данная лицензия разрешает лицам, получившим копию данного программного обеспечения и сопутствующей документации (в дальнейшем именуемыми «Программное Обеспечение»), безвозмездно использовать Программное Обеспечение без ограничений, включая неограниченное право на использование, копирование, изменение, слияние, публикацию, распространение, сублицензирование и/или продажу копий Программного Обеспечения, а также лицам, которым предоставляется данное Программное Обеспечение, при соблюдении следующих условий: 11 | 12 | Указанное выше уведомление об авторском праве и данные условия должны быть включены во все копии или значимые части данного Программного Обеспечения. 13 | 14 | ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ ГАРАНТИИ ТОВАРНОЙ ПРИГОДНОСТИ, СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И ОТСУТСТВИЯ НАРУШЕНИЙ, НО НЕ ОГРАНИЧИВАЯСЬ ИМИ. НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ПО КАКИМ-ЛИБО ИСКАМ, ЗА УЩЕРБ ИЛИ ПО ИНЫМ ТРЕБОВАНИЯМ, В ТОМ ЧИСЛЕ, ПРИ ДЕЙСТВИИ КОНТРАКТА, ДЕЛИКТЕ ИЛИ ИНОЙ СИТУАЦИИ, ВОЗНИКШИМ ИЗ-ЗА ИСПОЛЬЗОВАНИЯ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫХ ДЕЙСТВИЙ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Intel ME File System Explorer 2 | ===== 3 | This repository contains Python 2.7 scripts for parsing MFS/MFSB partition and extracting contained files. 4 | 5 | ## Usage 6 | 7 | parseMFS.py 8 | 9 | Extracted files would be stored in MFS_Partition_File_Name.bin.zip file 10 | 11 | ## Limitations 12 | 13 | Tested with MFS partitions obtained from ME 11.x firmware images 14 | 15 | ## Related URLs: 16 | 17 | [Intel ME: Flash File System Explained][1] 18 | 19 | [Intel ME: The Way of the Static Analysis][2] 20 | 21 | [Intel DCI Secrets][3] 22 | 23 | [How to Hack a Turned-Off Computer or Running Unsigned Code in Intel Management Engine][4] 24 | 25 | [Intel ME 11.x Firmware Images Unpacker][8] 26 | 27 | ## Author 28 | 29 | Dmitry Sklyarov ([@_Dmit][7]) 30 | 31 | ## Research Team 32 | 33 | Mark Ermolov ([@\_markel___][5]) 34 | 35 | Maxim Goryachy ([@h0t_max][6]) 36 | 37 | Dmitry Sklyarov ([@_Dmit][7]) 38 | 39 | ## License 40 | This software is provided under a custom License. See the accompanying LICENSE file for more information. 41 | 42 | [1]: https://www.blackhat.com/eu-17/briefings.html#intel-me-flash-file-system-explained 43 | [2]: https://www.troopers.de/troopers17/talks/772-intel-me-the-way-of-the-static-analysis/ 44 | [3]: http://conference.hitb.org/hitbsecconf2017ams/sessions/commsec-intel-dci-secrets/ 45 | [4]: https://www.blackhat.com/eu-17/briefings.html#how-to-hack-a-turned-off-computer-or-running-unsigned-code-in-intel-management-engine 46 | [5]: https://twitter.com/_markel___ 47 | [6]: https://twitter.com/h0t_max 48 | [7]: https://twitter.com/_Dmit 49 | [8]: https://github.com/ptresearch/unME11 50 | -------------------------------------------------------------------------------- /parseMFS.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys, struct, os, hashlib, hmac, zlib 3 | 4 | #*************************************************************************** 5 | #*************************************************************************** 6 | #*************************************************************************** 7 | 8 | CRC16tab = [0]*256 9 | for i in xrange(256): 10 | r = i << 8 11 | for j in xrange(8): r = (r << 1) ^ (0x1021 if r & 0x8000 else 0) 12 | CRC16tab[i] = r & 0xFFFF 13 | 14 | def CrcIdx(w, crc=0x3FFF): 15 | for b in bytearray(struct.pack("> 8)] ^ (crc << 8)) & 0x3FFF 16 | return crc 17 | 18 | def Crc16(ab, crc=0xFFFF): 19 | for b in bytearray(ab): crc = (CRC16tab[b ^ (crc >> 8)] ^ (crc << 8)) & 0xFFFF 20 | return crc 21 | 22 | #*************************************************************************** 23 | #*************************************************************************** 24 | #*************************************************************************** 25 | 26 | crcTabLo = bytearray([0, 7, 14, 9, 28, 27, 18, 21, 56, 63, 54, 49, 36, 35, 42, 45]) 27 | crcTabHi = bytearray([0, 112, 224, 144, 199, 183, 39, 87, 137, 249, 105, 25, 78, 62, 174, 222]) 28 | def CSum8(ab): 29 | csum = 1 30 | for b in bytearray(ab): 31 | b ^= csum 32 | csum = crcTabLo[b & 0xF] ^ crcTabHi[b >> 4] 33 | return csum 34 | 35 | #*************************************************************************** 36 | #*************************************************************************** 37 | #*************************************************************************** 38 | 39 | import zipfile, posixpath 40 | class zipStorage(object): 41 | compression = zipfile.ZIP_STORED 42 | #compression = zipfile.ZIP_DEFLATED 43 | def __init__(self, baseName): 44 | self.fn = baseName + ".zip" 45 | self.z = zipfile.ZipFile(self.fn, "w", self.compression) 46 | self.baseDir = None 47 | 48 | def setBaseDir(self, baseDir=None): 49 | self.baseDir = baseDir 50 | return self 51 | 52 | def add(self, path, data=None): 53 | if self.baseDir: path = posixpath.join(self.baseDir, path) 54 | if data is None: path += posixpath.sep # Folder 55 | zi = zipfile.ZipInfo(path) 56 | zi.external_attr = (040755 << 16) | 0x30 if data is None else (0644 << 16) # Assign permissions 57 | self.z.writestr(zi, data or "") 58 | del zi 59 | 60 | def __del__(self): 61 | self.z.close() 62 | 63 | #*************************************************************************** 64 | #*************************************************************************** 65 | #*************************************************************************** 66 | 67 | def sCfgMode(mode): 68 | assert 0 == (mode & ~0x1FFF) # Only 13 lowest bits used 69 | typ = " d" 70 | # 842184218421 71 | sfl = "AEIrwxrwxrwx" # A for Anti-Replay, E for Encrypton, I for Integrity 72 | r = [] 73 | for i in xrange(len(sfl)): 74 | r.append(sfl[i] if mode & (1<<(len(sfl)-1-i)) else "-") 75 | return typ[mode >> 12] + "".join(r) 76 | 77 | def sCfgOpt(mode): 78 | assert 0 == mode & ~0x001F, "mode == 0x%X" % mode # Only 5 lowest bits used 79 | sfl = "^?!MF" # M for afterManufacture, F for fromFIT 80 | r = [] 81 | for i in xrange(len(sfl)): 82 | r.append(sfl[i] if mode & (1<<(len(sfl)-1-i)) else "-") 83 | return "".join(r) 84 | 85 | #*************************************************************************** 86 | #*************************************************************************** 87 | #*************************************************************************** 88 | 89 | VFS_integrity = 0x0200 90 | VFS_encryption = 0x0400 91 | VFS_antireplay = 0x0800 92 | VFS_nonIntel = 0x2000 93 | VFS_directory = 0x4000 94 | 95 | def sVarMode(mode): 96 | typ = " d" 97 | # 21842184218421 98 | sfl = "N?AEIrwxrwxrwx" # N for Non-intel, A for Anti-Replay, E for Encrypton, I for Integrity 99 | r = [] 100 | for i in xrange(len(sfl)): 101 | r.append(sfl[i] if mode & (1<<(len(sfl)-1-i)) else "-") 102 | return typ[mode >> 14] + "".join(r) 103 | 104 | #*************************************************************************** 105 | #*************************************************************************** 106 | #*************************************************************************** 107 | 108 | class MFS_Var_Folder_Entry(object): 109 | fmtRec = struct.Struct("> 2) & 1 # bit 2 134 | self.u7 = (self.flags >> 3) & 0x7F # bits 3-9 135 | self.iAR = (self.flags >> 10) & 0x3FF # bits 10-19 136 | self.u12 = (self.flags >> 20) # bits 20-31 137 | if self.ar: self.rnd, self.ctr = struct.unpack_from(" 0 195 | assert 0xAA557887 == self.sign # Page signature 196 | assert csum == CSum8(self.data[:16]) # Page Header checksum 197 | assert 0 == b0 198 | self.oInfo = self.fmtHdr.size 199 | 200 | if self.bData: 201 | self.mFree = bytearray(self.data[self.oInfo:self.oInfo + MFS.nDataPageChunks]) 202 | self.oChunks = MFS.oDataChunks 203 | else: 204 | assert 0 == self.firstChunk 205 | self.axIdx = struct.unpack_from("<%dHH" % MFS.nSysPageChunks, self.data, self.oInfo) 206 | self.oChunks = MFS.oSysChunks 207 | 208 | iChunk, self.aiChunk = 0, [] 209 | for iRec in xrange(MFS.nSysPageChunks): # Enum Sys records 210 | if self.axIdx[iRec] & 0xC000: break # Stop on first unmaped chunk 211 | iChunk = CrcIdx(iChunk) ^ self.axIdx[iRec] 212 | self.aiChunk.append(iChunk) 213 | return 214 | 215 | def getChunk(self, iRec, iChunk): 216 | chunk, crc = self.fmtChunk.unpack_from(self.data, self.oChunks + iRec * self.fmtChunk.size) 217 | assert crc == Crc16(chunk + struct.pack("L") 298 | sign, crc, FFs = fmtHdr.unpack_from(mfsb) 299 | assert "MFSB" == sign 300 | o = fmtHdr.size 301 | assert ~zlib.crc32(mfsb[o:], -1) & 0xFFFFFFFF == crc 302 | assert '\xFF'*len(FFs) == FFs 303 | oEnd = mfsb.find('\xFF'*10, o) 304 | r = [] 305 | while True: 306 | o1324 = mfsb.find('\1\3\2\4', o, oEnd-8) 307 | if o1324 < 0: break 308 | r.append(mfsb[o:o1324]) 309 | r.append('\xFF'*fmtLen.unpack_from(mfsb, o1324+4)[0]) 310 | o = o1324+8 311 | r.append(mfsb[o:oEnd]) 312 | cb = sum(len(v) for v in r) 313 | r.append('\xFF'*(-cb % 0x2000)) 314 | return "".join(r) 315 | 316 | class MFS(object): 317 | cbHdr = MFS_Page.fmtHdr.size # 18(0x12) bytes 318 | PAGE_SIZE = 0x2000 # Page size is 8K 319 | CHUNK_SIZE = 0x40 # Chunk size is 64(0x40) bytes 320 | nDataPageChunks = (PAGE_SIZE - cbHdr) / (1 + CHUNK_SIZE + 2) # 122(0x7A) chunks per Data page 321 | oDataChunks = cbHdr + nDataPageChunks # 140(0x8C) 322 | nSysPageChunks = (PAGE_SIZE - cbHdr - 2) / (2 + CHUNK_SIZE + 2) # 120(0x78) chunks per System page 323 | oSysChunks = cbHdr + 2*(nSysPageChunks + 1) # 260(0x104) 324 | fmtVolHdr = struct.Struct("= self.nFiles 334 | chunk = self.dChunks[iNode - self.nFiles + self.nSysChunks] 335 | iNode = self.aFAT[iNode] # Get next node 336 | if iNode > 0 and iNode <= self.CHUNK_SIZE: break # Last chunk 337 | r.append(chunk) 338 | r.append(chunk[:iNode]) 339 | return "".join(r) 340 | 341 | def enumFiles(self): 342 | for iFile in xrange(self.nFiles): 343 | if 0x0000 == self.aFAT[iFile]: continue 344 | yield iFile, self.getFileData(iFile) 345 | 346 | def __init__(self, ab): 347 | self.ab = ab 348 | self.cbPart = len(self.ab) # Find total size of MFS partition 349 | assert 0 == self.cbPart % self.PAGE_SIZE 350 | self.nPages = self.cbPart / self.PAGE_SIZE # Total number of pages 351 | self.nSysPages = self.nPages/12 # Number of System pages 352 | self.nDataPages = self.nPages - self.nSysPages - 1 # Number of Data pages 353 | self.nDataChunks = self.nDataPages * self.nDataPageChunks # Number of Data chunks 354 | self.cbData = self.nDataChunks * self.CHUNK_SIZE # Data area capacity 355 | 356 | self.aSysPages = [] 357 | self.aDataPages = [] 358 | for iPage in xrange(self.nPages): # Process all pages 359 | if 0 == struct.unpack_from("" if e.mode & VFS_directory else "%5d" % len(blob.ab)) 413 | if e.mode & VFS_integrity: 414 | r.append(" %s" % blob.sec) # Integrity enabled -- append Security info 415 | self.r.append("".join(r).rstrip()) 416 | self.r.append("") 417 | 418 | for e in fld.aE: 419 | if e.name in (".", ".."): continue # Ignore self/parent references 420 | fn = posixpath.join(path, e.name) 421 | if e.mode & VFS_directory: # Directory 422 | self.stg.add(fn) 423 | self.walkFolder(fn, e.fileno, e.mode, e.salt) 424 | else: 425 | blob = MFS_Blob(self, e.fileno, e.mode, e.salt) 426 | if blob.sec: 427 | self.stg.add(fn + ".vfsSecurity", blob.sec.ab) # Security data 428 | if blob.sec and blob.sec.enc: # Encrypted 429 | self.stg.add(fn + ".vfsEncrypted", blob.ab) 430 | else: self.stg.add(fn, blob.ab) 431 | 432 | def dump(self, baseName=None): 433 | self.stg = zipStorage(baseName) 434 | self.stg.setBaseDir("chains") 435 | for iFile, data in self.enumFiles(): 436 | self.stg.add("%03X.bin" % iFile, data) 437 | 438 | for iFile in (6, 7): 439 | dName = {6:"intel.cfg", 7:"fitc.cfg"} 440 | data = self.getFileData(iFile) 441 | if data: 442 | if False: # Extract intel.cfg / fit.cfg 443 | h = hashlib.sha256(data).digest() 444 | fnCfg = "%s_%s" % (h[:8].encode("hex"), dName[iFile]) 445 | if not os.path.exists(fnCfg): 446 | with open(fnCfg, "wb") as fo: fo.write(data) 447 | self.stg.setBaseDir(dName[iFile]) 448 | MFS_Cfg_Storage(data).dump(self.stg) 449 | 450 | if self.aFAT[8]: 451 | self.stg.setBaseDir("varFS") 452 | self.r = [] 453 | self.walkFolder("home", 0x10000008) 454 | self.stg.add("varFS.log", "\n".join(self.r + [""])) 455 | del self.r 456 | 457 | def __str__(self): 458 | return "Size:%dK, nPages:%d, nSysPg:%d, nDataPg:%d, nDataChunks:%d, nSysChunks:%d, nFiles:%d/%d, cbData:%d, cbSys:%d, cbTotal:%d" % \ 459 | (self.cbPart/1024, self.nPages, self.nSysPages, self.nDataPages, self.nDataChunks, self.nSysChunks, self.nFiles, (self.cbSys-14)/2 - self.nDataChunks, self.cbData, self.cbSys, self.cbTotal) 460 | 461 | #*************************************************************************** 462 | #*************************************************************************** 463 | #*************************************************************************** 464 | 465 | def main(argv): 466 | if len(argv) <= 1: 467 | print "Usage: %s MFS.part {MFS.part}" % os.path.split(argv[0])[1] 468 | return 469 | 470 | for fn in argv[1:]: 471 | with open(fn, "rb") as fi: data = fi.read() 472 | 473 | if data.startswith("MFSB"): 474 | print ". Converting MFSB to MFS for %s" % fn 475 | data = MFSBtoMFS(data) 476 | 477 | mfs = MFS(data) 478 | print fn, mfs 479 | mfs.dump(fn) 480 | 481 | if __name__=="__main__": main(sys.argv) 482 | --------------------------------------------------------------------------------