RE
Reverse Engineering
逆向工程的作用目前对我来说就是一种能够将魔法一般的程序拉下神坛, 将这个黑箱子里面的东西的秘密破解出来的技术.
逆向工程的定义:
Reverse engineering, also called back engineering, is the process by which a man-made object is deconstructed to reveal its designs, architecture, or to extract knowledge from the object;
– Wikipedia
(这里面的知识有点多, 感觉自己没有很好地消化, 大概以后可以了解一些. )
程序的构成的思想
有一点感觉, 但是没发很好地表达出来, 肯定是学的还不够到位, 等闲下来我就开始着手看看这方面的书.
组合
世界上最厉害的玩具是什么? 是积木.
– 苏菲的世界
积木的种类可以不必很多, 但是积木却可以通过组合形成各种各样的不同的形状, 譬如说积木可以组成一个拱形, arch, 也就是古罗马人崇尚的形状. 他们对拱形的热爱导致了他们的建筑中的拱形元素是常常可见的. 由基本积木组成的拱形, 在建筑中又成为了新的基本元素. 可以说这样的组合的元素得到了一次抽象, 变成了一种基本的元素符号.
在语言里面也有这样的事情, 简单语言所定义的复杂含义在被意符指代(refer)之后, 就从一种块状的复合物的形象变成了独立的个体元素. 好像是原来一大块的内容淡出了人们的视野, 取而代之的是一张小小的标签, 标签上和原来的内容还连着的一根细细的丝线. 就像是”爽”字的故事(传说? )
比如说在程序设计里面函数也就像是一种抽象(操作过程的抽象):
# ruby code
def func(arg, *args, &block)
# massive codes
return
end
就像是一个把一堆代码包裹在一起, 用一个func
的符号来指引的思想.
(但是实际的执行并不是直接像是在func
的位置插入一段代码的方式来实现的,
而是通过打开一个新的环境scope
, 在其中执行完代码后返回值,
这个和汇编还有C的函数调用都是一样的, 打开一个栈, 运行, 关闭栈, 返回)
解剖
和组合相对应的是解剖, 将一个硕大无比的主体通过分割的方式化简为一个个的小的主体, 在部分之间通过数据的流动和交换联系在一起. (哎呀, 好像变成了对组合的解释了. )
将一个整体分割成部分进行审视, 可以得到对整体的更好的认识.
譬如说ruby中String
类型的对象的方法gsub
对于正则表达式匹配的时候的特性,
作为整体的时候可能难以理解, 但是可以将它分割成几个逻辑部分, 查找, 替换, 再查找.
于是就可以将gsub
命令写成这样的形式:
# ruby code
def fake_gsub(string, regexp, replace)
# 查找
if string.match regexp
m = Regexp.last_match
# 替换
string = m.pre_match + replace + m.post_match
# 再查找
fake_gsub(string, regexp, replace)
else
# 找不到就停止查找, 返回字符串
return string
end
end
# example
fake_gsub("a b c", /\w/, "*")
# => "* * *"
于是对程序的理解就会至少深那么一点点吧?
那么简单的思想就先到这里, 以后可以想得更加深入一些.
逆向的前置技能
软件的正向开发
首先要会编程, 知道如何构造一个软件.
(巧了, 我们学长今天带我们看例子的时候就吐槽了好多次, 说写例子程序的人可能编程技术不太好, 写判断代码的时候写的顺序是错误的, 导致判断代码的作用几乎没有. )
知道如何编写一个软件的话, 在看汇编代码的时候也就会有一个方向去思考尝试.
汇编
一般来说, 逆向遇到的程序都是打包好的二进制可执行文件, 用反汇编程序查看可以看到里面的汇编指令. 得到程序的运行的逻辑. 为了看懂程序, 知道简单的汇编语言是有必要的.
(我在Untitled(1)里面有过简单介绍. )
这里简单补充一些x86
汇编的一些之前没有很关注的点:
- 特殊寄存器
REFLAGS
, 里面有:ZF
Zero Flag, 在运算结果为0
的时候被设置为1
OF
Overflow Flag, 在运算结果溢出的时候被设置为1
- 等等
- 特殊的一些数据移动指令 (虽然很少见到就是了)
xchg
交换寄存器bswap
交换比特xadd
交换并相加
- 特殊的一些栈操作指令
pushad
保存通用寄存器popad
恢复通用寄存器- 上面的那一对往往会在需要脱壳程序里面出现, 就是程序在壳外
pushad
一下, 然后解开壳, 这样以后又popad
, 接下来正常运行. 往往会有那么一个逻辑.
- 调用和返回指令
call
调用函数ret
从函数返回 (在PWN的Overflow里面很好用)iret
从中断返回int
软件中断 (比如在混淆调试的方式里面, 常常有int 3
来扰乱调试器调试. )
- 函数调用(参数如何传递的)约定 (虽然好像不必记忆, 了解即可. )
cdecl
参数全部通过栈传递, 调用者清理参数thiscall
前两个参数放bx
,cx
, 其余参数放在栈里面, 调用者清理fastcall
第一个参数放cx
里面, 调用者清理stdcall
被调用者清理- linux
ax
放系统调用号,di
,si
,dx
, (r10
,r9
,r8
)放参数- linux x64:
rdi
,rsi
,rdx
,rcx
,r8
,r9
放参数, 被调用者清理
- windows x64:
rcx
,rdx
,r9
,r8
, 被调用者清理
- 函数调用的局部变量是放在栈上的
假如有一个函数是这样的:func(arg1, arg2, arg3, ..., argn)
那么在传递参数的时候, 前面的几个参数会被用寄存器传递(从左到右), 然后后面的参数数量比较多, 没法用寄存器来传递, 就会用栈来传递(从右到左)
但是传递归传递, 最终都还是要写入函数所在的栈里面的(从右到左, 这样的原因是因为栈是递减的) - x86函数模版
push ebp mov ebp, esp sub esp, stack_size ; 将传递的参数写到栈里面 mov [ebp + offset], rdx ; 瞎写的, 不知道对不对, 大概这个意思吧 ; ... ; mov esp, ebp pop ebp ; 默认的返回值是通过eax返回的 ret
多种编译器的输出pattern
不同的编译器的输出是有一定的模式可以找的, 这样的话就有助于看代码的时候很快就可以聚焦到真正有效的代码, 而不是在外面的模式的代码上迷幻浪费时间.
(虽然我现在接触的种类有限, 大部分都是gcc
的编译结果. )
比如在gcc
里面, 在函数的开头就经常有push ebp
等的东西, 几乎可以忽略.
还有就是在vc
里面, 常常有成堆的jmp
命令出现, 也是一个很好的例子.
以后多见应该可以增加经验, 现在的我还是too young too simple了.
操作系统, 计算机组成原理
…
密码学入门
一般逆向注册机的问题, 就需要解决一个破解密码的问题, 于是就需要一些简单的密码学知识. (虽然和真的密码学比起来还是很简单的. )
(在python
里面有一个叫z3
的很棒的库可以用来解决类似的问题,
我觉得我可以以后试试能不能也实现一个类似的, 听起来像是在做梦. )
耐心
嗯, 确实很重要.
逆向的常规思路
准备工作
找函数
这种一般是通过程序的运行表现来找主要的工作函数. 因为一个程序可以有一些标志性的输出, 往往就可以通过这种方式来得到主要的函数的位置.
或者也可以利用调试器来找.
抄写
就是把难看的汇编代码用自己比较熟悉的语言整理逻辑之后重新写成一个简单的代码.
这个时候可以利用IDA的辅助.
写逆
根据抄写后的代码, 比如说里面就有一个加密的模型, 然后就根据这个加密的模型写一个解密的代码.
调试器以及反调试的问题
这个时候, 作为mac用户, 我留下了嫉妒的泪水…
(不得不说, windows下的好东西真是多, 比如x64dbg, ida pro也很好得到)
那么我就要学习一下如何使用一些(我能用的)好用的调试器了, 首先是gdb, 以后慢慢学, (虽然现在我觉得我用的那些足够了, 但是今天学习让我认识到了不足, 只用我会的那么点还是不足以达到解决学长给的题目的水平的), (据说r2也不错, 不了解).
然后静态的东西, ida pro好耶.
调试器的利用基础
一个小目标: 学会这些, (虽然现在还不全会, 更不用说像学长那样玩得那么溜).
(我还是以gdb
为主吧… )
- 暂停和运行
ctrl-C
/c
- 单步
- 步过
n
/ni
- 步进
s
/si
- 步过
- 断点
- 指令断点
b
- 内存断点
watch
/rwatch
- 硬件断点
- 指令断点
- 查看内存
x
IDA 的一些技巧
只是零散地记录一下而已.
F5
可以将代码输出成一个类似于C代码一样的东西- 在IDA里面可以选择某一段的数据并将其标记为特定的类型, 比如选中一段代码, 右键取消掉类型, 然后可以重新设置新的类型
- 在IDA里面可以修改符号的名字, 为了更好看
- 然后对于F5输出的伪C代码, 可以通过修改参数的类型, 符号等方式帮助程序更好地解析, 没准最后会好看一点.
反调试
假如我能让一个东西被轻松调试的话, 那么这个东西就是我砧板上的鱼肉了. 所以程序为了避免成为砧板上的鱼肉, 自然要有各种方法进行挣脱.
一种思路就是检查一下自己是不是被调试了, 假如被调试的话就不会正常工作.
- 扫描进程
看看有没有名字十分可疑的进程, 比如gdb
,x64dbg
等的进程, 有的话就可以认为自己被调试了
这种可以通过./program &
先得到其PID, 然后再在gdb
里面用at PID
的方式调试, 因为一开始运行的时候并没有gdb
, 所以可以绕开. (或者就是直接把这个判断绕开? ) - 检测特殊标志
在windows里面有APIIsDebuggerPresent()
,CheckRemoteDebuggerPresent()
,NtGlobalflag()
等, 可以判断自己是不是在被调试.
可以通过hook这些API, 或者在运行的时候把标识给强制写掉, x64dbg的插件ScyllaHide可以帮助绕过检测(手头没有, 没法尝试) - 检测程序运行速度
因为被调试的程序运行速度会很慢
可以优化或者直接把时间判断的条件语句给干掉 - 抛出异常
因为异常会被调试器捕捉到, 然后再会决定要不要交还给程序, 于是程序可以通过抛出一个异常, 假如不可以被自己接受到, 那么就可以认为自己的异常被调试器拦截了, 自己就在被调试了.
可以尝试让调试器把异常交还给程序. 关于在gdb
里面的一些介绍:info signals
或info handle
会显示信号handle <signal_name> <descriptions>
会设置对应的信号出现的对待方式, 具体的<descriptions>
有nostop
,stop
,print
,noprint
,pass
,noignore
等
- 代码段检测
用到的原理就是调试器的断点一般就是int 3
, 然后软件可以计算一下自己代码段里面的int 3
的数量然后再和原来应该有的数量相对比, 假如不一样(多了)的话, 说明有调试器在自己里面设了断点. 自己就被调试了.
在调试器里面改变默认断点类型. 变成long int 3
,ud2
等. - 自检验
另外一种想法就是你调试任你调试, 但是我把代码写得让你很难调试, 扰乱正常的调试.
- 异常
比如说塞一堆的int 3
让调试变得很麻烦 - 花指令
就是花里胡哨的指令, 从这里跳转到那里然后又跳转回来, 反复横跳, 让看代码的人云里雾里 - 自调试
有种走别人的路, 让别人无路可走的味道. 因为没有办法同时存在两个调试器, 然后这种自调试还可以利用调试的特性, 比如说程序跳出一个中断, 然后自己的调试器捕捉到这个中断后处理数据再返回给程序, 就把原来一个程序能干的事分成了两个程序干的事情, 增加了困难.
逆向的高级操作
vm 类型
有点像是自己用代码编写了一个虚拟的机器来执行命令, 有点像是这样的东西:
operator = [
-> x {
# do sth
},
# ...
-> x {
# do other thing
}
]
codes = [[0, 1], [2, 3]] # ...
codes.each do |op, arg|
operator[op].call arg
end
(虽然上面的东西没有做条件转换之类的东西, 因为没有写ip
指针, 是个很弱的vm. )
这种类型就是自己也写一个vm, 然后区别就是这个vm的思路就是用来输出一些好看的代码, 让这个输出的代码可以用别的方式运行, 于是就可以知道里面程序的运行方式了.
脱壳
广义来说壳主要分两种: 压缩壳与加密壳, 即缩小文件体积和加密代码以提高逆向难度. UPX, ASPack等壳均属于压缩壳, 其可将文件体积缩小50%-70%.
来自网络
简单的UPX脱壳
UPX壳的程序的特征就是在代码里面有UPX
的标志, 比如什么UPX0
, UPX1
等.
网络上有专门的程序用来脱壳.
但是没有接触过, 所以很难说怎么做, 以后接触了再说.
无法调试, 可以trace
这个没有详细介绍, 网上的也看不太懂, 所以以后遇到了再说.
无法调试, 可以改源码
掌握一门叫做瞎几把乱改的技术…
后记
虽然后面的写得很潦草, 因为没有实操经验.
不过这篇文章就当作是对RE有哪些可以用的方法进行一个记录吧…
以后接触的多了估计会好一点…