Oyente 源码阅读笔记

大致文件结构

首先,通过 tree -L 2 查看大致 2 层文件结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
.
├── code.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── misc_utils
│   ├── generate-graphs.py
│   ├── get_source.py
│   ├── stats-collect.py
│   ├── transaction_scrape 2.py
│   ├── transaction_scrape.py
│   └── tx-stats.py
├── oyente
│   ├── analysis.py
│   ├── ast_helper.py
│   ├── ast_walker.py
│   ├── basicblock.py
│   ├── batch_run.py
│   ├── ethereum_data1.py
│   ├── ethereum_data.py
│   ├── global_params.py
│   ├── __init__.py
│   ├── input_helper.py
│   ├── opcodes.py
│   ├── oyente.py
│   ├── run_tests.py
│   ├── source_map.py
│   ├── state.json
│   ├── symExec.py
│   ├── test_evm
│   ├── utils.py
│   ├── vargenerator.py
│   └── vulnerability.py
├── pylintrc
├── README.md
├── setup.py
├── sol_examples
│   ├── BECToken.sol
│   ├── calls.sol
│   ├── etherstore.sol
│   ├── exceptions.sol
│   ├── hashforether.sol
│   ├── origin.sol
│   ├── returnvalue.sol
│   ├── rubixi.sol
│   ├── suicide.sol
│   ├── timelock.sol
│   ├── token.sol
│   ├── WalletLibrary.sol
│   └── weak_random.sol
└── web
├── app
├── bin
├── browser-solidity
├── config
├── config.ru
├── db
├── Gemfile
├── Gemfile.lock
├── lib
├── log
├── package.json
├── public
├── Rakefile
├── README.md
├── test
├── vendor
└── yarn.lock

15 directories, 52 files

Level One

几个关键些的文件(夹):

README.md :我们在 Github 下载的时候已经看了,讲解 docker 或者依赖安装等 code.md :中讲述了本项目的大致代码结构,可以先看一遍,然后看代码的时候对照着看。 setup.py : setuptools 的构建脚本,用来告诉 setuptools 关于本项目(包)的描述、入口点、数据、包含的模块等,可以看一遍了解项目概况。关于 setup.py 的详细信息,见 Packaging Python Projects 1 misc_utils 文件夹 : 杂项,放了一些对本项目实用的工具脚本 oyente 文件夹 : 项目的主要源码文件夹

sol_examples 文件夹 : 是我在 Mythril 的 github 仓库 下载的,便于测试使用(如果可以的话)。

Level Two

oyente.oyente:main

入口点: oyente 文件夹中 oyente.py 的 main() 函数

  1. 声明 args 全局变量

    后面在 analyze_bytecode 等函数中会用到输入参数们 args。

    这里编码不规范,应该在所有函数外声明全局变量,虽然可以在函数内创建全局变量,但是不推荐,2

  2. 通过 argparse 模块设置参数

    • 只需要 True 或者 False 的选项,使用了 argparse 的 action 参数3

    比如:args.source、args.evm、args.standard_json 等输入输出类型、过程配置相关参数

    • dest 参数明确指定参数属性名4

    • nargs 对一个 action 绑定多个参数,其值为 “+” 时,把该 action 所有参数 gathered into a list 5

    1. 输入

      接受三种输入:solidity program、evm bytecode、remote contracts 这里,它使用 add_mutually_exclusive_group(require=True) 互斥参数组来确保将互斥组中只有一个参数在命令行中可用,且至少有一个。 具体输入类型: | 类型| 本地 | 远程 URL| |-------|----|-------| | solidity | -s filename | -ru url_address| | 字节码 | -b -s filename | -b -ru url_address|

      solidity 源码,还可以指定输入为标准 json 格式。

    2. 程序运行配置相关

      如:指定运行 solidity 源码中的某(几)个合约 target-contract、并行 parallel、debug 等

    3. 运行过程中相关 IO 处理、操作相关

      如,输出报错,创建报告等

    4. 检测运行相关

      如:Gas、loop、depth 的限制、路径等

  3. 根据输入的参数和预先设置 0. 如果为本地文件(.sol 或 .evm),通过 -s 选项,获取到 args.source 的值

    1. 进行全局参数、日志配置
    2. 确保指定检测某个合约的功能,只能在源码输入情况下进行。
    3. has_dependencies_installed 判断依赖( z3、evm、solc )是否安装,并通过 compare_versions 函数比较版本是否为测试版本(及以下),如不满足,则提示、并退出程序。
    4. 如果是远程 URL,则创建 remote_contract.sol 或 remote_contract.evm,并使用 requests 模块写入,然后设置 args.source 为文件名
    5. 检测字节码 analyze_solidity 或者源码 analyze_solidity
    6. 检测完毕,退出 exit_code

oyente.oyente:has_dependencies_installed

本函数判断依赖是否安装、且版本是否为测试版本(及以下), 版本要求: z3 <= 4.5.1evm <= 1.7.3solc <= 0.4.19 ,否则退出程序

几个注意的点:

  1. Python 运行终端命令, type cmd 判断命令是否存在

    1. cmd_exist 开启一个子进程在 shell 中执行 type cmd 命令来判断是否存在

    为什么要使用子进程测试呢? 属于 ”Python 中调用 Linux 命令并获取返回值“ 知识范畴6 结论:如果要兼容 python 3.5 以下就用 subprocess.call,反之,用 subprocess.run7

  2. 获得版本

  3. 比较版本 8

    通过 compare_versions 函数,使用 pypthon2 的 cmp 比较,但是还通过 six 模块判断,在比较时,对 python3 做了兼容,

    python 中直接使用 <、>、==、>= 等进行比较即可

    为什么要比较小于等于? 版本的向后兼容性(Backward Compatibility)9

    不过,有的 urllib2 等又没有对 python2 做兼容,真是奇怪。我们直接上 python3,不用兼容 python2,它都要被淘汰了。

  4. 判断 z3 依赖的方法

    它采用捕获 import 异常的方法以及获取版本函数的方式

    在我的 linux 上,z3 也可以通过 type z3 判断存在与否,通过 z3 --version 来获取版本,即:和其他两个依赖方法一直。 但是我的 Ubuntu server 上貌似不行,后面看看你们可不可以,如果可以,那这里用函数把三者统一一下。

判断命令是否存在,一般有 type、hash、python 捕获导入异常10 11

oyente.oyente:analyze_bytecode

  1. 创建 Input_Helper 类的对象

    类对象的初始化操作主要工作 确定要检测的文件类型与位置 设置默认参数,并获取一些配置参数

    1. 并根据输入类型:字节码设置一些默认属性

      传入 InputHelper.BYTECODE 共有类属性[^class_attr],来说明输入为字节码

    2. 遍历字典项:属性

      使用 six.iteritems 遍历字典,做了 python2/3 的兼容12,这里再说一句,本项目没有全部兼容处理,有问题。。 通过字典的 指定 default 参数的 get 方法获取属性13 使用 setattr 实现在循环中,设置任意属性名和任意属性值14

  2. 调用 helper 对象的 get_inputs() 获取返回列表的第一项,其内容为:反汇编结果的文件名

    1. 对于指定检测 solidity 源码文件中的某几个合约的,get_inputs 采用了默认参数

      了解一下,可变参数(这里的话,就是 list)的默认参数需要设置为 None 15

    2. 对于字节码,它根据 args.source 中的文件名读取内容到 bytecode 变量

    3. 将 bytecode 传递给自身的私有方法 _prepare_disasm_file

    4. _prepare_disasm_file 处理字节码,并生成反汇编文件

      1. 通过 _write_evm_file 进行去 SwarmHash 处理16,然后写入到 f"{args.source}.evm" 文件中

        去除它的原因:swarm hash in bytecode can be misinterpreted as INVALID opcode

        SwarmHash/Swarm 是一个点对点文件共享系统,设计用来有效地存储和检索 ethereum 应用程序和合约中所需的数据。 最简单的类比应该是:Swarm 本质上是 ether 的 BitTorrent。 solc 编译后的字节码里面有个 CBOR 元数据编码,它里面就有 SwarmHash。SwarmHash 以固定的串开头,以固定的串结尾。

      2. 通过 _write_disasm_file 读取上一步处理好的 .evm ,然后反汇编,并写入到 f"{args.source}.evm.disasm" 文件中

        1. 读取上一步处理完的字节码到 evm_file 变量

        2. 开启一个子进程 subprocess,通过执行终端命令: evm disasm evm_file ,若反汇编失败则退出程序

          这里使用了 popen17 run 函数的底层,就是 Popen 函数。run 函数是同步的,要等待子进程实行结束,或者超时。Popen 创建子进程后,采用异步的方式,不会等待。Popen 比 run 要更加灵活,如果 run 函数还不能满足你的需求,就考虑 Popen

        3. 将反汇编结果写入 f"{args.source}.evm.disasm"

    5. 然后将反汇编结果文件名作 {'disasm_file': disasm_file} 字典,附加到 inputs 列表中并返回

  3. 将反汇编的文件名传入 symExec.run() 开始符号执行,检测漏洞,并返回结果和退出码

  4. 删除临时文件

    包括之前生成的“处理过的字节码文件”,“反汇编结果文件”,还有日志等。

  5. 如果是全局参数 WEB 设置了,使用 six.print_ 输出一下 json 格式的结果。

  6. 返回退出码

目前发现的缺点

  1. 在函数内使用 global 创建全局变量 苏安然可以这样用,但是貌似不推荐?

  2. python2 与 python3 兼容性没做好 虽然使用了 six 模块,但是只用其处理了 print,没处理好比较、urllib2 等,还是不能兼容 python2 和 python3

  3. 模块化不彻底,不同功能之间有点混乱。 感觉相互之间交叉,功能有点混乱,也可能是我看源码的能力不够?

  4. 动态为对象添加属性 不太方便看,不知道规不规范?

  5. 获取判断依赖那里没准可以统一用函数?

参考资料

  1. Copy file to xclip and paste to Firefox

[^class_attr]:Class and Instance Attributes Python 中的类属性、实例属性与类方法、静态方法