这次用OD分析Brad Soblesky.2的算法分析,并一步步写对应的注册机。
先不考虑有无壳的问题,着重在分析思路。
0x01 信息收集
先简单运行程序试试功能,直接输入1/2
获得提示信息1:user name must have at least 5 characters.
再输入正常点的用户名密码
获得提示信息2:incorrect!!, try again.
得出结论程序的功能为:
-
用户名长度至少要五个字符
-
用户名和注册码有对应关系(后续分析发现注册码并不是写死,而是通过算法计算出来)
0x02 程序分析
先找到主要的处理逻辑,提示信息可以用OD本身的功能或插件直接搜索
查找->所有参考文本字串 //OD自带
Ultra String Reference->Find ASCII //插件查找
-
双击跳转到对应汇编代码,F2下断点,F9运行输入1/2 执行,到断点处
在 00401575 处看到有 cmp 5,提示词又是用户名要5个字符以上,猜测此处是做用户名长度判断,[ebp-1C] 是用户名长度
在 00401579 处跳转到 004015BE,跳过了用户名长度不够的提示信息,猜测跳转后才是真正的处理逻辑,即 004015BE 处的后续才是具体的注册码处理逻辑。
-
在 004015BE 处下断点输入 admin/123456 执行,满足用户名长度后被断下来,继续分析:
来捋一捋这一段在干嘛,边执行边猜功能。
-
有个很明显的红箭头跳转,从 00401618 处跳转到前面 004015C7,猜测是个循环,中间还有个 jge 跳转到循环外的地址,猜测是break作用
-
[ebp-20] 处初始值为0,然后第一次循环从 004015C5 跳转到 004015D0,不执行加1。后续每次循环 [ebp-20] 都加1
-
jge 是基于 [ebp-1c] 和 [ebp-20] 的对比,查看内存 [ebp-1c] 值为5,猜测是用户名长度,因此此处的循环的次数是用户名的长度
那我们接下去看看每次循环都干了什么。执行到 004015DC ,查看内存 [ebp-14]对应的值
看起来存的是个地址,继续看存的什么
这不就是我们的用户名吗,因此 [ebp-14] 存的就是用户名的地址
在 004015E4 处有个 call,直接看汇编代码看不太出干嘛的,但执行完 call 后,ECX是输入的用户名,下一行代码为 movsx edx, al,而当前 al 的值为 61(此处是十六进制,61H对应的字符就是a),猜测此处这个 call,就是根据从0开始的序号取出对应用户名的字符值。
-
此处其实就当前的信息继续分析,看不出所以然,也不知道什么时候对应用户名的注册码才出来,先继续往下走
跳出那多次循环后往下执行,执行后 00401627 这个 call 后,发现 EAX 出现了一串值,猜测这就是序列号
猜测正确,此处的函数调用 push了EAX和%lu,后者是用于格式化字符串,即当时的EAX是通过用户名计算出来的序列号值,而EAX是刚结束循环后的 [ebp-10] 。那可以大胆猜测,上面的循环计算,最后就是把序列号放到 [ebp-10],那我们再重新看下循环内的 [ebp-10] 。
-
断点下在循环初始处,执行到 [ebp-10]
经过循环和多次执行发现,每次循环刚开始,[ebp-10] 的值都是81276345,猜测这是一个种子值。然后在每次循环过程中经过各种计算,[ebp-10] 的值一直在变化,到循环结束后[ebp-10] 的值就是序列号的十六进制,再转换成十进制就是我们应该输入的序列号。
0x03 注册机的编写(一)
通过上面分析可知,算法就是这个循环,循环完后序列号的十六进制已经出来,后续只是做个格式转换。
此处投机取巧,直接用汇编写注册机,就不用理解算法再重新实现了。但随之问题也来了,要分析那些 ebp对应的值是什么,提前初始化好,根据前面的分析,汇总如下:
[ ] : 用户名地址
[81276345 ] : 初始化种子值为
[ ] : 用户名长度
[0开始到len(user)-1 ] : 索引/序号,从
利用插件把这个循环的代码复制出来,这里用的是 Asm2Clipboard 插件
需要复制的是这段汇编代码
利用插件复制出来是这样的
mov dword ptr [ebp-20], 0
jmp L005
L002:
mov edx, dword ptr [ebp-20]
add edx, 1
mov dword ptr [ebp-20], edx
L005:
mov eax, dword ptr [ebp-20]
cmp eax, dword ptr [ebp-1C]
jge short 0040161A
mov ecx, dword ptr [ebp-20]
push ecx
lea ecx, dword ptr [ebp-14]
call 00401900
movsx edx, al
mov eax, dword ptr [ebp-10]
add eax, edx
mov dword ptr [ebp-10], eax
mov ecx, dword ptr [ebp-20]
shl ecx, 8
mov edx, dword ptr [ebp-10]
xor edx, ecx
mov dword ptr [ebp-10], edx
mov eax, dword ptr [ebp-20]
add eax, 1
mov ecx, dword ptr [ebp-1C]
imul ecx, dword ptr [ebp-20]
not ecx
imul eax, ecx
mov edx, dword ptr [ebp-10]
imul edx, eax
mov dword ptr [ebp-10], edx
jmp L002
可以看到,其中 [ebp-10],[ebp-14],[ebp-1C] 的值需要我们去定义,这部分前面已经写过,直接替代
[ebp-14] : 用户名地址
[ebp-10] : 初始化种子值为81276345
[ebp-1C] : 用户名长度
[ebp-20] : 索引/序号,从0开始到len(user)-1
用汇编写个简易的窗口程序,源码如下
;Register.Asm
.386
.model flat, stdcall ;32 bit memory model
option casemap :none ;case sensitive
include Register.inc
.data
g_szCode db 256 dup(0) ;用于存储注册码
g_szFmt db "%lu", 0 ;用于格式化字符串
.code
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke InitCommonControls
invoke DialogBoxParam,hInstance,IDD_DIALOG1,NULL,addr DlgProc,NULL
invoke ExitProcess,0
;########################################################################
OnBtnRegister proc hWin:HWND
LOCAL @dwUserLen:DWORD
LOCAL @dwIdx:DWORD
LOCAL @szBuff[256]:BYTE
LOCAL @dwSeed:DWORD
;getuser
invoke GetDlgItemText, hWin, EDT_USER, addr @szBuff, 256
mov @dwUserLen, eax ;此处eax为DialogBoxParam返回的用户名长度
mov @dwSeed, 81276345h ;注册序列号初始种子值
mov @dwIdx, 0
jmp L005
L002:
mov edx, @dwIdx ;index
add edx, 1
mov @dwIdx, edx
L005:
mov eax, @dwIdx
cmp eax, @dwUserLen
jge LEND
;此处代码里为函数调用,进入函数摘取关键操作
;mov ecx, dword ptr [ebp-20]
;push ecx
;lea ecx, dword ptr [ebp-14]
;call 00401900
mov ecx, @dwIdx ;index
lea eax, @szBuff
mov al, [eax+ecx];getchar
movsx edx, al
mov eax, @dwSeed
add eax, edx
mov @dwSeed, eax
mov ecx, @dwIdx
shl ecx, 8
mov edx, @dwSeed
xor edx, ecx
mov @dwSeed, edx
mov eax, @dwIdx
add eax, 1
mov ecx, @dwUserLen
imul ecx, @dwIdx
not ecx
imul eax, ecx
mov edx, @dwSeed
imul edx, eax
mov @dwSeed, edx
jmp L002
LEND:
invoke wsprintf, offset g_szCode, offset g_szFmt, @dwSeed
invoke SetDlgItemText, hWin, EDT_CODE, offset g_szCode ;return value in code
ret
OnBtnRegister endp
DlgProc proc hWin:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
mov eax,uMsg
.if eax==WM_INITDIALOG
.elseif eax==WM_COMMAND
mov eax, wParam
.if ax == BTN_REGISTER
invoke OnBtnRegister, hWin
.endif
.elseif eax==WM_CLOSE
invoke EndDialog,hWin,0
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
DlgProc endp
end start
;Register.Inc
include windows.inc
include kernel32.inc
include user32.inc
include Comctl32.inc
include shell32.inc
includelib kernel32.lib
includelib user32.lib
includelib Comctl32.lib
includelib shell32.lib
DlgProc PROTO :HWND,:UINT,:WPARAM,:LPARAM
.const
IDD_DIALOG1 equ 101
EDT_USER equ 1002
EDT_CODE equ 1003
BTN_REGISTER equ 1001
;#########################################################################
.data?
hInstance dd ?
;#########################################################################
执行试试,随便输入用户名
0x04 注册机的编写(二)
python实现注册机
本人拙劣,直接照着算法的一步步操作复刻,汇编里的循环算法图已经在上边就不贴了。
说下编写过程中的几个点:
-
内存中是以二进制进行操作,位与、异或、取反等操作,代码中操作时,操作数是十进制或二进制结果相同,直接操作就行,输出时默认转换为十进制
-
内存中负数的表示是补码,代码中计算出来的是值,所以如果是负值要转换为对应的补码。
def limit(user_name):
if len(user_name) < 5:
print("user name must have at least 5 characters.")
exit()
#操作数转换成十进制/十六进制进行操作结果相同
def pojie(user_name):
seed = 0x81276345
length = len(user_name)
for i in range(length):
seed = hex(seed + ord(user_name[i]))
tmp_index = i << 8
seed = int(seed,16) ^ tmp_index
tmp_index = i + 1
tmp = i * length
tmp = ~tmp & 0xFFFFFFFF
tmp_seed = tmp_index * tmp
seed = (seed * tmp_seed) & 0xFFFFFFFF
return seed
if __name__ == '__main__':
user_input = str(input("请输入用户名:"))
limit(user_input)
register_code = pojie(user_input)
print("用户名: "+user_input+",对应的注册是", register_code)
声明:该公众号大部分文章来自作者日常学习笔记,也有部分文章是经过作者授权和其他公众号白名单转载,未经授权,严禁转载,如需转载,联系开白名单。
请勿利用文章内的相关技术从事非法测试,如因此产生的一切不良后果与本公众号无关。
✦
✦
原文始发于微信公众号(白帽学子):Brad Soblesky.2 逆向分析&注册机编写