fontfuzzer/fuzzers/native_glyf.py
#!/usr/bin/env python
# awful
import sys
sys.path.append('../')
sys.path.append('.')
import parsers.TTF as TTF
import os
import time
import random
import shutil
import string
import struct
import sqlite3
import hashlib
import datetime
import win32api
import win32gui
import threading
from ctypes import *
from struct import *
from win32con import *
from fontTools import ttLib
from multiprocessing import Process, Queue
FONT_SPECIFIER_NAME_ID = 4
FONT_SPECIFIER_FAMILY_ID = 1
FR_PRIVATE=0x10
def shortNameMine(fontPath):
try:
font = TTF.TTFont(fontPath, simpleParsing=True)
name = font.fontTableDirectories['name'].table
f = open( fontPath,'rb')
for i in name.nameRecords:
if i.platformId != 3:
continue
if i.nameId == FONT_SPECIFIER_FAMILY_ID:
offset = name.offsetWithinFile + name.offsetToStartOfStringStorage + i.stringOffsetFromStartOfStorage
f.seek(offset)
family = unpack('>{}s'.format(i.stringLength), f.read(i.stringLength))[0]
# unicode
if i.encodingId == 1:
family = unicode(family, 'utf-16-be').encode('utf-8')
elif i.nameId == FONT_SPECIFIER_NAME_ID:
offset = name.offsetWithinFile + name.offsetToStartOfStringStorage + i.stringOffsetFromStartOfStorage
f.seek(offset)
name = unpack('>{}s'.format(i.stringLength), f.read(i.stringLength))[0]
# unicode
if i.encodingId == 1:
name = unicode(name, 'utf-16-be').encode('utf-8')
if not name:
name = "default"
if not family:
family = "default"
except:
name = 'default'
family = 'default'
print ">>", name, family
return name, family
def shortName(font):
try:
for record in font['name'].names:
if record.nameID == FONT_SPECIFIER_NAME_ID and not name:
if '\000' in record.string:
name = unicode(record.string, 'utf-16-be').encode('utf-8')
else:
name = record.string
elif record.nameID == FONT_SPECIFIER_FAMILY_ID and not family:
if '\000' in record.string:
family = unicode(record.string, 'utf-16-be').encode('utf-8')
else:
family = record.string
if name and family:
break
except:
print '[D] Names issues'
name = "default"
family = "default"
print '>>>>', name, family
return name, family
# for deployment
class mainWindow():
def __init__(self):
win32gui.InitCommonControls()
self.hinst = windll.kernel32.GetModuleHandleW(None)
self.classNameRandom = ''.join( random.choice(string.ascii_lowercase + string.digits) for x in range(6) )
def CreateWindow(self):
reg = self.RegisterClass()
hwnd = self.BuildWindow(reg)
return hwnd
def RegisterClass(self):
WndProc = { WM_DESTROY: self.OnDestroy }
wc = win32gui.WNDCLASS()
wc.hInstance = self.hinst
wc.hbrBackground = COLOR_BTNFACE + 1
wc.hCursor = win32gui.LoadCursor(0, IDC_ARROW)
wc.hIcon = win32gui.LoadIcon(0, IDI_APPLICATION)
wc.lpszClassName = 'FontFuzzer{}'.format(self.classNameRandom)
wc.lpfnWndProc = WndProc
reg = win32gui.RegisterClass(wc)
return reg
def BuildWindow(self, reg):
hwnd = windll.user32.CreateWindowExW (
WS_EX_TOPMOST | WS_EX_NOACTIVATE,
reg, # atom returned by RegisterClass
'FontFuzzer{}'.format(self.classNameRandom),
WS_POPUP,
10,
10,
2000,
200,
0,
0,
self.hinst,
0 )
windll.user32.ShowWindow(hwnd, SW_SHOW)
windll.user32.UpdateWindow(hwnd)
return hwnd
def OnDestroy(self, hwnd, message, wparam, lparam):
win32gui.PostQuitMessage(0)
return True
def draw(handleDeviceContext, char_map):
chars_not_rendered = 0
chars_rendered = 0
array_types = c_wchar * len(char_map)
var1 = array_types()
for y in range(0, len(char_map) ):
var1[y] = char_map[y]
ETO_GLYPH_INDEX = 16
result = windll.gdi32.ExtTextOutW( handleDeviceContext,
5,
5,
ETO_GLYPH_INDEX,
None,
var1,
len(var1),
None )
#try:
# print '[D]\t{} : {}'.format(var1[y], result)
#except:
# continue
if result == 0:
chars_not_rendered +=1
elif result == 1:
chars_rendered += 1
return chars_rendered, chars_not_rendered
def deployMultiProcess(fontPath, queue):
try:
charsRendered, charsNotRendered = deploy(fontPath, ttLib.TTFont(fontPath))
queue.put(charsRendered)
queue.put(charsNotRendered)
except IOError as e:
print '[E] Issues', e
def deploy(fontPath, ttfInstance=None):
# setup
fontName = shortNameMine(fontPath)[1] #''.join( random.choice(string.digits) for i in range(0,6) )
lf = win32gui.LOGFONT()
number_of_font_added = windll.gdi32.AddFontResourceExA(fontPath, 0, None)
if number_of_font_added != 1:
print "[WTF] Clusterfuck: invalid font"
return 0,0
win = mainWindow()
hwnd = win.CreateWindow()
hdc = windll.user32.GetDC(hwnd)
# draw some crap
chars_not_rendered = 0
chars_rendered = 0
for j in range(10, 50, 1):
time.sleep(.1)
lf.lfHeight = j #int(random.choice(string.digits))
lf.lfFaceName = fontName # todo: http://www.undocprint.org/winspool/getfontresourceinfo
lf.lfWidth = j #int(random.choice(string.digits))
lf.lfEscapement = 0
lf.lfOrientation= 0
lf.lfWeight = FW_NORMAL
lf.lfItalic = False
lf.lfUnderline = False
lf.lfStrikeOut = False
lf.lfCharSet = DEFAULT_CHARSET
lf.lfOutPrecision = OUT_DEFAULT_PRECIS
lf.lfClipPrecision = CLIP_DEFAULT_PRECIS
lf.lfPitchAndFamily = DEFAULT_PITCH|FF_DONTCARE
hFont = win32gui.CreateFontIndirect(lf)
oldFt = win32gui.SelectObject(hdc, hFont) # replace font
#print '[*]', lf.lfFaceName, ':', lf.lfWidth, ' - ', lf.lfHeight
# chars to draw
char_map = [ chr(i) for i in range(1,255) ]
chars_rendered_current, chars_not_rendered_current = draw(hdc, char_map)
chars_rendered += chars_rendered_current
chars_not_rendered += chars_not_rendered_current
windll.gdi32.DeleteObject( win32gui.SelectObject(hdc, oldFt) )
windll.gdi32.GdiFlush()
windll.gdi32.RemoveFontResourceExW( fontPath, 0, None)
print '[*]\t{} chars not rendered'.format(chars_not_rendered)
print '[*]\t{} chars rendered'.format(chars_rendered)
# bail
releaseResult = windll.user32.ReleaseDC(hwnd, hdc)
if releaseResult == 0:
print '[E] ReleaseDc failed'
destroyResult = windll.user32.DestroyWindow(hwnd)
if destroyResult == 0:
print '[E] Destroy Window error {}'.format(GetLastError())
print '[*] Destroy window'
return chars_rendered, chars_not_rendered
def generateTestCases(fontSourceDir, dbConnection):
fonts = {}
testcasesFolder = 'testcases'
print '[D] Generating testcases: ', os.path.abspath( fontSourceDir )
for i in os.listdir( fontSourceDir ):
time.sleep(0.1)
print '----------- Start -------------------'
try:
print '[*] Loading', i
except UnicodeEncodeError as e:
print '[D] Font name\'s screwed:', e
abspath = os.path.abspath( fontSourceDir + '\\' + i)
try:
tt = TTF.TTFont(abspath)
fonts[abspath] = tt
print '[*] Fuzzing {}'.format(abspath)
# cmap -> fuzzFactor 100
# maxp -> fuzzFactor 2
# loca -> fuzzFactor 5
# glyf -> fuzzFactor 80
#tableToBeFuzzed = random.choice(list(set(TTFont(sys.argv[1]).fontTableDirectories.keys()) - set(TTFont.REQUIRED_TABLES) ))
#fuzzFactor = 100
tableToBeFuzzed = random.choice(list(set(fonts[abspath].fontTableDirectories.keys()) - set(TTF.TTFont.REQUIRED_TABLES) ))
fuzzFactor = fonts[abspath].fontTableDirectories[tableToBeFuzzed].length / 50
if fuzzFactor < 2:
fuzzFactor = 2
fileInMemory, isFontFuzzed, numberOfBytesChanged = fonts[abspath].fuzzGlyfsBitFlipping()
#fonts[abspath].fuzzDirectoriesBitFlipping()
#fileInMemory = fonts[abspath].fuzzCffTableBitFlipping()
if isFontFuzzed:
testcaseName = os.path.abspath( os.path.join( testcasesFolder,
os.path.basename(abspath).split('.ttf')[0] + '_' +
''.join( random.choice( string.ascii_lowercase + string.digits) for x in range(8) ) + '.ttf')
)
open( testcaseName, 'wb').write(fileInMemory)
b = hashlib.md5()
b.update(fileInMemory)
h = b.hexdigest()
query = "INSERT INTO results(md5, bytesChanged, fuzzer, fileName, createTime) VALUES ( '{}', '{}', 'native single glyf bitflipped', '{}', '{}')".format(h, numberOfBytesChanged, os.path.basename(abspath), datetime.datetime.now() )
print "WTF", query
try:
res = dbConnection.execute(query)
res = dbConnection.commit()
except Exception as e:
print '[F]', e
#self.fontsPath.append( '/static' + testcaseName.split('/static')[1] )
else:
print '[*] Font {} not fuzzed'.format(abspath)
print '------------ End -------------------'
except Exception as e:
print e
continue
return len( os.listdir(testcasesFolder) )
def viewerDeploy(dbConnection, fuzzerRef):
print '[*] Deploy fonts found in testcases folder'
for i in os.listdir('testcases'):
time.sleep(0.1)
if fuzzerRef.stopMe:
return
try:
print '[*] Testing {}'.format(i)
abspath = os.path.abspath('testcases' + '\\' + i)
renderTime = str(datetime.datetime.now())
print '[*] Spawn a process for rendering {}'.format(abspath)
queue = Queue()
p = Process( target=deployMultiProcess, args=( abspath, queue) )
p.start()
p.join()
charsRendered = queue.get()
charsNotRendered = queue.get()
# performance wise it's crap
b = hashlib.md5()
b.update(open(abspath, 'rb').read())
h = b.hexdigest()
try:
res = dbConnection.execute("UPDATE results SET charsRendered=?, charsNotRendered=?, renderTime=? WHERE md5=?", (charsRendered, charsNotRendered, renderTime, h))
dbConnection.commit()
# crap, clear the 1 elem queue
with fuzzerRef.queue.mutex:
fuzzerRef.queue.queue.clear()
result = dbConnection.execute("SELECT * FROM results WHERE md5=?", (h,)).fetchone()
# time now - font name - fuzzer name - bytes changed - chars rendered - chars not rendered - ctime - render time
message = str( datetime.datetime.now() ) + '@@@' + str(result[5]) + '@@@' + str(result[4]) + '@@@' + str(result[1]) + '@@@'+ str(result[2]) + '@@@'+ str(result[3]) + '@@@'+ str(result[6]) + '@@@'+ str(result[7])
fuzzerRef.queue.put( message )
finally:
# in case shit happens and the sqlite is pure crap
dbConnection.commit()
except Exception as e:
print '[E] Issues parsing font {}: {}'.format(i, e)
continue
def validateInputFonts(fontSourceDir):
fonts = {}
for i in os.listdir(sys.argv[1]):
try:
abspath = os.path.abspath(sys.argv[1] + '\\' + i)
tt = ttLib.TTFont(abspath)
fonts[abspath] = tt
print '[*] Validated {} TTF'.format(shortName(tt)[1])
except Exception as e:
print '[E] Issues parsing font {}: {}'.format(i, e)
continue
def cleanUp():
print '[*] Cleaning up fuzzed fonts'
old = 'old_testcases'
tbdel = 'testcases_to_be_deleted'
curr = 'testcases'
shutil.rmtree(tbdel)
shutil.move(old, tbdel)
shutil.move(curr, old)
os.mkdir(curr)
'''
For fuzzing framework
'''
class NativeFuzzer(threading.Thread):
def __init__(self, folder, queue):
threading.Thread.__init__(self)
self.fontsFolder = folder
self.stopMe = False
self.dbConnection = sqlite3.connect('agent1.db', check_same_thread = False, timeout=1, detect_types=sqlite3.PARSE_DECLTYPES)
self.queue = queue
# setup code
try:
os.mkdir('testcases_to_be_deleted')
os.mkdir('testcases')
os.mkdir('old_testcases')
except OSError as e:
print e
def run(self):
while not self.stopMe:
# 2] generate test cases
numberOfTestCases = generateTestCases(self.fontsFolder, self.dbConnection)
print '[*] Generated {} test cases'.format(numberOfTestCases)
# 3] deploy
viewerDeploy(self.dbConnection, self) # shit args
cleanUp()
self.dbConnection.commit()
self.dbConnection.close()
def stop(self):
self.stopMe = True
def getDescription(self):
return 'Native Ben fuzzer'
def getFuzzerInstance(folder, queue):
return NativeFuzzer(os.path.join('fonts_extracted', folder), queue)
if __name__ == '__main__':
if( len(sys.argv[1:]) < 1 ):
print 'Usage: {} font_directory'.format(sys.argv[0])
elif( len(sys.argv[1:]) == 2 and sys.argv[1] == 'd'):
try:
abspath = os.path.abspath(sys.argv[2])
print 'Testing font {}'.format(abspath)
deploy(abspath, ttLib.TTFont(abspath) )
except Exception as e:
print '[E] Issues parsing font {}: {}'.format(sys.argv[2], e)
# fuzz
else:
for i in range(0, 1):
#break
# 2] generate test cases
numberOfTestCases = generateTestCases(sys.argv[1])
print '[*] Generated {} test cases'.format(numberOfTestCases)
for i in os.listdir('testcases'):
m = hashlib.md5()
m.update(open('testcases' + '\\' + i, "rb").read())
print '[*]\t {} : {}'.format(i, m.hexdigest())
# 3] deploy
viewerDeploy()