Binary:生成.lib 和 .h from .dll

1. 前言

When 逆向的时候,会有这样一种情况,你只有一个 .dll 文件,而没有相应的 .h.lib,which 使你不能够非常方便的使用这个 .dll 中的功能函数。那么这个时候就需要我们自己动手去构建这两个文件了。

.dll 文件生成 .lib 文件并不是什么困难的事情,在 GenLibFromDll 中已经对此过程进行了较为详细的描述。对应的详细 VS 工具集使用可以看 Create .lib file from .dll file : Visual Studio

仅仅有 .lib 文件也是不方便使用 .dll 的,因为代码的编写需要的是 .h 文件。而对于采用 MSVC 编译生成的 C++ 来说,it’s possible to 通过名称还原代码。我也找到了 Others 的实现结果。But for fishwheel,I 不想 仅仅是 一个使用者。

2. Generate .lib from .dll

2.1 基本过程

通过 .dll 生成 .lib 是容易的,在 VS 的相应工具集中有现成的工具可以去实现这个功能,which involved 两个主要工具—— dumpbin.exelib.exe

按照如下的命令格式可以按照 .dll 的导出表得到相应的 .def 文件。

# the Common.dll is the file you want export
dumpbin Common.dll /exports /out:Common.def

运行 over 上面的命令 in VS Console,一个 .def 文件将会生成。

Dump of file Common.dll

File Type: DLL

  Section contains the following exports for Common.dll

    00000000 characteristics
    FFFFFFFF time date stamp
        0.00 version
           1 ordinal base
        1915 number of functions
        1898 number of names

    ordinal hint RVA      name
    14    2 0007B860 ??0?$SupportsWeakPtr@VCShortStreamContainer@@@AsyncTask@@QAE@XZ
    ...
    52   17 000DD170 ??0CHttpClient@@QAE@XZ
    ...
    1909  758 001833F0 __DetourRemoveWrapper
    1910  759 00013C70 __GetG3Guid
    7  75A 001DAED8 compress
    ...
    21      00007890 [NONAME]
    ...
    Summary

		8C000 .data
		64000 .rdata
		19000 .reloc
		1000 .rsrc
		1E3000 .text

上述的 .def 文件无法直接传入到 lib.exe 中生成相应的 .lib 文件,it’s because 文件格式需要进行一些处理,将 [NONAME] 的导出部分去除,仅保留序列号 ordinalname 两个字段的内容,按照 name @ordinal 的格式进行输出。

最后文本的格式需要是这样的(why 要这样,可以看 Building an Import Library and Export File):

EXPORTS
	??0?$SupportsWeakPtr@VCShortStreamContainer@@@AsyncTask@@QAE@XZ	@14
	...
	??0CHttpClient@@QAE@XZ	@52
	...
	__DetourRemoveWrapper	@1909
	__GetG3Guid	@1910
	compress	@7
	...

得到上述格式的 .def 文件之后,运行如下命令就可以生成 .lib 文件了。

lib /def:Common.def /out:Common.lib

2.2 Python 自动化实现

2.1 的基本过程中可以看到,对于 dumpbin 的运行结果需要进行一些处理才能够传入到 lib.exe 中输出 .lib 文件。所以 python 自动化实现要做的就是两件事:处理 dumpbin 生成的 .def文件控制对 dumpbin.exe 和 lib.exe 的调度

fishwheel 希望 python 脚本按照如下的命令行格式进行运行:

GenLibFromDll.py ??.dll

同时由于 Object Oriented 较为深入我心,所以完成这个任务我首先考虑的是如何的设计一个 class 来合理的组织数据和功能。

2.2.1 对象设计

由于我的命令行格式是需要接收一个参数,也就是 .dll 的文件名作为参数的,所以需要在 class 中要有相应的结构来存储这个名称,同时后续通过 名称粉碎 生成 头文件 时需要有相应的函数名,so 需要一个有相应的结构来存储。因此我设计了如下的数据部分来存储这些 eastwest

三个数据成员 szFullNameszFileNameszExtName 分别来存储传入参数的解析结果。而 listFuncName 成员被初始化为了 list 对象,它将用来存储 导出函数名

class FWGenLib:
    def __init__(self, szFullName=None):
        self.szFullName = szFullName
        self.szFileName = None
        self.szExtName = None
        # save the function name in this member, which will be used to generate the .h file
        self.listFuncName = list()
        if(szFullName != None):
            nDot = self.szFullName.find(".")
            # save the file name without extend name
            self.szFileName = self.szFullName[:nDot]
            # save the extend name
            self.szExtName = self.szFullName[nDot + 1:]

2.2.2 进程创建

dumpbin.exelib.exe 是两个单独的程序,因此 when 调度时就需要进行 进程 的创建,以及输出结果的捕获。这一点在 python 中实现起来是十分方便的。可以直接使用 subprocess 对象的 run 成员函数。

class FWGenLib:
	...
    def RunCommand(self, cmd):
        """
        * RunCommand of the windows its output will be return
        * parameter 0 : the cmd you want to run
        * return value : the output string of this command. if the command not existing it will 
        occur an exception which I don't handle.
        """
        retval = subprocess.run(cmd, capture_output=True, text=True)
        return retval.stdout

返回结果的 stdout 成员中就存储了 console 的输出结果。

2.2.3 .def 文件处理

In 2.1 中介绍了 dumpbin.exe 生成的 .def 文件和 lib.exe 需要的 .def 文件之间的差异,所以这一节要做的 thing 就是解析字符串。将格式从 ordinal hint RVA name 输出为 name @ordinal

首先需要做的是将 dumpbin.exe 输出的 “\n\n” 字符串替换为 “\n”,这样便于进行后续的查找分析。

接着通过 str 类的 find 方法找到字符串 “ordinal hint RVA name”,忽略掉它之前的字符串,将其后的字符串作为分析的对象。

最后就是确定截止位置,在 dumpbin 的输出结果中会在最后有一个 “Summary” 字符串,找到该字符串作为分析的结束标志。

class FWGenLib:
	...
    def ParseDumpbin(self):
        """
        * Parse the output of the dumpbin ??.dll /exports
        * return value : none, but it will create a .def file
        """
        cmd = f"dumpbin {self.szFullName} /exports"
        retval = self.RunCommand(cmd)
        # lock the useful data
        retval = retval.replace("\n\n", "\n")
        nStart = retval.find("ordinal hint RVA      name")
        nStart = retval.find("\n", nStart)
        nEnd = retval.find("Summary\n")
        # attention: the rfind() use the end as start, start as end. So two position needed than find()
        nEnd = retval.rfind("\n", 0, nEnd)
        retval = retval[nStart + 1 : nEnd]
        ...

通过上面操作之后将 dumpbin 中的无用信息去除掉了,只留下了我们感兴趣的部分,其结果大致如下所示:

         14    2 0007B860 ??0?$SupportsWeakPtr@VCShortStreamContainer@@@AsyncTask@@QAE@XZ
         15    3 0007B860 ??0?$SupportsWeakPtr@VCStorageIterator@@@AsyncTask@@QAE@XZ
         16    4 001676E0 ??0AutoBenchTimerLog@Bench@Util@@QAE@ABV012@@Z
         ...
         21      00007890 [NONAME]
         22      000078D0 [NONAME]
         ...

通过 str 类的 splitlines 方法将整段的字符串切片成一系列的行,每一行就是一个 导出函数 的信息,在一个循环中对其进行处理。

将每一行的前后空格去除,按照 " " 对其进行切片。partition 方法每次只会处理一个分隔符,所以需要使用一个 while 循环来进行处理 until 整个行被成功切片。其切片的结果保存到了一个 list 对象 lineList 中。

class FWGenLib:
	...
    def ParseDumpbin(self):
    	...
        # split the line
        lines = retval.splitlines()
        parseList = list()
        lineList = list()
        for line in lines:
            # make the line is like "ordinal hint rva name" which is separate by single " "
            # delete the front space
            line = line.strip()
            # delete the mutil space
            newline = line.partition(" ")
            while(newline[1]):
                line = newline[2]
                # record the partition string
                lineList.append(newline[0])
                # delete the front space
                line = line.strip()
                newline = line.partition(" ")
            # append the last string which should be the function name
            lineList.append(line)
            # !!!there must use the deepcopy one, because the lineList is use as referred which will be clear
            # when a new loop start, however, in C++, use the string.push_back() will create a new one rather than
            # referring it.
            parseList.append(copy.deepcopy(lineList))
            # clear the data
            lineList.clear()
		...

需要注意一点的是,在 lineList 是作为局部变量,用来存放每次 for 循环的结果,而最终需要将其放入到 parseList 中保存。通过 partition 方法进行添加时需要使用 deepcopy 的结果,不然添加的只是 Reference,当 lineList 调用 clear 方法后其中的数据就会清除。

将每一行的数据切片之后就会得到 4 个部分的数据,分别对应了ordinal hint RVA name,而我们只需要其中的,nameordinal

在某些情况下没有 hintname 字段的内容,此时 list 对象中的数据长度就会 小于 4,遇到这种情况 fishwheel 选择直接放弃处理。

最后一个额外操作是将分析到的函数名添加到 self.listFuncName 中保存,为后面生成头文件做准备。

class FWGenLib:
	...
    def ParseDumpbin(self):
    	...
        # write to the .def file
        fFile = open(f"{self.szFileName}.def", "w")
        fFile.write("EXPORTS\n")
        for lineList in parseList:
            # there some occasion don't hve the hit and function name
            if(len(lineList) != 4):
                continue
            line = "\t" + lineList[3] + "\t@" + lineList[0] + "\n"
            fFile.write(line)
            # append the function name to listFuncName
            self.listFuncName.append(lineList[3])
        fFile.close()
        return

2.2.4 生成 .lib 文件

通过 FWGenLib 对象的 ParseDumpbin 方法处理之后,会生成一个符合 lib.exe 要求的 .def 文件,那么就可以直接创建 进程 来生成该 .dll 对应的 .lib 文件。

class FWGenLib:
	...
    def GenLib(self):
        """
        * use the .def file to generate a .lib file
        * return value : none, but will create a .lib file
        """
        self.ParseDumpbin()
        cmd = f"lib /def:{self.szFileName}.def /out:{self.szFileName}.lib"
        self.RunCommand(cmd)
        return

3. Generate .h from .dll

3.1 基础知识

首先让我们看看通过 FWGenLib 对象的 ParseDumpbin 方法处理之后,放入到 self.listFuncName 中保存的是些什么 eastwest

...
??0BigNum@Ecdsa@@QAE@PAUbignum_st@@@Z
??0BigNum@Ecdsa@@QAE@PBD@Z
??0BigNum@Ecdsa@@QAE@XZ
??0CCmdCodecBase@@QAE@XZ
??0CDataSenderBase@@QAE@ABV0@@Z
...

上面的内容是存储在其中的一部分内容,它们有一个统一的名字 名称粉碎,采用的是 MSVC 的标准而不是 gccclang 的标准。我们可以使用 undname.exe 将其还原回原本的名字。
在这里插入图片描述
fishwheel 大致总结出了这些函数名的特征,其中会有域运算符 ::,模板实例化部分 <>,以及函数的形参部分。

private: void __thiscall ClassA::ClassB<class ClassC, class ClassD>::func(struct structA);

上面这样的一个实例还原成头文件时应该是这样的:

// the customed type must be defined before using it
struct structA;
class ClassA {
public:
	// ClassB is a template, need use the parameters to replace the arguments
	template <typename T1, typename T2>
	class ClassB {
	priivate:
		void func(struct structA);
	};
};

还原过程中如下原则:

  • 具有 privatepublicprotect 关键字修饰的才是类的调用。不具有的统一按照 namespace 进行处理。
  • C++ 应该先定义后使用,所以函数 返回值形参 中的自定义类型应该在该函数定义之前。
  • 模板 <> 中可能存在嵌套,将嵌套部分作为单独类型。(如果不知道 what 是 模板 可以查看 Learn:C++ Primer Plus Chapter 8)。
  • 域名解析符号 :: 作为分隔的不同名称的分隔符。对于挂载在其下的子域,应当采用 Tree 数据结构进行存储(如果不知道 what 是 命名空间 可以查看 Learn:C++ Primer Plus Chapter 9)。

3.2 数据结构

3.2.1 TreeNode

fishwheel 设计如下的 TreeNode 类作为存储解析数据的基本单元。由这些树节点最终会组成一棵树,将其结果输出之后就可以还原出 .h 头文件。

class TreeNode:
    """
    * this call using a tree to save the parse output
    * value is the string this node save whild children is the branchs of this root
    """
    def __init__(self):
        # the string saving the node
        self.value = list()
        # the branchs of this root
        self.children = list()
        # the security attribute, ["public", "private", "protested"]
        self.nSecState = -1
        # the domain name of the name, ["class", "struct", "enum", "namespace"]
        self.nDomainName = -1
  • value:该成员用来记录需要输出的 字符串。初始化为 list 类是由于后续会用其来存储 template <typename T1>\nclass ClassA{}; 这种类型的字符串,分隔存储有利于最后进行输出。
  • children:该成员用来记录以当前节点为 父域子域 节点。
  • nSecState:该成员用来表示该节点的 安全属性,即 [“public”, “private”, “protested”] 中的一个。该值被初始化为 -1 表示无安全属性,以便后续进行默认处理。It’s necessary that 使得相同的安全属性只用输出一次,而不必每次都输出,avoid 下面的情况出现。
class ClassA{
public:
	void func1();
public:
	void func2();
public:
	void func3();
};
  • nDomainName:该成员用来表示节点的 修饰,即 [“class”, “struct”, “enum”, “namespace”] 中的一个。该值被初始化为 -1,以便后续进行默认处理。

3.2.2 ParseTree

使用 TreeNode 构成一棵树,which will be used to 输出 .h 头文件。ParseTree 类包含了解析还原的基本操作,以及相应的数据 enumeration

class ParseTree:
    # the list of closure brackets
    listFront = ["(", "[", "{", "<"]
    listBehind = [")", "]", "}", ">"]
    # this mutual variable is used for the head file generation
    listSeparator = ["__cdecl", "__stdcall", "__thiscall", "__fastcall"]
    # below use to add public, private and protested in the class and struct in C++
    listSecAttri = ["public:", "private:", "protected:"]
    nPublic = 0
    nPrivate = 1
    nProtected = 2
    # below use to generate the decorated name
    listDomainName = ["class", "struct", "enum", "namespace"]
    nClass = 0
    nStruct = 1
    nEnum = 2
    nNamespace = 3
    # a set to record visited type name
    setVisited = set()
    def __init__(self):
        # the root of this parse tree
        self.root = TreeNode()
        self.root.value.append("")
        # the attribute of security, public, private or protested
        self.nSecState = -1
        # the nTableCount is count the tab number in the recursive calling output
        self.nTabCount = 0
  • listFrontlistBehind:枚举前括号与后括号,which will be used in 括号闭合识别。
  • listSeparator:调用约定字符串的枚举,which belong to x86。使用枚举中的字符串作为分隔符来区分函数 返回值函数名
  • listSecAttrinPublicnPrivatenPortect:类的安全属性字符串枚举以及对应的数组索引。
  • listDomainNamenClassnStructnEnumnNamespaceC++ 域的修饰符字符串的枚举以及对应的数组索引。
  • setVisited:一个集合 that 用来记录已经分析的 type name 避免重复记录。
  • root:树的根节点。
  • nSecState:当前的安全属性。输出时如果节点的安全属性相同则不输出,否则输出。
  • nTabCounttab 键的数量。递归输出时控制缩进。

3.3 函数操作

对于 this ParseTree 来说,需要有 添加节点按括号闭合切片解析域运算符树形字符串输出 等方面的功能。

[1]添加节点

3.3.1 AddNode

AddNode 函数将一个子 TreeNode 添加到父 TreeNodechildren 中。

实现时首先判断该节点是否在 父节点children 中,使用 for 循环遍历 children list,比较 TreeNodevalue 成员是否相等。

如果相等,表明该节点已经记录在树中,则将局部变量 bFlagIn 置为 1,同时对于当前节点的 nDomainName 成员进行更新,如果是,默认的值 -1 则对其进行更新。

如果不相等,表明该节点没有记录在树中,将其添加在 parentchildren list 中。

class ParseTree:
	...
    def AddNode(self, node:TreeNode, parent:TreeNode) -> TreeNode:
        """
        * add a node to the tree
        * parameter 0 : the node need to add
        * parameter 1 : the parent of this node
        * return value : the node that should be next node's parent
        """
        bFlagIn = 0
        for child in parent.children:
            if child.value == node.value:
                bFlagIn = 1
                # set the domain name
                if child.nDomainName == -1:
                    child.nDomainName = node.nDomainName
                return child
        # the parent don't have this node
        if not bFlagIn:
            parent.children.append(node)
            return node
  • nodeTreeNode 类型,被添加的节点。
  • parentTreeNode 类型,添加到的节点。
  • return valueTreeNode 类型,下一个的父节点。

[2]按括号闭合切片

3.3.2 ClosureMatch

寻找反括号 in a 括号 closure 的字符串中。

主要的操作部分在 for 循环中,通过 listappendpop 方法作为匹配栈。when 栈被清空的时候就找到与之匹配的反括号位置了。

class ParseTree:
	...
    def ClosureMatch(self, szStr:str, nSymIndex:int) -> int:
        """
        * match the closed symbol of this symbol. such as "(" to find ")" 
        * parameter 0 : the string need to find the match
        * parameter 1 : the index of bracket in the string
        * return value : the index of the closed symbol in the szStr, failed will return -1
        """
        listFront = self.listFront
        listBehind = self.listBehind
        stack = list()
        nIndex = listFront.index(szStr[nSymIndex])
        if(nIndex == -1):
            print(f"Error, Can't find {szStr[nSymIndex]} is not in {listFront}")
            return -1
        nStart = nSymIndex
        nEnd = -1
        szTmp = szStr[nStart : ]
        stack.append(szStr[nStart])
        for i in range(1, len(szTmp)):
            if(szTmp[i] in listFront):
                stack.append(szTmp[i])
                continue
            if(szTmp[i] in listBehind):
                ch = stack.pop()
                # below just a condition tell, if need effiency can be commented
                if(listFront.index(ch) != listBehind.index(szTmp[i])):
                    print("Error, not match closed bracket")
                    return -1
            if(len(stack) == 0):
                nEnd = nStart + i
                break
        return nEnd
  • szStr:括号闭合的字符串。由于是从 名称粉碎 中直接获取的,所以不用进行输入的检查。
  • nSymIndex:一个正括号在字符串中的索引号,you can 通过 find 函数找到 before calling 这个函数。
  • return value:与正括号匹配的反括号的索引号,如果失败则返回 -1

3.3.3 SplitWithClosure

使用分隔符对于传入的字符串进行切片,与 strsplit 不同的是,when 遇到括号时,会将括号中的部分作为一个整体,而不会进行切片。

下面的例子有效说明了该函数的作用,传入下面的字符串,使用分隔符 “,”

(class ClassA, class ClassB<class ClassC, class ClassD>, class ClassE)

通过 SplitWithClosure 函数进行切片之后会得到下面的结果。

class ClassA
class ClassB<class ClassC, class ClassD>
class ClassE

SplitWithClosure 中首先对于传入的字符串进行 check,如果是被括号包裹的,则将最外侧的括号去除。然后是对于 listSub 参数的使用,如果引用为 None 则创建一个局部变量进行占位,否则对其进行引用。

class ParseTree:
	...
    def SplitWithClosure(self, szStr:str, listSub:list=None, szSeparator:str=",") -> int:
        """
        * Split a string using szSeparator, which will see {} () <> [] as a whole
        * parameter 0 : [in]the string need to be split, like <class a, class b<class b1, class b2>>
        * parameter 1 : [out]the sub string list in the string
        * parameter 2 : [in]the separator string
        * return value : the number of sub string at least 1
        """
        listFront = self.listFront
        # delete the bracket
        if(szStr[0] in self.listFront):
            szStr = szStr[1 : -1]
        # the return value
        nRet = 1
        nStart = nEnd = 0
        nLen = len(szSeparator)
        # there is receiver or not
        if(listSub == None):
            ArgList = list()
        else:
            ArgList = listSub
        ...
  • szStr:[in] 需要进行切片的字符串。
  • listSub:[out] 接收切片后的子串。
  • szSeparator:[in] 分隔符。
  • return value:切片子串的数量。

In while 循环中,首先找到分隔符,获取 nStart 到分隔符的字符串切片。接着使用 for 循环字符串切片,如果其中有一个字符位于 listFront 中就调用 ClosureMatch 方法获取到该 前括号 对应 反括号 的索引,接着将整个括号包围的部分作为一个整体加入到返回 list 中保存,while 循环结束后将剩余的字符串也添加到其中,从而完成按 szSeparateor 的字符串切片,同时保存了括号的完整性。

class ParseTree:
	...
    def SplitWithClosure(self, szStr:str, listSub:list=None, szSeparator:str=",") -> int:
    	...
		nEnd = szStr.find(szSeparator)
        while(nEnd != -1):
            szSub = szStr[nStart : nEnd]
            szSub = szSub.strip()
            # the const sz
            szcSub = copy.deepcopy(szSub)
            # check if there are brackets in the szSub
            for ch in szcSub:
                # having the brackets in the szSub
                if(ch in listFront):
                    nEnd = self.ClosureMatch(szStr, szStr.find(ch, nStart))
                    szSub = szStr[nStart : nEnd + 1]
                    szSub = szSub.strip()
                    nEnd = szStr.find(szSeparator, nEnd)
                    break
            # set next start. if break from above for and the closure is the last one, nEnd will be -1
            if(nEnd != -1):
                nStart = nEnd + nLen
                nEnd = szStr.find(szSeparator, nStart)
                nRet += 1
                ArgList.append(szSub)
        ArgList.append(szStr[nStart : ].strip())
        return nRet

[3]解析域运算符

3.3.4 ParseName

ParseName 将传入的完整函数名进行解析,按照域运算符 :: 将函数名拆解,放入到函数返回值的 list 中。同时遇到 template 时,将 <> 中的实例化类型解析出来放入到 setRecset 中保存。

对于如下的 Demangling 函数名,ParseName 需要接收其中的 ClassA::ClassB<class ClassC, class ClassD>::ClassB<class ClassC, class ClassD>(struct structA) 作为参数 szName 的内容。

private: void __thiscall ClassA::ClassB<class ClassC, class ClassD>::ClassB<class ClassC, class ClassD>(struct structA);

运行结束后会得到如下结果:

listRet = ["ClassA", "tepmpalte <typename T1, typename T2>\nClassB", "ClassB<class ClassC, class ClassD>(struct structA)"]
setRec = {"class ClassC", "class ClassD", "struct structA"}

首先 按照 域名 对传入的函数名进行切分 by calling SplitWithClosure 函数,using “::” 作为切片的 delimiter

class ParseTree:
	...
	def ParseName(self, szName:str, setRec:set) -> list:
        """
        * parse a namespace operator name to a list
        * parameter 0 : [in] the string of the function name
        * parameter 1 : [out] a set of subname of the template
        * return value : receive a list of parse result
        """
        listSub = list()
        listArg = list()
        listRet = list()
        nBase = 1
        self.SplitWithClosure(szName, listSub, "::")
        ...
  • szName:[int] 需要 Parse 的函数名字符串。
  • setRec:[out] 模板中的实例化类型字符串 set
  • return value:一个解析结果的字符串 list

接着对于分片好的 域名 进行 参数类型 识别,主要是 template <> 实例中的类型名,以及函数 参数列表 () 中的类型。

遍历切片结果 list 判断 “<”“(” 的相对位置。只有在 “(” 之前的识别到的 “<” 才能作为当前需要处理的模板,而在 “(” 之后出现的则将其记录到 setRec 中返回。

实现时 nStart 作为后续分析参数类型的起始位置,需要进行一系列判定。nStart1 作为 “(” 的索引,while nStart2 作为 “<” 的索引。(Attention:nStart2 需要排除 operator< 运算符重载的影响)。选择 nStart1nStart2 的那一个作为 nStart 的值,同时如果选择了 nStart2 则将 bFlagTemplate 作为置位。

class ParseTree:
	...
	def ParseName(self, szName:str, setRec:set) -> list:
		...
		for szSub in listSub:
            # handle the template instantiation
            if("<" in szSub or "(" in szSub):
                bFlagTemplate = 0
                nStart1 = szSub.find("(")
                nStart2 = szSub.find("<")
                # handle the operator<
                if nStart2 != -1:
                    nStart3 = szSub.find("operator<")
                    if nStart3 != -1 and (nStart3 + len("operator")) == nStart2:
                        nStart2 = szSub.find("<", nStart2 + 1)
                nStart = nStart1
                if nStart < 0 or (nStart2 >= 0 and nStart1 > nStart2):
                    nStart = nStart2
                    bFlagTemplate = 1
		...

接着对于找到的 <…>(…) 闭包,调用 SplitWithClosure 函数以 “,” 作为 delimiter,对其中的参数列表进行获取,最后将其添加到接收 set setRec 中。

class ParseTree:
	...
	def ParseName(self, szName:str, setRec:set) -> list:
		...
		for szSub in listSub:
			# handle the template instantiation
            if("<" in szSub or "(" in szSub):
				...
				nEnd = self.ClosureMatch(szSub, nStart)
                nNum = self.SplitWithClosure(szSub[nStart : nEnd + 1], listArg)
                # add the template type arg to the set
                for arg in listArg:
                    arg = self.ClearKeyword(arg)
                    setRec.add(arg)
		...

对于具有 bFlagTemplate 标记的 condition,需要对域名字符串进行更改,下面代码的目的就是将 class ClassA<ClassB, ClassC> 改为 template <T1, T2>\nclass ClassA

There is a condition in which 函数名是带有模板实例化类型参数的,这就是模板类的 Constructor 函数。When 碰到这样的模板时,应该保持原本的样子 rather than 更改为 template <T1, T2>\n... 的样子。

由于 Constructor 函数与 类型名 是一样的,所以只需要判断返回值 list listRet 中的最后一个元素是否是模板实例化即 “\n” 字符是否含有,并且当前的函数名是否为其子串。

class ParseTree:
	...
	def ParseName(self, szName:str, setRec:set) -> list:
		...
		for szSub in listSub:
			# handle the template instantiation
            if("<" in szSub or "(" in szSub):
				...
				# generate template for it
                if bFlagTemplate:
                    listArgName = list()
                    for i in range(nNum):
                        listArgName.append(f"typename T{nBase}")
                        nBase += 1
                    szTemplate = "template <"
                    for i in range(len(listArgName) - 1):
                        szTemplate += listArgName[i] + ", "
                    szTemplate += listArgName[-1] + ">\n"
                    # don't insert the szTemplate first
                    szSub = szSub[0 : nStart] + szSub[nEnd + 1 : ]
                    # the previous is a template and next is the construct function of it
                    nIndex = -1
                    if(len(listRet)):
                        nIndex = listRet[-1].find("\n")
                    if(nIndex != -1):
                        nIndex += 1
                        # not the construct function, add the szTemplate
                        if (listRet[-1][nIndex : ] not in szSub):
                            szSub = szTemplate + szSub
                    else:
                        szSub = szTemplate + szSub
		listRet.append(szSub)
		...

上面是 ParseName 函数的一般情况处理,fishwheel 在实例化过程中遇到了 转换函数(如果不知道 what 是 转换函数 可以查看 Learn:C++ Primer Plus Chapter11)which need 特殊处理。

In a class 转化函数大致是这样的 operator class ATL::CTXComPtr<struct ITXData>(void)const,与普通函数名不同的是,该函数名中间是有 空格 的,而且参数列表一定为空,所以最好的处理方法就是找到 operator空格 字符串,将后面的部分直接添加到返回列表中。

首先需要做的是将被 SplitWithClosure 函数切片的 operator 后的类型还原回去,并将多余部分从 listSub 列表中 eliminate。将这个还原回来的函数名复制给 szOperator,并从中取出类型名添加到 setRec 接收 set 中。

此时关于 转换函数 的处理完成,当完成 for 循环的分析之后,只需要将 szOperator 中的内容添加到返回 list listRet 中就可以了。

class ParseTree:
	...
	def ParseName(self, szName:str, setRec:set) -> list:
		...
		# handle the operator tyepname, which transfer the type of a class or struct
        szOperator = ""
        for i in range(len(listSub)):
            if("operator " in listSub[i]):
                # append the operator behind
                for j in range(i + 1, len(listSub)):
                    listSub[i] += "::" + listSub[j]
                # delete the operator behind
                for j in range(i + 1, len(listSub)):
                    listSub.pop()
                szOperator = listSub.pop()
                # get the type name of the operator and add it to the set
                nStart = szOperator.find("operator ") + len("operator ")
                nEnd = szOperator.find("(", nStart)
                setRec.add(self.ClearKeyword(szOperator[nStart : nEnd]))
                break
		...
		if(szOperator != ""):
            listRet.append(szOperator)
		return listRet

3.3.5 HandleFuncPointer

HandleFuncPointer 处理函数指针类型。我在函数的描述注释中已经写得比较清晰了,就是将 int (__cdecl)(struct _EXCEPTION_POINTERS ) 解析为 ["int", "(struct _EXCEPTION_POINTERS )"] 的过程。

首先通过 calling convention 来得到 nIndex,接着以此为锚点向前寻找 “(” 得到函数返回值;向后寻找 “)” 得到函数参数列表。

    def HandleFuncPointer(self, szFuncPtr:str, setRec:set):
        """
        * some function use the function pointer as the argument type,
        such as "void __cdecl TXBugReport::SetCustomFiltFunc(int (__cdecl*)(struct _EXCEPTION_POINTERS *))"
        the function pointer type is "int (__cdecl*)(struct _EXCEPTION_POINTERS *)" after add to set
        it becomes "int (__cdecl)(struct _EXCEPTION_POINTERS )"
        so I just need to do it spearate the return type and argument type as ["int", "(struct _EXCEPTION_POINTERS )"]
        * parameter 0 : [in] the function type pointer
        * parameter 1 : [out] a set to receive the output
        """
        for sep in self.listSeparator:
            nIndex = szFuncPtr.find(sep)
            if nIndex != -1:
                break
        nStart = szFuncPtr.rfind("(", 0, nIndex)
        nEnd = szFuncPtr.find(")", nIndex)
        setRec.add(szFuncPtr[0 : nStart].strip())
        listSub = list()
        self.SplitWithClosure(szFuncPtr[nEnd + 1 : ].strip(), listSub)
        for sub in listSub:
            setRec.add(sub)
        return
  • szFuncPtr:[in] 函数指针类型字符串。
  • setRec:[out] 接收类型字符串的 set
  • return value :无。

3.3.6 ParseFuncName

传入解析的名称粉碎,并将其添加到树中。该函数会对传入的字符串进行处理,之后调用 ParseNameSplitWithClosure 等函数。

class ParseTree:
	...
    def ParseFuncName(self, szFunc:str):
        """
        * parse a demangling function name
        * parameter 0 : the string of the function name
        """
  • szFunc:解名称粉碎的结果
  • return value:无,but 会将结果存入 self.root 中。

首先对于传入的字符串进行处理,需要将其分为三个部分,存入到一个 tuple tupleRec 中保存。这三部分大致有两个主体组成 typename。而处于他们中间的则是分隔符,大多数情况下这个分隔符是由 calling convention 组成的,但是也存在一些特殊情况,需要进行特殊处理。

for 全局变量,typename 之间的分隔符则是一个 " “,此时需要从第一个域运算符 :: 开始向前寻找 ” ",从而分割出 typename

for 函数指针,这是一个非常麻烦的情况 because 函数指针变量是被括号包裹的,与通常的结构是不同的。代码中的例子需要拆分成这样void, (__cdecl*, TXBugReport::pfPostBugReport)(void)

class ParseTree:
	...
    def ParseFuncName(self, szFunc:str):
		for sep in self.listSeparator:
            nIndex = szFunc.find(sep)
            if(nIndex != -1):
                # get a tuple like ["public: int", "__thiscall", "FunctionName"]
                tupleRec = szFunc.partition(sep)
                break
        # handle the global variable
        if nIndex == -1:
            nIndex = szFunc.find("::")
            nIndex1 = szFunc.rfind(" ", 0, nIndex)
            tupleRec = (szFunc[ : nIndex1], "", szFunc[nIndex1 + 1 : ])
        # handle the function pointer, void (__cdecl* TXBugReport::pfPostBugReport)(void)
        if tupleRec[0][-1] == "(":
            tupleRec = (tupleRec[0][ : -1], "(" + tupleRec[1] + "*", tupleRec[2][1 : ])
		...

接着将 name 部分传入 ParseName 中进行解析,解析的 域名 list listFirst 中的每一个字符串创建一个新的 TreeNode 节点,并调用 AddNode 方法将自身添加到树中。

listFirst 中的最后一个字符串是函数本身的名字,需要进行单独处理,主要是为了确定函数的 Security Attribute which include private, public and protect。Why only 函数的 actual name 能够确定 Security Attribute 这是 because 解粉碎的名称前的修饰符只能对函数真名有效。如果切分出来的 type 部分中有安全属性的字符串,则证明当前节点是类中的一部分,而不是 namespace,同时需要对于当前字符串进行切个,将安全属性去除。此后 strTmp 中存储的就是真正的 type 了。

最后一个节点的 value list 中需要存放函数去除掉 域名 之后的状态,此时需要进行一下字符串拼接。同时如果 strTmp 的值与 tupleRec[0] 中的不相等则表示之前进过切片,即含有 Security Attribute ,那么证明该节点的父节点一定是 structclass 中的一个,而不可能是 namespace,我默认将其设置为 class

class ParseTree:
	...
    def ParseFuncName(self, szFunc:str):
    	...
		setRec = set()
        # handle the function name
        listFisrt = self.ParseName(tupleRec[2], setRec)
        szCalingConvention = tupleRec[1]
        # don't display __thiscall
        if szCalingConvention == self.listSeparator[2]:
            szCalingConvention = ""
        nodeParent = self.root
        for i in range(len(listFisrt) - 1):
            node = TreeNode()
            node.value = listFisrt[i].split("\n")
            nodeParent = self.AddNode(node, nodeParent)
        # the last one is the actual name
        node = TreeNode()
        # find the securiy attribute
        strTmp = tupleRec[0].strip()
        for i in range(len(self.listSecAttri)):
            if (self.listSecAttri[i]) in tupleRec[0]:
                node.nSecState = i
                # delete the security attribute in the declaration
                strTmp = tupleRec[0][len(self.listSecAttri[i]) : ].strip()
                break
        if szCalingConvention == "":
            node.value.append(strTmp + " " + listFisrt[-1])
        else:
            node.value.append(strTmp + " " + szCalingConvention + " " + listFisrt[-1])
        node.value[-1] = node.value[-1].strip()
        # set the parent's DomainName to nClass
        if strTmp != tupleRec[0].strip():
            nodeParent.nDomainName = self.nClass
        self.AddNode(node, nodeParent)
        ...

下面的代码是处理 setRec 中的自定义类型,这些类型有一个非常明显的特征 what is 必须以 “class”,“struct”,“enum” 这三个中的一个作为开头。对于这些用户自定义类型,如果 Tree 中有则更新其 nDomainName 字段内容,如果没有则新建一个 TreeNode 节点并将其挂载到 root 节点下。

首先处理 strTmp 中的返回值类型,only when 其为用户自定义类型时,才将其添加到 setRec 中以便进行后续处理。

进入 while 循环中首先进行一些判断,排除掉一些特殊情况,依次排除 已处理基础类型std::标准库函数指针

class ParseTree:
	...
    def ParseFuncName(self, szFunc:str):
    	...
		# handle the sub type which is not a function but a type name
        # get the return type
        strTmp = self.ClearKeyword(strTmp)
        for doname in self.listDomainName:
            # if there is a class or struct in the name add it to the set
            if doname in strTmp:
                setRec.add(strTmp)
                break
        # handle the type set
        setVisited = self.setVisited
        while(len(setRec)):
            bFlagContinue = 1
            strTypeName = setRec.pop()
            if strTypeName in setVisited:
                continue
            else:
                setVisited.add(strTypeName)
            # exclude the basic type
            for doname in self.listDomainName:
                # if there is a class or struct in the name add it to the set
                if doname in strTypeName:
                    bFlagContinue = 0
            if bFlagContinue:
                continue
            # exclude std::
            if "std::" in strTypeName:
                continue
            # handle the function pointer type
            for sep in self.listSeparator:
                if sep in strTypeName:
                    self.HandleFuncPointer(strTypeName, setRec)
                    bFlagContinue = 1
                    break
            if bFlagContinue:
                continue
		...

将特殊情况排除之后就可以进行一般处理,首先根据类型的前缀来确定确定该类型的 域名。接着调用 ParseName 函数对于内容部分进行解析,将得到的 listRec 按照之前的逻辑添加到 Tree 中。

class ParseTree:
	...
    def ParseFuncName(self, szFunc:str):
    	...
    	while(len(setRec)):
			...
			# analysis the domain type
            nSpace = strTypeName.find(" ")
            if strTypeName[ : nSpace] == "class":
                nDomain = self.nClass
            elif strTypeName[ : nSpace] == "struct":
                nDomain = self.nStruct
            elif strTypeName[ : nSpace] == "enum":
                nDomain = self.nEnum
            strTypeName = strTypeName[nSpace : ].strip()
            listRec = self.ParseName(strTypeName, setRec)
            # add the result to the tree
            nodeParent = self.root
            for i in range(len(listRec) - 1):
                node = TreeNode()
                node.value = listRec[i].split("\n")
                node.nDomainName = self.nNamespace
                nodeParent = self.AddNode(node, nodeParent)
            # the last one is the function name
            node = TreeNode()
            node.nDomainName = nDomain
            node.value = listRec[-1].split("\n")
            self.AddNode(node, nodeParent)
		return

[4]树形字符串输出

3.3.7 OutputTree

通过前面的分析已经将一个 解名称粉碎 的函数名字符串中的相关信息记录到了树中,下面要做就是将这个树输出出来,形成头文件。既然是树那么最方便的输出方式就是 递归 了。

代码实现时首先是递归出口,only when 当前节点是孩子节点时才能结束递归。首先输出 Security Attribute,只有当 self.nSecStatenode.nSecState 不同时才将其输出。接着将节点 value 中的前部分输出,只保留最后一个进行特殊处理,根据 nDomainName 字段是否被赋值来确定是否输出相应的修饰。

进入普通递归部分,还是将节点的 value 前部分输出只保留最后一个进行特殊处理,同样是为了确定其域名修饰符,但不同的是,如果 nDomainName 没有被赋值则将输出 namespace 作为默认修饰。

最后进行递归调用完成后续节点的输出。

class ParseTree:
	...
	def OutputTree(self, node:TreeNode) -> str:
        ret = ""
        # the recursive exit
        if(len(node.children) == 0):
            # output the securty attribute. public, private or protested
            if(node.nSecState != self.nSecState):
                self.nSecState = node.nSecState
                if(self.nSecState != -1):
                    ret += "\t" * (self.nTabCount - 1) + f"{self.listSecAttri[self.nSecState]}\n"
            for i in range(len(node.value) - 1):
                ret += "\t" * self.nTabCount + f"{node.value[i]}\n"
            if node.nDomainName != -1:
                ret += "\t" * self.nTabCount + f"{self.listDomainName[node.nDomainName]} {node.value[-1]};\n"
            else:
                ret += "\t" * self.nTabCount + f"{node.value[-1]};\n"
            return ret
        
        # ouput the value
        for i in range(len(node.value) - 1):
            ret += "\t" * self.nTabCount + f"{node.value[i]}\n"
        if node.nDomainName != -1:
            ret += "\t" * self.nTabCount + f"{self.listDomainName[node.nDomainName]} {node.value[-1]}" + " {\n"
        else:
            ret += "\t" * self.nTabCount + f"{self.listDomainName[self.nNamespace]} {node.value[-1]}" + " {\n"
        # # add "public" for every class
        # if "namespace" not in node.value[0]:
        #     ret += "\t" * nTabCount + "public:\n"
        # output the child value
        self.nTabCount += 1
        node.children.reverse()
        for child in node.children:
            ret += self.OutputTree(child)
        self.nTabCount -= 1
        # clear the security attribute
        self.nSecState = -1
        ret += "\t" * self.nTabCount + "};\n"
        return ret

3.3.8 CallOutputTree

CallOutputTree 对于递归调用进行了一次 encapsulate,方便其使用。

class ParseTree:
	...
    def CallOutputTree(self, node:TreeNode) -> str:
        self.nTabCount = 0
        return self.OutputTree(node)

3.3.9 OutputRoot

OutputRoot 是最终的对外接口,它调用相应的函数完成将以 root 为根的树的输出。

class ParseTree:
	...
    def OutputRoot(self) -> str:
        ret = ""
        self.root.children.reverse()
        for child in self.root.children:
            ret += self.CallOutputTree(child)
        return ret

4. Unsolved Problems

fishwheel 的这个方案的 implementation 还是存在一些未能解决的问题,这里先将其记录下来,后续如果需要则来进行解决。

  • 输出顺序问题:C++ 是先声明后使用,所以类型声明一定要在使用之前。目前只是将节点中的孩子节点的逆向输出,不能解决根本问题。
  • 疑难定义:int (__cdecl*__cdecl TXBugReport::GetCustomFiltFunc(void))(struct _EXCEPTION_POINTERS *) like 这样的函数定义,我看不懂,也不知道该怎么解析。
  • 局限性:目前只能解决 MSVC 编译的 x86.lib 和头文件生成,无法解决 x64 的问题。

最后如果你需要直接使用可以直接去 Generate .lib and .h from .dll

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值