main 函数及其参数#
第 17 章中,通过远程调试的方式对程序进行了脱壳,此处实验的程序来自第 17 章脱壳后的程序。这里直接用作者给的程序,而不加载自己脱壳的程序。
此章对上述程序进行分析并编写注册机。
复习下程序,32 位架构。
打开 ida 加载脱壳后的程序,首先查看字符串。
此程序运行后,在命令行输出Pone un User
,英文是Enter a user
。
单击Pone un User
,跳转到程序具体位置。
按 “X” 键查看字符串引用
点击 ok,进入引用具体位置
上图中,在以 ebp 为基址的函数中,首先执行push ebp
指令将上一层函数的 ebp 值保存到栈上,然后执行mov ebp,esp
,将 ebp 作为计算函数本身的局部变量、参数、缓存区的基准。
之后再执行 sub esp,94h 指令从 ebp 的基址开始获取 0x94 个字节用于存储局部变量和缓存区
对函数任意变量或者参数双击,ida 会显示函数的静态栈视图,下图点击 var_4 的显示内容,静态栈视图
上图中,该函数有两个参数,也就是函数的默认参数 argc 和 argv,在函数调用之前通过 push 指令传到了栈上,并且显示在返回地址(r)的下方。(注:书中这里讲解一开始没有参数,后面将 sub_201070 重命名为 main 函数后才显示了这两个参数)
这两个参数虽然显示了,但是未被引用,所以也可以当成无参
security cookie 等局部变量#
回到静态栈视图,S 表示STO RED EBP
,是通过push ebp
指令保存的上一层函数的 ebp,再上方就是局部变量空间,一般都有一个var_4
变量来保护栈,防止缓寸区溢出。
var_4
有两处引用,第一处是函数的起点,用于在栈上存储一个安全口令security cookie
。
这是一个随机数,在函数刚运行时,跟 ebp 进行异或运算后,结果保存到var_4
中
上图中是另一处引用,var_4
的值传给了 ecx,然后在与 ebp 进行异或运算,恢复原始值,然后再调用另一个函数检查其值
点击@__security_check_cookie@4
,进入到这个函数内部,如下图
上图中,如果值一切正常,则返回,如果 ecx 中不是_security_cookie
的值,将会终止进程,而不是执行返回,这只会发生在内存溢出将 var_4 覆盖的时候
按 N 键,现在将var_4
重命名为COOKIE
下面的函数命名为check_cookie
修改后最终如下:
回到函数起始点处,有两个变量还不清楚作用,一个是var_90
的初始值为0
,另一个是size
,初始值为8
,如下图
如下图,查看var_7d
的引用,在某个函数被调用后,var_7d
的值被传入了 AL 寄存器,然后将值传给了 EDX 寄存器,检查其值是否为 0,确定输出 Good reverser 或者 Bad reverser,所以这是一个单字节的变量,重命名 SUCCESS_FLAG,也就是最终计算的 flag 值。
按 N 重命名对var_7d
进行重命名
将左侧注册成功的代码块改成绿色,右侧注册失败改成黄色,上图中,只要修改 JZ 这个指令,也就能实现成功注册。
用户名和密码处理#
上图中,有另一个变量var_90
,初始值为 0,程序将 BUF 中的每个字节读取出来,传入 edx(在 0x231109 指令处),并和var_90
相加,第一次循环中加的是 0,结果再保存到var_90
,那么之后的循环中 edx 加的是之前所有字节的和,将var_90
重命名为SUMMARY
。如下图
上图中var_84
是循环的计数器,计数器加 1,只循环前 4 个字节,var_84
大于等于 4 会跳出循环。将其命名为COUNTADDR
。
为了让这个循环更好的显示,按住 ctrl 键,在这三个循环上单击,右键选择 group nodes
最终显示如下
如果想取消编组,则右键选择 unhide group 或者点击图标
循环之后,获取密码
程序使用 Buf 存放获取的密码,因为程序已经保存了用户名前 4 个字节的和。
上图中,程序接着调用strlen()
函数获取密码的长度,如果长度小于 4,那么就退出程序,如果不小于 4,那么程序进入右边绿色代码块。
右边绿色代码块中,使用atoi
函数将密码字符串转成一个十六进制数,这个函数类似 python 中的hex()
然后十六进制密码和0x1234
进行异或运算,然后保存到 edx 变量中。
上图中,将密码与0x1234
异或的结果及用户名前 4 个字节的和传入sub_231010
函数中进行比较,根据比较的结果决定输出结果。
sub_231010
函数的参数中,arg_4 参数是首先传到栈上的,可以对这两个参数进行重命名。
右键单击set type
,ida 会根据参数识别函数原型。
函数声明如下图所示
ida 自动注释与传入参数一致
在sub_231010
内部,在两个参数比较之前,密码变量传入了 eax,执行了 shl eax,1,相当于乘以 2
最后进行比较,如果这两个数相等,程序转向绿色代码并将 1 传入 al,跳出循环,然后传入SUCCESS_FLAG
,最终决定是否注册成功。
算法总结#
程序首先将用户名的前 4 个字节相加
密码转换为 16 进制后和 0x1234 进行异或运算,结果再乘以 2
下面构建一个基于用户名的公式,注册机也是基于这一点,根据用户名计算密码
x = password (16进制数)
(x ^ 0x1234)*2 = SUCCESS_FLAG
那么x ^ 0x1234 = (SUCCESS_FLAG/2)
由于异或运算可逆。
A ^ B = C
A = B ^ C
那么x = (SUCCESS_FLAG/2) ^ 0x1234
使用 python 编写注册机#
假如用户输入 “pepe”,长度小于 8 字节,那么字节之和就可以按如下方式计算
sum = 0
user='pepe'
length=len(user)
for i in range(length):
sum+=ord(user[i])
print(hex(sum))
那么 pepe 前 4 个字节之和就是:
sum = 0
user='pepe'
for i in range(4):
sum+=ord(user[i])
print(hex(sum))
接下来编写一个适用于任何合法用户名的注册机
sum = 0
user=input("input user name:")
length=len(user)
for i in range(4):
sum+=ord(user[i])
if(length>=4):
print(hex(sum))
使用 input () 函数获取命令行输入,但是现在上述代码适用于任何合法的账户
根据之前总结的公式x = (SUCCESS_FLAG/2) ^ 0x1234
,将用户名的计算结果除以 2,在和 0x1234 进行异或运算,找到十六进制的密码
sum = 0
user=input("input user name:")
length=len(user)
for i in range(4):
sum+=ord(user[i])
print(user)
if(length>=4):
print("success_flag",hex(sum))
password = (sum//2)^0x1234
print("password:",password)
目前注册机已经完成,密码也从 16 进制转成了 10 进制,python 的默认输出就是 10 进制
上述代码当输入用户名字符数达到 8 个字符时程序会崩溃,因为字符串最后还有一个终止符 null,连同终止符在内不能超过 8 个。当然输入 7 个字符是没问题的。
最后还有一个问题就是如果 4 个字符的和是个奇数。因为在比较之前密码是乘以 2 的,结果永远是一个偶数,所以这种情况就没有解。
加个检查流程
sum = 0
user=input("input user name:")
length=len(user)
for i in range(4):
sum+=ord(user[i])
print(sum)
print(user)
if(sum%2==0):
print("偶数")
if(length>=4):
print("success_flag",hex(sum))
password = (sum//2)^0x1234
print("password:",password)
else:
print("奇数")
检查用户名字节加和 sum 除以 2 的余数,如果不等于 0,sum 是奇数,对应的密码是无解的。
奇数的情况
偶数的情况
这样,注册机就完成了