好友
阅读权限40
听众
最后登录1970-1-1
|
楼主|
mycsy
发表于 2008-5-22 19:44
加载后,看到
00401368 >/$ 55 PUSH EBP <------- 我们在这里
00401369 |. 8BEC MOV EBP,ESP
0040136B |. 6A FF PUSH -1
0040136D |. 68 E8404000 PUSH ncrackme.004040E8
00401372 |. 68 9C1E4000 PUSH ncrackme.00401E9C ; SE handler installation
00401377 |. 64:A1 00000000 MOV EAX,DWORD PTR FS:[0]
0040137D |. 50 PUSH EAX
0040137E |. 64:8925 000000>MOV DWORD PTR FS:[0],ESP
00401385 |. 83EC 58 SUB ESP,58
00401388 |. 53 PUSH EBX
这样的一大堆,便是程序开始的地方。一般程序的入口点都是 40xxxx 这样的,如果程序加了壳,入口点会不同。
程序的入口点,是可以由作者自行设定的,没有特别规定
在程序开始段常见的 API : KERNEL32.GetVersion, GetCommandLineA, GetStartupInfoA
我们在这片密麻麻的字海里,是很难找到我们要破解的地方,我们第一步是要找出检查序号的部份。程序多数会用 GetWindowTextA,
GetDlgItemTextA 这类 API 来得到文字方块里的字符串。此外,当我们输入错误序号的时候,程序会弹出一个失败信息,这个信息由
MessageBoxA 所提供。假若我们要分析算法,便需要在 GetWindowTextA 下断,一步一步跟踪。现在我们想从内存中找出序号,比较
快速的方法是在 MessageBoxA 下断。
在 OD 上方的 Plugins ,选 Commandline ,这里可以像 softice 一样输入命令行
设下一个断点 :
bp MessageBoxA
输入这行后,当程序使用 MessageBoxA ,程序便会断下
现在,我们把程序正常运行 (按 F9),输入一个名字,我在里输入了 riijj ,在序号那行输入了 AAAABBBBCCCC
按下 "Register" ,这时候程序遇到断点,停在以下地方
77E16544 > 55 PUSH EBP <---停在这里
77E16545 8BEC MOV EBP,ESP
77E16547 51 PUSH ECX
77E16548 833D 1893E477 00 CMP DWORD PTR DS:[77E49318],0
77E1654F 0F85 EA220100 JNZ USER32.77E2883F
77E16555 6A 00 PUSH 0
77E16557 FF75 14 PUSH DWORD PTR SS:[EBP+14]
77E1655A FF75 10 PUSH DWORD PTR SS:[EBP+10]
77E1655D FF75 0C PUSH DWORD PTR SS:[EBP+C]
77E16560 FF75 08 PUSH DWORD PTR SS:[EBP+8]
77E16563 E8 04000000 CALL USER32.MessageBoxExA
77E16568 C9 LEAVE
77E16569 C2 1000 RETN 10
77E1656C > 55 PUSH EBP
看看 OD 的上方,写着 "CPU - main thread, module USER32" ,这说明了我们身处 user32.dll 的领空里,这是系统的程序
部份,我们是不会修改这里的。
(注 : 如果你把文字窗口向上卷的时候,发现 OD 的编码出现不正常现象 (例如刚刚那行 77E16544 的上方变成了 ??? ) ,
这可能是 OD 的对位出错)
我们要返回 crackme 的领空里,有几个方法
1. 不停接 F8 ,一步一步地执行直至程序遇上 retn ,这是返回指令,它会带我们回去
2. 按 Ctrl + F9 ,这样 OD 会不停执行,直至遇到 retn 停下
3. 按一下返回的 retn ,再按 F4 ,程序会执行到光标所在的地方
4. 打开 OD 的 call stack window,看看我们从那里飞来,便设一个断点在那地方,之后 F9 运行
我们按一下 F2 清除断点,再按一下 77E16569 (retn) 那行,按 F4 执行到那里,再按一下 F7 进入 retn。
00401050 . 817C24 08 1101>CMP DWORD PTR SS:[ESP+8],111
00401058 . 75 74 JNZ SHORT ncrackme.004010CE
0040105A . 8B4424 0C MOV EAX,DWORD PTR SS:[ESP+C]
0040105E . 66:3D EA03 CMP AX,3EA
00401062 . 75 42 JNZ SHORT ncrackme.004010A6
00401064 . E8 C7010000 CALL ncrackme.00401230
00401069 . 85C0 TEST EAX,EAX
0040106B . 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
0040106D . 68 80504000 PUSH ncrackme.00405080 ; |Title = "ncrackme"
00401072 . 75 1B JNZ SHORT ncrackme.0040108F ; |
00401074 . A1 B8564000 MOV EAX,DWORD PTR DS:[4056B8] ; |
00401079 . 68 64504000 PUSH ncrackme.00405064 ; |Text = "Registration successful."
0040107E . 50 PUSH EAX ; |hOwner => 001C0218 ('Newbie smallsize crackme -
v1',class='myWindowClass')
0040107F . FF15 C0404000 CALL DWORD PTR DS:[<&USER32.MessageBoxA>>; \MessageBoxA
00401085 . E8 A6020000 CALL ncrackme.00401330
0040108A . 33C0 XOR EAX,EAX
0040108C . C2 1000 RETN 10
0040108F > 8B0D B8564000 MOV ECX,DWORD PTR DS:[4056B8] ; |
00401095 . 68 50504000 PUSH ncrackme.00405050 ; |Text = "Registration fail."
0040109A . 51 PUSH ECX ; |hOwner => 001C0218 ('Newbie smallsize crackme -
v1',class='myWindowClass')
0040109B . FF15 C0404000 CALL DWORD PTR DS:[<&USER32.MessageBoxA>>; \MessageBoxA
004010A1 . 33C0 XOR EAX,EAX <--------------- 我们停在这里
004010A3 . C2 1000 RETN 10
004010A6 > 66:3D EB03 CMP AX,3EB
004010AA . 75 22 JNZ SHORT ncrackme.004010CE
004010AC . A1 C0564000 MOV EAX,DWORD PTR DS:[4056C0]
004010B1 . 85C0 TEST EAX,EAX
004010B3 . 74 19 JE SHORT ncrackme.004010CE
我们看见了这里是 MessageBox 跳出的程序,这里是成功信息和失败信息的地方。 (读者可能已经大叫,只要把跳往失败信息的跳转修改
一下,便可以成功爆破) 。这是对的,假如我们要爆破它,只要把 00401072 的 JNZ (jump if not zero),修改成 JZ 便可以。
那么,假若我们不想修改软件,而是找出正确的序号,那又怎办呢 ? 我们需要往那个 jump 的上面看看。在 00401069 这里有一个 TEST
,这就是进行比较的指令,它的结果用在其后的 jnz 上。
这句 TEST EAX,EAX 是检查 EAX 是否等于 0 的典型语句,假如 EAX 不等于 0,JNZ 便会跳。我们就是不想它跳,所以我们要找出让 EAX
等于 0 的条件。在 TEST 的上方是 CALL ncrackme.00401230,这个 CALL 的返回值就是 EAX 的值。
我们按右键选 Go to,在 expression 输入 00401230 这个位置
我们来到了检查序号的地方,一看之下便感到这里是一堆很麻烦的运算。今次我们的目的是检出一个给
自己用的序号,我们有没有方法可以从算法中看出序号呢 ?
[PART. 2 第二部分]
按 F7 单步跟踪,细心观察它在干甚么
00401230 /$ 8B0D BC564000 MOV ECX,DWORD PTR DS:[4056BC]
00401236 |. 83EC 30 SUB ESP,30 // 在 stack 划出空间,作为变量
00401239 |. 8D4424 00 LEA EAX,DWORD PTR SS:[ESP]
0040123D |. 53 PUSH EBX
0040123E |. 56 PUSH ESI
0040123F |. 8B35 94404000 MOV ESI,DWORD PTR DS:[<&USER32.GetDlgIte>; USER32.GetDlgItemTextA
00401245 |. 6A 10 PUSH 10 ; /Count = 10 (16.)
00401247 |. 50 PUSH EAX ; |Buffer // <---- 注意这里,
00401248 |. 68 E8030000 PUSH 3E8 ; |ControlID = 3E8 (1000.) // 可以看出存放字符串的地方
0040124D |. 51 PUSH ECX ; |hWnd => NULL
0040124E |. 33DB XOR EBX,EBX ; |
00401250 |. FFD6 CALL ESI ; \GetDlgItemTextA
00401252 |. 83F8 03 CMP EAX,3 // 找到注册名字,如果字名的长度不小于 3,便跳,否则便完结
00401255 |. 73 0B JNB SHORT ncrackme.00401262
00401257 |. 5E POP ESI
00401258 |. B8 01000000 MOV EAX,1
0040125D |. 5B POP EBX
0040125E |. 83C4 30 ADD ESP,30
00401261 |. C3 RETN // 如果来了这里,便完结了
00401262 |> A1 BC564000 MOV EAX,DWORD PTR DS:[4056BC] // 从 JNB 来了这里
00401267 |. 8D5424 28 LEA EDX,DWORD PTR SS:[ESP+28]
0040126B |. 6A 10 PUSH 10
0040126D |. 52 PUSH EDX // <----- 注意这里,可以看出存放序号字符串的地方
0040126E |. 68 E9030000 PUSH 3E9
00401273 |. 50 PUSH EAX
00401274 |. FFD6 CALL ESI // 再用 GetDlgItemTextA ,得到序号
00401276 |. 0FBE4424 08 MOVSX EAX,BYTE PTR SS:[ESP+8] // 把名字的第一个位,放入 EAX
0040127B |. 0FBE4C24 09 MOVSX ECX,BYTE PTR SS:[ESP+9] // 把名字的第二个位,放入 ECX
00401280 |. 99 CDQ // 把 EAX 扩展,成为 EDX:EAX 的 QWORD(64 位长)
00401281 |. F7F9 IDIV ECX // 把 EDX:EAX 除以 ECX,余数放在 EDX
00401283 |. 8BCA MOV ECX,EDX
00401285 |. 83C8 FF OR EAX,FFFFFFFF // EAX = 0xffffffff
00401288 |. 0FBE5424 0A MOVSX EDX,BYTE PTR SS:[ESP+A] // 把名字的第一个位,放入 EDX
0040128D |. 0FAFCA IMUL ECX,EDX // 把刚才的余数乘以 EDX
00401290 |. 41 INC ECX // ECX 增加 1
00401291 |. 33D2 XOR EDX,EDX // EDX = 0
00401293 |. F7F1 DIV ECX // 以 0xffffffff 除以 ECX
以 C 语言表达,想象成
EAX 的值: 0xFFFFFFFF / (1+(buffer[0] % buffer[1] * buffer[2])
数值相等于 4546e6
00401295 |. 50 PUSH EAX // push 了 4546e6
00401296 |. E8 A5000000 CALL ncrackme.00401340 // 进去看看
00401340 /$ 8B4424 04 MOV EAX,DWORD PTR SS:[ESP+4] // 把刚才传进来的值 (4546e6),放到 EAX
00401344 |. A3 AC504000 MOV DWORD PTR DS:[4050AC],EAX // 又由 EAX 放到 DS:[4050AC]
00401349 \. C3 RETN
这两行把上面的运算结果放到 DS:[4050AC] 去,不明白原因,继续跟下去
回到外面
0040129B |. 83C4 04 ADD ESP,4 // 清理 stack
0040129E |. 33F6 XOR ESI,ESI // 设 ESI 为 0
004012A0 |> E8 A5000000 /CALL ncrackme.0040134A // 走进去看看
0040134A /$ A1 AC504000 MOV EAX,DWORD PTR DS:[4050AC] // 把那个值 (4546e6) 放入 EAX
0040134F |. 69C0 FD430300 IMUL EAX,EAX,343FD // EAX 乘以 343FD
00401355 |. 05 C39E2600 ADD EAX,269EC3 // 再加 269EC3
0040135A |. A3 AC504000 MOV DWORD PTR DS:[4050AC],EAX // 把结果放回 DS:[4050AC]
0040135F |. C1F8 10 SAR EAX,10 // 向右方 bit shift 10 位
00401362 |. 25 FF7F0000 AND EAX,7FFF // 再 AND 了 7fff
00401367 \. C3 RETN // 得到一个古怪的值,返回了
没有甚么特别,只是一大轮运算,得到一个新的值 3add
返回外面
004012A5 |. 99 |CDQ // 把那个值扩展成 EDX:EAX
004012A6 |. B9 1A000000 |MOV ECX,1A // ECX = 1a
004012AB |. F7F9 |IDIV ECX // 把 EDX:EAX 除以 1a
004012AD |. 80C2 41 |ADD DL,41 // 把余数加 41,说起来,41 是 ASCII 的大写 A
004012B0 |. 885434 18 |MOV BYTE PTR SS:[ESP+ESI+18],DL // 把这个余数的下 8 位,放入 SS:[ESP+ESI+18] 的地方
004012B4 |. 46 |INC ESI // ESI 增加 1 了
004012B5 |. 83FE 0F |CMP ESI,0F // 检查 ESI 是否已经等于 0f
004012B8 |.^72 E6 \JB SHORT ncrackme.004012A0 // 跳回上面,这像是一个小型 loop
从这里的结构,我们估计 ESI 由 0 增加至 0f ,不停把一组数字放入 stack 里面,这是一个回圈十六次的 loop
要注意这些数值都加上了 41 (ASCII A),它们都是由 41 开始
我们在 OD 下方的数值窗口,把数值打开来看
0012FC34 50 49 5A 49 56 4A 45 44 PIZIVJED
0012FC3C 42 47 5A 41 50 53 49 00 BGZAPSI.
这像是一堆 ASCII 字
我们继续看
004012BA |. 57 PUSH EDI // 没有特别,只是保存 EDI,我们看见程序末段会有一个 pop edi
004012BB |. 8D7C24 0C LEA EDI,DWORD PTR SS:[ESP+C] // 从 OD 发现,SS:[ESP+C] 是注册名字位置,现在位置放入EDI
004012BF |. 83C9 FF OR ECX,FFFFFFFF // ECX = 0xffffffff
004012C2 |. 33C0 XOR EAX,EAX // EAX = 0
004012C4 |. 33F6 XOR ESI,ESI // ESI = 0
004012C6 |. F2:AE REPNE SCAS BYTE PTR ES:[EDI]
这里有一个 REPNE SCAS 的设置,它扫瞄注册名字,最多 ECX 次 (这里 ECX 是 0xffffffff 次),
或是遇上 AL 便停止 (这里 AL 是 0)
看来它在注册名字中寻找出 0 (字符串结尾位)
004012C8 |. F7D1 NOT ECX // 把 ECX 反转,变成了扫瞄的次数
004012CA |. 49 DEC ECX // ECX 减 1
现在 ECX 得到了注册名字的长度,是 6
004012CB |. 74 59 JE SHORT ncrackme.00401326 // 如果 ECX 是 0,跳到完结处
004012CD |> 8A4434 0C /MOV AL,BYTE PTR SS:[ESP+ESI+C] // 把注册名字第 esi 个位,放入 AL (刚开始是第 0 个)
004012D1 |. C0F8 05 |SAR AL,5 // 向右方 bit shift 5 位
004012D4 |. 0FBEC0 |MOVSX EAX,AL // 把 AL 扩展到 EAX
004012D7 |. 8D1480 |LEA EDX,DWORD PTR DS:[EAX+EAX*4] // EDX = EAX + (EAX * 4)
004012DA |. 8D04D0 |LEA EAX,DWORD PTR DS:[EAX+EDX*8] // EAX = EAX + (EDX * 8)
004012DD |. 8D0440 |LEA EAX,DWORD PTR DS:[EAX+EAX*2] // EAX = EAX + (EAX * 2)
004012E0 |. 85C0 |TEST EAX,EAX // 检查结果是否 0
004012E2 |. 7E 0A |JLE SHORT ncrackme.004012EE // 如果 0,便跳
004012E4 |. 8BF8 |MOV EDI,EAX // EAX 放入 EDI
004012E6 |> E8 5F000000 |/CALL ncrackme.0040134A // 很面熟,这是刚才的 call,把 EAX 进行一堆运算
004012EB |. 4F ||DEC EDI // EDI 减 1
004012EC |.^75 F8 |\JNZ SHORT ncrackme.004012E6 // 当 EDI 不等于 0 的时候,便跳上去,这是 loop
这个小型 loop 会循环 EDI 次,不停呼叫 ncrackme.0040134A,这个 call 我们刚才看过,
它会把 DS:[4050AC] 这个值改变。这样看来,随着我注册名字的位不同, EDI 的值
不同,它循环的次数便会不同
char X, Y
X = (buffer[esi] >> 5)
Y = X + (X * 4)
X = X + (Y * 8)
X = X + (X * 2)
呼叫 ncrackme.0040134A X 次
继续看下去
004012EE |> E8 57000000 |CALL ncrackme.0040134A // 再呼叫一次,现在 EAX 是返回值
004012F3 |. 99 |CDQ // 把 EAX 扩展到 EDX:EAX
004012F4 |. B9 1A000000 |MOV ECX,1A // ECX = 1a ( decimal 26)
004012F9 |. 8D7C24 0C |LEA EDI,DWORD PTR SS:[ESP+C] // 把注册名字的位置放在 EDI
004012FD |. F7F9 |IDIV ECX // EDX:EAX 除以 ECX (1a)
004012FF |. 0FBE4C34 2C |MOVSX ECX,BYTE PTR SS:[ESP+ESI+2C]
从 OD 查看,SS:[ESP+ESI+2C] 是序号字符串,这里把序号的第 esi 个字符放进 ECX
00401304 |. 80C2 41 |ADD DL,41 // 把 DL (除数的余值,低位值) 加 41
00401307 |. 0FBEC2 |MOVSX EAX,DL // 把 DL 放入 EAX
0040130A |. 2BC1 |SUB EAX,ECX // EAX 减 ECX ,得到一个新的值,放在 EAX
0040130C |. 885434 1C |MOV BYTE PTR SS:[ESP+ESI+1C],DL
从 OD 查看,SS:[ESP+ESI+1C] 是那一组特别字符的位置,
0012FC34 50 49 5A 49 56 4A 45 44 PIZIVJED
0012FC3C 42 47 5A 41 50 53 49 00 BGZAPSI.
这行把 DL 放入这里,取代原有的值
00401310 |. 99 |CDQ // 把 EAX 扩展成 EDX:EAX
00401311 |. 33C2 |XOR EAX,EDX // 把 EAX 跟 EDX 进行 XOR 合并
00401313 |. 83C9 FF |OR ECX,FFFFFFFF // ECX = 0xffffffff
00401316 |. 2BC2 |SUB EAX,EDX // 把 EAX 减 EDX
00401318 |. 03D8 |ADD EBX,EAX
把 EBX 加 EAX,这真是奇怪,那里来的 EBX ? 我们向上追查,发现 EBX 一直没有使用, EBX 在 0040124E
这一行被清空,它的初始值是 0。
0040131A |. 33C0 |XOR EAX,EAX // EAX = 0
0040131C |. 46 |INC ESI // ESI 增加 1
0040131D |. F2:AE |REPNE SCAS BYTE PTR ES:[EDI]
看看 004012F9 这一行,发现 EDI 是注册名字的位置。
现在 SCAS 扫瞄注册名字,找出 AL (0)
0040131F |. F7D1 |NOT ECX // 还记得上面的结构吗 ? 把 ECX 反转,便是注册名字的长度 + 1
00401321 |. 49 |DEC ECX // 把 ECX 减 1,现在 ECX 是注册名字长度
00401322 |. 3BF1 |CMP ESI,ECX // 比较 ESI 和注册名字长度
00401324 |.^72 A7 \JB SHORT ncrackme.004012CD // 当 ESI 小于注册名字长度,便往上跳,这是 loop
以上的结构,是一个大型的 loop,以不停增加 ESI 的值,把注册名字和序号进行运算,
其运算结果便累加到 EBX 上
00401326 |> 5F POP EDI // 这是完结部分,复原 EDI
00401327 |. 8BC3 MOV EAX,EBX // 把 EBX 的最后值,放入 EAX ,EAX 将会是这个 CALL 的返回值
00401329 |. 5E POP ESI // 复原 ESI
0040132A |. 5B POP EBX // 复原 EBX
0040132B |. 83C4 30 ADD ESP,30 // 清理 stack 空间
0040132E \. C3 RETN // 离开
还记得我们在 PART. 1 的结尾部份吗 ? 我们遇上了跳出成功注册信息的检查程序,如果这个 CALL 的返回值是 0 的话,
我们便会成功注册。我们的任务是分析上面的算法,找出一个序号,可以使返回值 (前身是 EBX) 最后的结果是 0。
用 OD 跟踪后,我们可以观察到以下特点:
1. EBX 的初始值是 0,它在每一次循环都会被增加,只会增加,不会减少
2. 如果要 EBX 的值保持是 0,那么它上面 SUB EAX,EDX 这一行的结果,必须是 0,也就是说, EAX 和 EDX 必须相等
3. 再往上看,发现 EAX 和 EDX 是由 00401310 这一行, CDQ 指令产生的。 CDQ 把原来的 EAX 扩展成 EDX:EAX (带正负值),
这个指令把 EAX 的第 31 bit 复制到 EDX 的每一个 bit 上。例如 :
假设 EAX 是 FFFFFFFB (-5) ,它的第 31 bit (最左边) 是 1,
执行 CDQ 后, CDQ 把第 31 bit 复制至 EDX 所有 bit
EDX 变成 FFFFFFFF
这时候, EDX:EAX 变成 FFFFFFFF FFFFFFFB ,它是一个 64 bit 的大型数字,数值依旧是 -5
我们发现, 00401310 和 00401311 这两行,执行了 CDQ 和 XOR EAX,EDX ,这说明了它的目的,当 EAX 的值是一个
负数,执行 CDQ 后 EDX 变成 FFFFFFFF,它再跟 EAX 进行 XOR,便会使 EAX 反转,变成一个正数。假如 EAX 本来
是一个正数,执行 CDQ 后 EDX 是 0,它再跟 EAX 进行 XOR ,没有任何效果,保留 EAX 的值
由始可见,
00401310 |. 99 |CDQ
00401311 |. 33C2 |XOR EAX,EDX
这两行其实是从 EAX 得出 "绝对值"。如果 EAX 是 -5,便会变成 5,如果 EAX 是 5,便依然是 5
4. 由上面的分析可见,要使 SUB EAX,EDX 这一行的结果是 0,只有一个可能性,这就是 EAX 本来是 0 。因为当 EAX 是 0
的时候, CDQ EAX 才会使 EDX 和 EAX 成为 0,这种情况下 SUB EAX,EDX 便会是 0
5. 再往上追查,我们发现了这行
0040130A |. 2BC1 |SUB EAX,ECX
这行把 EAX 减去 ECX,这里的 ECX 是甚么 ? 再往上追查 004012FF,发现它正是我们的序号字符串
004012FF |. 0FBE4C34 2C |MOVSX ECX,BYTE PTR SS:[ESP+ESI+2C]
由此可见,这个 EAX 的值会减去相对位置的序号字符。
只要我们得到每一个 EAX 的值,把它们串行成序号,便可以用它顺利成功注册,我们找到了光明之路了 !
我们把 crackme 再运行一次,在 0040130A 设下断点,记录每一个 EAX 的值
50 4d 51 4a 44
把这些值转换成 ASCII,那就是
P M Q J D
现在,我们再运行 crackme ,以 riijj 作为名字, PMQJD 作为序号,成功注册了 !! |
|