间接OCR解字体混淆加密的实践思路
- 网页→字体文件的base64码
- base64码→字体文件
- 字体文件→字符、字形图像
- 字形图像→明文
- 字符→明文
从网页获取字体文件的base64码
通过观察可知,“某通”有关于字体混淆加密的字体样式具有独特性(ID=”cxSecretStyle”)。因此,我们可以通过 CSS选择器 轻松定位此元素(CSS_SELECTOR=”#cxSecretStyle”),在获取它的 textContent 属性之后,再通过 “正则表达式” 来提取它的 base64码 。
def get_base64_selenium(selector): # 获取指定style标签中的base64值
# 定位指定的<style>标签
try: style_element = driver.find_element(By.CSS_SELECTOR, selector)
except NoSuchElementException: return None
style_content = style_element.get_attribute("textContent")
# 提取Base64值(增强正则)
base64_pattern = r"base64,([A-Za-z0-9+/=\s]+?)(?=['\";)])"
matches = re.findall(base64_pattern, style_content, re.DOTALL)
if matches:
# 清理空白字符并选择最长的结果
clean_base64 = max([m.replace(" ", "").replace("\n", "") for m in matches], key=len)
return clean_base64
return None
@contextmanager # 构造上下文管理器
def return_base64(selector): # 切换到目标frame,调用 get_base64_via_selenium 获取ttf文件的base64值
frame1 = driver.find_element(By.ID,'iframe')
driver.switch_to.frame(frame1)
frame2 = driver.find_element(By.CSS_SELECTOR,'#ext-gen1049 > div.wrap > div > p > div > iframe')
driver.switch_to.frame(frame2)
frame3 = driver.find_element(By.ID,'frame_content')
driver.switch_to.frame(frame3)
try:
yield get_base64_selenium(selector) # "通过ID定位构造CSS选择器"
finally:
driver.switch_to.default_content() # 最终切换回默认主框架
接下来我们要通过 base64码 生成相关的字体文件。
import base64
def ttf_get(base64_string,opt_path):
bytes_data = base64.b64decode(base64_string) # 从 base64字符串 中解码出二进制数据
with open( opt_path , 'wb' ) as ttf_file: # 以二进制写入模式打开 opt_path路径 指向的文件
ttf_file.write(bytes_data)
接下来我们通过字体文件渲染出其包含的各个字符的 “字形图像” 。(此图像为 “明文图像” 但字符却是密文字符,相当于输入“密文字符”却能显示出“明文字符的字形”)
import os
from PIL import ImageFont,Image,ImageDraw
from fontTools.ttLib import TTFont
def image_get(character,font_path,font_size=32,pad=10): # 根据字体文件渲染出指定字符的图像
font = ImageFont.truetype(font_path,font_size) # 加载 truetype font,即 ttf 文件
left,top,right,bottom = font.getbbox(character) # 获取相应字符在字体文件中的字形坐标信息
char_width = int(right - left)
char_height = int(bottom - top)
canvas_width = char_width + pad*2
canvas_height = char_height + pad*2
canvas = Image.new('RGB',(canvas_width,canvas_height),color='white') # new一个 RGB 通道的指定长宽的纯白“画布”
draw = ImageDraw.Draw(canvas) # 创建一个与“画布”绑定的“画笔”对象
draw.text((pad-left,pad-top),character,font=font,fill='black') # 在画布上绘画出一个字形使其位于“正中心”,填充颜色为黑色
return canvas # 返回“画布”
def plaintext_image_get(ttf_path,font_size=32,pad=10,opt='./font'):
os.makedirs(opt)
font = TTFont(ttf_path) # 创建ttf字体对象
cmap = font.getBestCmap() # 获取判断最优的"字符字形映射表"
unicodes = cmap.keys() # 获取字典中的所有“键” ,这里对应“字符”的Unicode码点
for unicode in unicodes:
char = chr(unicode) # 根据 Unicode码点 获得对应的字符
img = image_get(char,ttf_path,font_size,pad) # 获取字形图像
filepath = os.path.join(opt,f'{char}.png')
img.save(filepath)
至于其中,为什么在绘画字形的时候,”draw.text()” 的 “xy” 参数,即 “字形原点” 要指定为 “(pad-left,pad-top)” 以使得字形位于“画布”中心呢?其实这是经过了相对严密的计算得来的结果。感兴趣的可以点开下面的链接,听我细细道来。
我们现在需要通过字形图像获取”密文字符“所对应的明文,很简单还是要使用 OCR ,但是我们只需要对这几个 ”字符“ 单独使用 OCR ,然后建立一个 ”密文到明文的映射表“ ,我们就可以直接 ”爬取“ 网页的密文字符,然后利用映射表将其中的密文映射到明文,最终获得明文字符段。
OCR 市面上有很多相应的工具可以选择,我当时使用的主要是 PaddleOCR 。
值的一提的是, PaddleOCR 对中文数字一到十的单独识别效果不是很好,经常识别不出来,所以我之后又使用 pytorch 的VGG16网络自己训练了一个专门识别中文数字的OCR,但PaddleOCR与pytorch不兼容,所以我又将PaddleOCR进行了单独打包,然后通过Subprocess对其作为子进程调用……
这里不再赘述 OCR 部分,大家可以自行选择写一个 OCR 函数,用以传入图片并返回识别出的字符 。
import json
import os
def plaintext_ciphertext_json(imgs_dir):
ciphertext_plaintext = {}
filenames = os.listdir(imgs_dir)
for filename in filenames:
ciphertext = filename.split('.' , 1) # 因为我当时直接把密文字符设置为文件名了,就很方便
img_path = os.path.join(imgs_dir,filename)
plaintext = ocr(img_path) # 这里假设有一个已经写好了的 ocr 函数
ciphertext_plaintext[ciphertext] = plaintext or 'OCR未能正常识别成功'
os.mkdir(./result,exist_ok=True) # 这里以当前目录下的 result 文件夹为储存文件夹创建,存在容忍
with open('./result/decode.json','w',encoding = 'utf-8') as f:
json.dump(ciphertext_plaintext,f,ensure_ascii=False,indent=4) # 将字典以 json 格式保存,非确保为ASCII以容忍Unicode中的非ASCII字符,设置缩进indent为4以求美观
OK,现在万事俱备,只差一个”解密函数“了!
import json
def decode(ciphertext,json_path='./result/decode.json'):
with open(json_path,'r',encoding='utf-8')as f:
ciphertext_plaintext = json.load(f)
# 从密文中遍历字符,如果字符存在于映射表,则将其映射为明文字符,否则保持原字符。
plaintext = [ciphertext_plaintext[char] for char in ciphertext if char in ciphertext_plaintext else char]
return ''.join(plaintext) # 将明文字符连接并返回,因为是中文字符所以不需要空格分隔,英文字符可以使用空格分隔连接
完工!
作者
3049874370@qq.com
