给定一个 PE 二进制文件,导出其中可能的所有字符串。
网上找了一大堆,实在没找到满意的,最后自己写了一个,输入一个 50 MB 的 PE 文件,第一个版本需要 30 分钟出答案,优化到最后一个版本,只须要 10 秒。
通过脚本生成 C 代码,把判定树做到 PE 文件里面,做成 函数跳转,避免字典查询。
strings.exe
,貌似只支持搜索不支持导出。
C:\kSource\blog>D:\Strings\strings.exe
Strings v2.54 - Search for ANSI and Unicode strings in binary images.
Copyright (C) 1999-2021 Mark Russinovich
Sysinternals - www.sysinternals.com
usage: D:\Strings\strings.exe [-a] [-f offset] [-b bytes]
[-n length] [-o] [-s] [-u] <file or directory>
-a Ascii-only search (Unicode and Ascii is default)
-b Bytes of file to scan
-f File offset at which to start scanning.
-o Print offset in file string was located
-n Minimum string length (default is 3)
-s Recurse subdirectories
-u Unicode-only search (Unicode and Ascii is default)
-nobanner
Do not display the startup banner and copyright message.
strings.exe
,说是可以导出 PE 文件的字符串,实际上对中文支持不好。
C:\kSource\blog>C:\Qt\Tools\mingw810_32\bin\strings.exe -help
Usage: C:\Qt\Tools\mingw810_32\bin\strings.exe [option(s)] [file(s)]
Display printable strings in [file(s)] (stdin by default)
The options are:
-a - --all Scan the entire file, not just the data section [default]
-d --data Only scan the data sections in the file
-f --print-file-name Print the name of the file before each string
-n --bytes=[number] Locate & print any NUL-terminated sequence of at
-<number> least [number] characters (default 4).
-t --radix={o,d,x} Print the location of the string in base 8, 10 or 16
-w --include-all-whitespace Include all whitespace as valid string characters
-o An alias for --radix=o
-T --target=<BFDNAME> Specify the binary file format
-e --encoding={s,S,b,l,B,L} Select character size and endianness:
s = 7-bit, S = 8-bit, {b,l} = 16-bit, {B,L} = 32-bit
-s --output-separator=<string> String used to separate strings in output.
@<file> Read options from <file>
-h --help Display this information
-v -V --version Print the program's version number
C:\Qt\Tools\mingw810_32\bin\strings.exe: supported targets:
pe-i386 pei-i386 elf32-i386 elf32-iamcu elf32-little elf32-big
plugin srec symbolsrec verilog tekhex binary ihex
Report bugs to <http://www.sourceware.org/bugzilla/>
以上都不怎能理想,我需要的是 gbk、utf8、utf16le 三种编码。
我正在编写一些分析 PE 文件中的字符串的代码(Python,但实际上并不重要)。 我正在寻找一个可以调用的命令行工具,它将返回 PE 文件中的完整字符串列表。 我知道 PEDUMP,但它似乎给出了不完整的字符串。 此外,此工具能够处理不同类型的字符串非常重要,例如 C 字符串(NULL 终止),Pascal 字符串(长度前缀)等。
GBK、GB2312 以及 Unicode 都既是字符集,也是编码方式,而 UTF-8 只是编码方式,并不是字符集。
抽象出问题:给定一个二进制串,暴力枚举里面所有可能的各种编码的字符串。 计算给定二进制字符串的所有可能解码组合。动态规划 增强的 brute-force 方法。
针对每一种编码可能,尝试每一种可能的编码长度,尝试解码。
只尝试解码 gbk
字符集里面的字符串。
def trystr(fdata, startp, encoding, xli, fcache, checkenc):
手工撸了一个暴力解码:
#encoding=utf8
import os
import string
def readfile(path):
if not os.path.exists(path):
return b""
fin = open(path, "rb")
page = fin.read()
fin.close()
return page
def trystr(fdata, startp, encoding, xli, fcache, checkenc):
eatstr = ""
eatsize = 0 # 消耗了多少个字节。
while True:
char = None
charlen = 0
for ilen in xli:
try:
char = fdata[startp:startp+ilen].decode(encoding)
if checkenc: char.encode(checkenc)
assert len(char) == 1, char
char = char[0] # 字符串变成字符
# https://docs.python.org/zh-tw/3.8/library/string.html
if ord(char) <= 0x7f:
assert char in string.printable, char
charlen = ilen
break
except KeyboardInterrupt as ex:
raise ex
except:
continue
if char and charlen and ord(char):
fcache[startp] = charlen
eatstr = eatstr + char
startp += charlen
eatsize += charlen
else:
fcache[startp] = -1
break
return eatsize, eatstr
def hackstring(fpath, encoding, checkenc, xli, strset, printx=False):
fdata = readfile(fpath)
fsize = len(fdata)
fcache = [0 for i in range(fsize + xli[-1])]
retv = []
for startp in range(fsize):
if fcache[startp]: # 这里已经校验过了。
continue
eatsize, eatstr = trystr(fdata, startp, encoding, xli, fcache, checkenc)
eatbin = eatstr.encode(encoding)
# https://docs.microsoft.com/zh-cn/sysinternals/downloads/strings
if len(eatstr) <= 1 or len(eatbin) <= 2: continue # 只有一个字符,忽略掉。
if eatstr in strset: continue # 已经存在了。
strset.add(eatstr)
if printx:
print(fsize, startp, eatsize, encoding, len(eatstr), '"{}"'.format(eatstr))
retv.append((fsize, startp, eatsize, encoding, len(eatstr), eatstr))
return retv
if __name__ == "__main__":
fpath = r"E:\kapp\toyapp\test.dll"
strset = set()
hackstring(fpath, "gbk", None, [1, 2,], strset, True)
hackstring(fpath, "utf-16-le", "gbk", [2, 4], strset, True)
hackstring(fpath, "utf8", "gbk", [1, 2, 3, 4], strset, True)
输入一个 50 MB 的 PE 文件,需要 30 分钟出答案。
首先写一个程序生成所有 gbk 编码 字符 的解码树。
{
"09": "\t",
"0a": "\n",
"81": {
"40": "丂",
"41": "丄",
"42": "丅",
"43": "丆"
},
"82": {
"40": "侤",
"41": "侫",
"42": "侭",
"43": "侰"
}
}
{
"09": "\t",
"0a": "\n",
"0b": "\u000b",
"0c": "\f",
"ce": {
"91": "Α",
"92": "Β",
"93": "Γ",
"94": "Δ",
"95": "Ε"
},
"e3": {
"80": {
"80": " ",
"81": "、",
"82": "。",
"83": "〃",
"85": "々",
"86": "〆"
},
"81": {
"81": "ぁ",
"82": "あ",
"83": "ぃ",
"84": "い",
"85": "ぅ",
"86": "う"
}
}
}
{
"00": {
"25": "─",
"30": " ",
"4e": "一",
"9f": "鼀"
},
"01": {
"01": "ā",
"04": "Ё",
"25": "━",
"30": "、",
"4e": "丁",
"4f": "企"
},
"02": {
"25": "│",
"30": "。",
"4e": "丂",
"4f": "伂"
}
}
完整的版本: gbk.table.json utf8.table.json utf16le.table.json
一共 21891 个字符。
线段树存储,大概长这样。
09 0a ['\t', '\n']
81 82 ['func_81', 'func_82']
81 - 40 43 [' 丂 ', ' 丄 ', ' 丅 ', ' 丆 ']
82 - 40 43 [' 侤 ', ' 侫 ', ' 侭 ', ' 侰 ']
用什么数据结构存储呢? 最快的应该是把这个数据结构做到 PE 文件里面,避免任何查询操作,直接就是函数跳转。 代码大概这样:
int func_81(GETBYTE getbyte) { if (!getbyte) return -1;
if (getbyte(1) >= 0x40 && getbyte(1) <= 0x43) {
const static MYCHAR chararray[] = { 0x4e02, 0x4e04, 0x4e05, 0x4e06 };
return chararray[getbyte(1) - 0x40];
}
return -1;
}
int func_82(GETBYTE getbyte) { if (!getbyte) return -1;
if (getbyte(1) >= 0x40 && getbyte(1) <= 0x43) {
const static MYCHAR chararray[] = { 0x4fa4, 0x4fab, 0x4fad, 0x4fb0 };
return chararray[getbyte(1) - 0x40];
}
return -1;
}
int func_gbk(GETBYTE getbyte) { if (!getbyte) return -1;
if (getbyte(0) >= 0x09 && getbyte(0) <= 0x0a) {
const static MYCHAR chararray[] = { 0x9, 0xa };
return chararray[getbyte(0) - 0x09];
}
if (getbyte(0) >= 0x81 && getbyte(0) <= 0x82) {
const static MYFUNC funcarray[] = { func_81, func_82 };
return funcarray[getbyte(0) - 0x81](getbyte);
}
return -1;
}
脚本代码生成的代码: mycode_gbk.c mycode_utf8.c mycode_utf16le.c
最后编译出来:toystrings.exe。
直接命令行输入文件,结果存在文件 *.pedump.strings.txt
里面。
C:\kSource\pythonx>filetool\toystrings.exe filetool\toystrings.exe
https://sunocean.life/tools/
filetool\toystrings.exe
time=94 // 毫秒
结果存在文件 toystrings.exe.pedump.strings.txt
里面。
462848 77 44 gbk 44 "!This program cannot be run in DOS mode.
$"
462848 128 6 gbk 5 "eW 瀡 !6"
462848 131 3 gbk 3 "v!6"
....
第一行,文件大小 462848,从 77 开始的 44 字节,gbk 编码,字符串 44 长度:"!This program cannot be run in DOS mode… 所有可能的字符串都导出来了。
有过破解 native 程序经验的人都知道,在大量的汇编代码面前不可能是从头开始理解代码的,必须找到一两个点进行突破。 字符串往往就是这样的关键点,在代码中 hardcode 的字符串会可以原封不动的在生成的 binary 中查找到。 所以要增加破解的难度,对字符串进行混淆(或者叫加密,下面的文字可能混淆和加密混着用,在这里没有区别)是很重要的一步。 只要字符串在代码中出现,那么其必然会在 binary 中出现,所以要想在 binary 中查找不到字符串,必须在代码进行编译之前进行变形。 所以从理论上来说,可以从下面几个角度进行入手:
Literalstring encryption as part of the build process 这里还有一个项目:strenc StringsObfuscation System 如何防止客户端被破解 Literalstring encryption as part of the build process PE 文件中隐藏明文字符串 PE 文件中隐藏明文字符串(续) In-Depth:Quasi Compile-Time String Hashing