IOLI-crackme0x01-0x05 writeup
上一篇开了个头, 使用Radare2并用3中方法来解决crackme0x00, 由于是第一篇, 所以解释得事无巨细, 今天就稍微加快点步伐, 分析一下另外几个crackme.
如果你忘记了crackme的来源, 那就再告诉你一遍, 它们都是来自IOLI-crackme.
crackme0x01
直接用radare2打开分析:
[0x080483e4]> pdf @ main
;-- main:
/ (fcn) main 113
| main ();
| ; var int pInput @ ebp-0x4
| ; DATA XREF from 0x08048347 (entry0)
| 0x080483e4 55 push ebp
| 0x080483e5 89e5 mov ebp, esp
| 0x080483e7 83ec18 sub esp, 0x18
| 0x080483ea 83e4f0 and esp, 0xfffffff0
| 0x080483ed b800000000 mov eax, 0
| 0x080483f2 83c00f add eax, 0xf
| 0x080483f5 83c00f add eax, 0xf
| 0x080483f8 c1e804 shr eax, 4
| 0x080483fb c1e004 shl eax, 4
| 0x080483fe 29c4 sub esp, eax
| 0x08048400 c70424288504. mov dword [esp], str.IOLI_Crackme_Level_0x01 ; [0x8048528:4]=0x494c4f49 ; "IOLI Crackme Level 0x01\n"
| 0x08048407 e810ffffff call sym.imp.printf ; int printf(const char *format)
| 0x0804840c c70424418504. mov dword [esp], str.Password: ; [0x8048541:4]=0x73736150 ; "Password: "
| 0x08048413 e804ffffff call sym.imp.printf ; int printf(const char *format)
| 0x08048418 8d45fc lea eax, [pInput]
| 0x0804841b 89442404 mov dword [esp + 4], eax
| 0x0804841f c704244c8504. mov dword [esp], 0x804854c ; [0x804854c:4]=0x49006425
| 0x08048426 e8e1feffff call sym.imp.scanf ; int scanf(const char *format)
| 0x0804842b 817dfc9a1400. cmp dword [pInput], 0x149a ; [0x149a:4]=-1
| ,=< 0x08048432 740e je 0x8048442
[0x080483e4]> ps @ 0x804854c
%d
还是scanf获取用户输入, 不过这次是%d
即用户输入一个整数, 然后和0x149a比较, 使用rax2转换数据格式:
$ rax2 0x149a
5274
所以:
$ ./crackme0x01
IOLI Crackme Level 0x01
Password: 5274
Password OK :)
密码正确! 和crackme0x00差不多, 逻辑比较简单.
crackme0x02
先运行一下, 发现和之前一样还是要输入密码. radare2打开:
[0x080483e4]> pdf @ main
;-- main:
/ (fcn) main 144
| main ();
| ; var int local_ch @ ebp-0xc
| ; var int local_8h @ ebp-0x8
| ; var int local_4h @ ebp-0x4
| ; var int local_4h_2 @ esp+0x4
| ; DATA XREF from 0x08048347 (entry0)
| 0x080483e4 55 push ebp
| 0x080483e5 89e5 mov ebp, esp
| 0x080483e7 83ec18 sub esp, 0x18
| 0x080483ea 83e4f0 and esp, 0xfffffff0
| 0x080483ed b800000000 mov eax, 0
| 0x080483f2 83c00f add eax, 0xf
| 0x080483f5 83c00f add eax, 0xf
| 0x080483f8 c1e804 shr eax, 4
| 0x080483fb c1e004 shl eax, 4
| 0x080483fe 29c4 sub esp, eax
| 0x08048400 c70424488504. mov dword [esp], str.IOLI_Crackme_Level_0x02 ; [0x8048548:4]=0x494c4f49 ; "IOLI Crackme Level 0x02\n"
| 0x08048407 e810ffffff call sym.imp.printf ; int printf(const char *format)
| 0x0804840c c70424618504. mov dword [esp], str.Password: ; [0x8048561:4]=0x73736150 ; "Password: "
| 0x08048413 e804ffffff call sym.imp.printf ; int printf(const char *format)
| 0x08048418 8d45fc lea eax, [local_4h]
| 0x0804841b 89442404 mov dword [local_4h_2], eax
| 0x0804841f c704246c8504. mov dword [esp], 0x804856c ; [0x804856c:4]=0x50006425
| 0x08048426 e8e1feffff call sym.imp.scanf ; int scanf(const char *format)
| 0x0804842b c745f85a0000. mov dword [local_8h], 0x5a ; 'Z' ; 90
| 0x08048432 c745f4ec0100. mov dword [local_ch], 0x1ec ; 492
| 0x08048439 8b55f4 mov edx, dword [local_ch]
| 0x0804843c 8d45f8 lea eax, [local_8h]
| 0x0804843f 0110 add dword [eax], edx
| 0x08048441 8b45f8 mov eax, dword [local_8h]
| 0x08048444 0faf45f8 imul eax, dword [local_8h]
| 0x08048448 8945f4 mov dword [local_ch], eax
| 0x0804844b 8b45fc mov eax, dword [local_4h]
| 0x0804844e 3b45f4 cmp eax, dword [local_ch]
| ,=< 0x08048451 750e jne 0x8048461
[0x080483e4]> ps @ 0x804856c
%d
这个就比之前复杂一点, main函数有三个本地变量, local_ch
, local_8h
和local_4h
,
但似乎没有初始值. 由0x08048418~0x08048426
这几句可以发现local_4h
是用户的输入,
且类型为整数.
分析一下用户输入后的逻辑, 先给两个本地变量分别赋值为0x5a和0x1ec, 然后进行数学运算,
先改几个名字方便阅读:
[0x080483e4]> afv-local_4h_2
[0x080483e4]> afvn local_4h input
[0x080483e4]> afvn local_8h a
[0x080483e4]> afvn local_ch b
[0x080483e4]> pd 10 @ 0x0804842b
0x0804842b c745f85a0000. mov dword [a], 0x5a ; 'Z' ; 90
0x08048432 c745f4ec0100. mov dword [b], 0x1ec ; 492
0x08048439 8b55f4 mov edx, dword [b]
0x0804843c 8d45f8 lea eax, [a]
0x0804843f 0110 add dword [eax], edx
0x08048441 8b45f8 mov eax, dword [a]
0x08048444 0faf45f8 imul eax, dword [a]
0x08048448 8945f4 mov dword [b], eax
0x0804844b 8b45fc mov eax, dword [input]
0x0804844e 3b45f4 cmp eax, dword [b]
重新打印scanf之后的10条汇编, 转换成伪代码大意是:
int a = 0x5a
int b = 0x1ec
a = b + a
a = a * a
b = a
if input == b
所以最后和input比较的是(a+b)*(a+b)=582*582=338724
, 验证一下:
$ ./crackme0x02
IOLI Crackme Level 0x02
Password: 338724
Password OK :)
抬走, 下一个.
crackme0x03
流程和之前一样:
$ ./crackme0x03
IOLI Crackme Level 0x03
Password: 12345
Invalid Password!
不过这次似乎里面的字符串被混淆了, 没有找到Invalid Password
出现的地方:
$ rabin2 -z ./crackme0x03
000 0x000005ec 0x080485ec 17 18 (.rodata) ascii Lqydolg#Sdvvzrug$
001 0x000005fe 0x080485fe 17 18 (.rodata) ascii Sdvvzrug#RN$$$#=,
002 0x00000610 0x08048610 24 25 (.rodata) ascii IOLI Crackme Level 0x03\n
003 0x00000629 0x08048629 10 11 (.rodata) ascii Password:
radare2打开并分析main函数, 发现用户输入后调用了test函数, 如下:
...忽略
| 0x080484c0 c70424298604. mov dword [esp], str.Password: ; [0x8048629:4]=0x73736150 ; "Password: "
| 0x080484c7 e884feffff call sym.imp.printf ; int printf(const char *format)
| 0x080484cc 8d45fc lea eax, [local_4h]
| 0x080484cf 89442404 mov dword [local_4h_2], eax
| 0x080484d3 c70424348604. mov dword [esp], 0x8048634 ; [0x8048634:4]=0x6425
| 0x080484da e851feffff call sym.imp.scanf ; int scanf(const char *format)
| 0x080484df c745f85a0000. mov dword [local_8h], 0x5a ; 'Z' ; 90
| 0x080484e6 c745f4ec0100. mov dword [local_ch], 0x1ec ; 492
| 0x080484ed 8b55f4 mov edx, dword [local_ch]
| 0x080484f0 8d45f8 lea eax, [local_8h]
| 0x080484f3 0110 add dword [eax], edx
| 0x080484f5 8b45f8 mov eax, dword [local_8h]
| 0x080484f8 0faf45f8 imul eax, dword [local_8h]
| 0x080484fc 8945f4 mov dword [local_ch], eax
| 0x080484ff 8b45f4 mov eax, dword [local_ch]
| 0x08048502 89442404 mov dword [local_4h_2], eax
| 0x08048506 8b45fc mov eax, dword [local_4h]
| 0x08048509 890424 mov dword [esp], eax
| 0x0804850c e85dffffff call sym.test
| 0x08048511 b800000000 mov eax, 0
| 0x08048516 c9 leave
\ 0x08048517 c3 ret
main函数内同样有三个本地变量, 面对这种多层调用的目标时候, 可以选择深度优先或者广度优先分析,
这里选择深度优先, 即先分析sym.test
函数:
[0x08048498]> pdf @ sym.test
/ (fcn) sym.test 42
| sym.test (int arg_8h, int arg_ch);
| ; arg int arg_8h @ ebp+0x8
| ; arg int arg_ch @ ebp+0xc
| ; CALL XREF from 0x0804850c (sym.main)
| 0x0804846e 55 push ebp
| 0x0804846f 89e5 mov ebp, esp
| 0x08048471 83ec08 sub esp, 8
| 0x08048474 8b4508 mov eax, dword [arg_8h] ; [0x8:4]=-1 ; 8
| 0x08048477 3b450c cmp eax, dword [arg_ch] ; [0xc:4]=-1 ; 12
| ,=< 0x0804847a 740e je 0x804848a
| | 0x0804847c c70424ec8504. mov dword [esp], str.Lqydolg_Sdvvzrug ; [0x80485ec:4]=0x6479714c ; "Lqydolg#Sdvvzrug$"
| | 0x08048483 e88cffffff call sym.shift
| ,==< 0x08048488 eb0c jmp 0x8048496
| |`-> 0x0804848a c70424fe8504. mov dword [esp], str.Sdvvzrug_RN ; [0x80485fe:4]=0x76766453 ; "Sdvvzrug#RN$$$#=,"
| | 0x08048491 e87effffff call sym.shift
| | ; JMP XREF from 0x08048488 (sym.test)
| `--> 0x08048496 c9 leave
\ 0x08048497 c3 ret
可以看到该函数接受2个参数, 值得一提的是根据(x86)cdecl调用约定, 函数参数通过栈传递, 并且顺序为从右到左. 可以看到test函数中调用了shift函数, 接受1个字符串参数, 估计是解密字符串相关的函数, 先看看它:
[0x08048498]> pdf @ sym.shift
/ (fcn) sym.shift 90
| sym.shift (int arg_8h);
| ; var int local_7ch @ ebp-0x7c
| ; var int local_78h @ ebp-0x78
| ; arg int arg_8h @ ebp+0x8
| ; var int local_4h @ esp+0x4
| ; CALL XREF from 0x08048491 (sym.test)
| ; CALL XREF from 0x08048483 (sym.test)
| 0x08048414 55 push ebp
| 0x08048415 89e5 mov ebp, esp
| 0x08048417 81ec98000000 sub esp, 0x98
| 0x0804841d c74584000000. mov dword [local_7ch], 0
| ; JMP XREF from 0x0804844e (sym.shift)
| .-> 0x08048424 8b4508 mov eax, dword [arg_8h] ; [0x8:4]=-1 ; 8
| : 0x08048427 890424 mov dword [esp], eax
| : 0x0804842a e811ffffff call sym.imp.strlen ; size_t strlen(const char *s)
| : 0x0804842f 394584 cmp dword [local_7ch], eax ; [0x13:4]=-1 ; 19
| ,==< 0x08048432 731c jae 0x8048450
| |: 0x08048434 8d4588 lea eax, [local_78h]
| |: 0x08048437 89c2 mov edx, eax
| |: 0x08048439 035584 add edx, dword [local_7ch]
| |: 0x0804843c 8b4584 mov eax, dword [local_7ch]
| |: 0x0804843f 034508 add eax, dword [arg_8h]
| |: 0x08048442 0fb600 movzx eax, byte [eax]
| |: 0x08048445 2c03 sub al, 3
| |: 0x08048447 8802 mov byte [edx], al
| |: 0x08048449 8d4584 lea eax, [local_7ch]
| |: 0x0804844c ff00 inc dword [eax]
| |`=< 0x0804844e ebd4 jmp 0x8048424
| `--> 0x08048450 8d4588 lea eax, [local_78h]
| 0x08048453 034584 add eax, dword [local_7ch]
| 0x08048456 c60000 mov byte [eax], 0
| 0x08048459 8d4588 lea eax, [local_78h]
| 0x0804845c 89442404 mov dword [local_4h], eax
| 0x08048460 c70424e88504. mov dword [esp], 0x80485e8 ; [0x80485e8:4]=0xa7325
| 0x08048467 e8e4feffff call sym.imp.printf ; int printf(const char *format)
| 0x0804846c c9 leave
\ 0x0804846d c3 ret
可以看到shift的作用是接受一个字符串->处理->printf
, 其实我们可以不用分析shift函数的逻辑,
因为开启gdb一调就知道在test函数中哪个分支是"Password OK"了, 甚至都不用调试, 因为一共就2个分支,
非此即彼, 但秉承着知易行难
的原则, 还是分析了一遍, shift函数伪代码如下:
void shift(char *src) {
int i;
char dst[N];
for (i = 0; i < strlen(src); i++) {
dst[i] = src[i] - 3;
}
dst[i] = 0;
printf("%s", dst);
}
写个python脚本验证下之前rabin2发现.rodata
段的两个字符串解密:
# /usr/bin/env python2
# shift.py
def shift(src):
dst = []
for i in src:
dst.append(chr(ord(i)-3))
print(''.join(dst))
shift('Lqydolg#Sdvvzrug$')
shift('Sdvvzrug#RN$$$#=,')
运行:
$ python shift.py
Invalid Password!
Password OK!!! :)
OK, 现在回到test函数, 这个函数比较简单, 接受2个参数, 如果第二个参数等于第一个参数, 则进入我们想要的分支. 再回到main函数, scanf接受一个整数input, 然后进行数学运算, 如下(重命名了一些变量名称):
0x080484df c745f85a0000. mov dword [a], 0x5a ; 'Z' ; 90
0x080484e6 c745f4ec0100. mov dword [b], 0x1ec ; 492
0x080484ed 8b55f4 mov edx, dword [b]
0x080484f0 8d45f8 lea eax, [a]
0x080484f3 0110 add dword [eax], edx
0x080484f5 8b45f8 mov eax, dword [a]
0x080484f8 0faf45f8 imul eax, dword [a]
0x080484fc 8945f4 mov dword [b], eax
0x080484ff 8b45f4 mov eax, dword [b]
0x08048502 89442404 mov dword [esp + 4], eax
0x08048506 8b45fc mov eax, dword [input]
0x08048509 890424 mov dword [esp], eax
0x0804850c e85dffffff call sym.test
转化为人类语言就是:
int a = 0x5a, b = 0x1ec;
a = a + b;
b = a * a;
test(input, b)
好吧, 结果还是要用输入和(0x5a*0x1ec)^2=338724
比较, 若相等则通过, 验证下:
$ ./crackme0x03
IOLI Crackme Level 0x03
Password: 338724
Password OK!!! :)
密码和上一题一样, 囧~
crackme0x04
老样子, 直接跳转到main函数然后查看汇编:
[0x08048509]> pdf @ main
...
0x08048528 c704245e8604. mov dword [esp], str.IOLI_Crackme_Level_0x04 ; [0x804865e:4]=0x494c4f49 ; "IOLI Crackme Level 0x04\n"
0x0804852f e860feffff call sym.imp.printf ; int printf(const char *format)
0x08048534 c70424778604. mov dword [esp], str.Password: ; [0x8048677:4]=0x73736150 ; "Password: "
0x0804853b e854feffff call sym.imp.printf ; int printf(const char *format)
0x08048540 8d4588 lea eax, [local_78h]
0x08048543 89442404 mov dword [local_4h], eax
0x08048547 c70424828604. mov dword [esp], 0x8048682 ; [0x8048682:4]=0x7325
0x0804854e e821feffff call sym.imp.scanf ; int scanf(const char *format)
0x08048553 8d4588 lea eax, [local_78h]
0x08048556 890424 mov dword [esp], eax
0x08048559 e826ffffff call sym.check
...
[0x08048509]> ps @ 0x8048682
%s
这回main函数挺简单, 主要是scanf输入一个字符串, 然后调用check函数, 汇编如下:
[0x080484fb]> pdf @ sym.check
/ (fcn) sym.check 133
| sym.check (char *input);
| ; var int local_dh @ ebp-0xd
| ; var int local_ch @ ebp-0xc
| ; var int local_8h @ ebp-0x8
| ; var int local_4h @ ebp-0x4
| ; arg char * input @ ebp+0x8
| ; CALL XREF from 0x08048559 (sym.main)
| 0x08048484 55 push ebp
| 0x08048485 89e5 mov ebp, esp
| 0x08048487 83ec28 sub esp, 0x28 ; '('
| 0x0804848a c745f8000000. mov dword [local_8h], 0
| 0x08048491 c745f4000000. mov dword [local_ch], 0
| ; JMP XREF from 0x080484f9 (sym.check)
| .-> 0x08048498 8b4508 mov eax, dword [input] ; [0x8:4]=-1 ; 8
| : 0x0804849b 890424 mov dword [esp], eax
| : 0x0804849e e8e1feffff call sym.imp.strlen ; size_t strlen(const char *s)
| : 0x080484a3 3945f4 cmp dword [local_ch], eax ; [0x13:4]=-1 ; 19
| ,==< 0x080484a6 7353 jae 0x80484fb
| |: 0x080484a8 8b45f4 mov eax, dword [local_ch]
| |: 0x080484ab 034508 add eax, dword [input]
| |: 0x080484ae 0fb600 movzx eax, byte [eax]
| |: 0x080484b1 8845f3 mov byte [local_dh], al
| |: 0x080484b4 8d45fc lea eax, [local_4h]
| |: 0x080484b7 89442408 mov dword [esp + 8], eax
| |: 0x080484bb c74424043886. mov dword [esp + 4], 0x8048638 ; [0x8048638:4]=0x50006425
| |: 0x080484c3 8d45f3 lea eax, [local_dh]
| |: 0x080484c6 890424 mov dword [esp], eax
| |: 0x080484c9 e8d6feffff call sym.imp.sscanf ; int sscanf(const char *s, const char *format, ...)
| |: 0x080484ce 8b55fc mov edx, dword [local_4h]
| |: 0x080484d1 8d45f8 lea eax, [local_8h]
| |: 0x080484d4 0110 add dword [eax], edx
| |: 0x080484d6 837df80f cmp dword [local_8h], 0xf ; [0xf:4]=-1 ; 15
| ,===< 0x080484da 7518 jne 0x80484f4
| ||: 0x080484dc c704243b8604. mov dword [esp], str.Password_OK ; [0x804863b:4]=0x73736150 ; "Password OK!\n"
| ||: 0x080484e3 e8acfeffff call sym.imp.printf ; int printf(const char *format)
| ||: 0x080484e8 c70424000000. mov dword [esp], 0
| ||: 0x080484ef e8c0feffff call sym.imp.exit ; void exit(int status)
| ||: ; JMP XREF from 0x080484da (sym.check)
| `---> 0x080484f4 8d45f4 lea eax, [local_ch]
| |: 0x080484f7 ff00 inc dword [eax]
| |`=< 0x080484f9 eb9d jmp 0x8048498
| | ; JMP XREF from 0x080484a6 (sym.check)
| `--> 0x080484fb c70424498604. mov dword [esp], str.Password_Incorrect ; [0x8048649:4]=0x73736150 ; "Password Incorrect!\n"
| 0x08048502 e88dfeffff call sym.imp.printf ; int printf(const char *format)
| 0x08048507 c9 leave
\ 0x08048508 c3 ret
这个函数比之前的复杂一点, 所以我们用视图模式先有个大局观:
[0x08048484]> VV @ sym.check
[0x08048484]> VV @ sym.check (nodes 6 edges 6 zoom 100%) BB-SUMM mouse:canvas-y mov-speed:5
.--------------------.
| 0x8048484 ;[ga] |
`--------------------'
|
.--'
.--------------------------------------.
| |
| |
| .---------------------------.
| | 0x8048498 ;[gd] |
| | 0x0804849e sym.imp.strlen |
| `---------------------------'
| | |
| | '---------.
| .-----------------' |
| | |
| | |
| .---------------------------. .-----------------------------------.
| | 0x80484a8 ;[gg] | | [0x80484fb] ;[gc] |
| | 0x080484c9 sym.imp.sscanf | | 0x080484fb str.Password_Incorrect |
| `---------------------------' | 0x08048502 sym.imp.printf |
| | | `-----------------------------------'
| | |
| | '-------------.
| .---------------' |
| | |
| | |
|.----------------------------. .--------------------.
|| 0x80484dc ;[gj] | | 0x80484f4 ;[gf] |
|| 0x080484dc str.Password_OK | `--------------------'
|| 0x080484e3 sym.imp.printf | |
|| 0x080484ef sym.imp.exit | |
|`----------------------------' |
| |
`--------------------------------------'
radare2在视图模式下可以通过p/P切换视图, 通过O切换asm的类型. 直接按
?
键可以查看快捷键的帮助.
让我们F5一下, 噢忘了没有F5, 那就人肉反编译一下, check函数有4个本地变量,
但还不知道他们的作用, 有一个参数我已经改成了char *input
, 先来个伪代码:
local_8h = 0, local_ch = 0;
BEGIN:
if (local_ch >= strlen(input)) {
printf("Password Incorrect!\n");
return;
}
eax = input + local_ch;
eax = (int)*eax;
(char*)&local_dh[0] = eax;
sscanf(local_dh, "%d", local_4h);
local_8h = local_4h + local_8h;
if (local_8h != 0xf) {
local_ch ++;
goto BEGIN;
}
printf("Password OK!\n");
return;
这里要注意mov byte [local_dh], al
的意思是把eax中的最低字节移动到local_dh
的第一字节. 也就是说, check对输入的字符串的每个字节都进行sscanf扫描, 如果是个整数
就累加local_8h
里, 只要其等于0xf(=15), 则通过, 所以密码可以有多个, 最简单就是15个1:
$ ./crackme0x04
IOLI Crackme Level 0x04
Password: 111111111111111
Password OK!
只要满足条件都可以, 比如最短的9+6=15:
$ ./crackme0x04
IOLI Crackme Level 0x04
Password: 96
Password OK!
crackme0x05
这题和0x04一样, 都是用户输入一个字符串, 然后调用check, 但是check函数有所不同:
[0x080484c8]> VV @ sym.check (nodes 7 edges 8 zoom 100%) BB-SUMM mouse:canvas-y mov-speed:5
.--------------------.
| 0x80484c8 ;[ga] |
`--------------------'
|
.--'
.----------------------.
| |
| |
| .---------------------------.
| | 0x80484dc ;[gd] |
| | 0x080484e2 sym.imp.strlen |
| `---------------------------'
| | |
| | '---------.
| .-----------------' |
| | |
| | |
|.---------------------------. .-----------------------------------.
|| 0x80484ec ;[gg] | | [0x8048532] ;[gc] |
|| 0x0804850d sym.imp.sscanf | | 0x08048532 str.Password_Incorrect |
|`---------------------------' | 0x08048539 sym.imp.printf |
| | | `-----------------------------------'
| | |
| | '---------------------------------------------------------.
| '-. |
| | |
| | |
| .-----------------------. |
| | 0x8048520 ;[gi] | |
| | 0x08048526 sym.parell | |
| `-----------------------' |
| | |
| '---------------------------. |
| | .-------------------------------'
| | |
| | |
| .--------------------.
| | 0x804852b ;[gf] |
| `--------------------'
| |
`----------------------------------'
我们待会再来看它, check函数里还调用了parell函数, 其流程图如下:
[0x08048484]> VV @ sym.parell (nodes 3 edges 2 zoom 100%) BB-NORM mouse:canvas-y mov-speed:5
.---------------------------------------------.
| [0x8048484] ;[gc] |
| (fcn) sym.parell 68 |
| sym.parell (int arg_8h); |
| ; var int local_4h @ ebp-0x4 |
| ; arg int arg_8h @ ebp+0x8 |
| ; CALL XREF from 0x08048526 (sym.check) |
| push ebp |
| mov ebp, esp |
| sub esp, 0x18 |
| lea eax, [local_4h] |
| mov dword [esp + 8], eax |
| mov dword [esp + 4], 0x8048668 |
| mov eax, dword [arg_8h] |
| mov dword [esp], eax |
| call sym.imp.sscanf;[ga] |
| mov eax, dword [local_4h] |
| and eax, 1 |
| test eax, eax |
| jne 0x80484c6;[gb] |
`---------------------------------------------'
| |
| '-------------------------.
.-------------' |
| |
.--------------------------------------. .--------------------.
| 0x80484ae ;[gf] | | 0x80484c6 ;[gb] |
| ; [0x804866b:4]=0x73736150 | | leave |
| ; "Password OK!\n" | | ret |
| mov dword [esp], str.Password_OK | `--------------------'
| call sym.imp.printf;[gd] |
| mov dword [esp], 0 |
| call sym.imp.exit;[ge] |
`--------------------------------------'
其接受一个参数, 并且经过一顿操作后选择静默返回或者进入正确分支并退出程序. 试着写下伪代码:
void parrel(arg) {
int local_4h;
sscanf(arg, "%d", &local_4h);
local_4h &= 1; // 除了最后一位全部清0
if (local_4h != 0) {
return;
}
printf("Password_OK\n");
exit(0);
}
可以猜测arg应该是char *
类型, 该函数意思是将输入转化为整数, 如果结果的最低有效位为1则通过.
现在可以回到check函数了. 该函数有4个本地变量, 姑且先将其命名为a,b,c,d:
/ (fcn) sym.check 120
| sym.check (int input);
| ; var int a @ ebp-0xd
| ; var int b @ ebp-0xc
| ; var int c @ ebp-0x8
| ; var int d @ ebp-0x4
| ; arg int input @ ebp+0x8
| ; CALL XREF from 0x08048590 (sym.main)
| 0x080484c8 55 push ebp
| 0x080484c9 89e5 mov ebp, esp
| 0x080484cb 83ec28 sub esp, 0x28 ; '('
| 0x080484ce c745f8000000. mov dword [c], 0
| 0x080484d5 c745f4000000. mov dword [b], 0
| ; JMP XREF from 0x08048530 (sym.check)
| .-> 0x080484dc 8b4508 mov eax, dword [input] ; [0x8:4]=-1 ; 8
| : 0x080484df 890424 mov dword [esp], eax
| : 0x080484e2 e89dfeffff call sym.imp.strlen ; size_t strlen(const char *s)
| : 0x080484e7 3945f4 cmp dword [b], eax ; [0x13:4]=-1 ; 19
| ,==< 0x080484ea 7346 jae 0x8048532
| |: 0x080484ec 8b45f4 mov eax, dword [b]
| |: 0x080484ef 034508 add eax, dword [input]
| |: 0x080484f2 0fb600 movzx eax, byte [eax]
| |: 0x080484f5 8845f3 mov byte [a], al
| |: 0x080484f8 8d45fc lea eax, [d]
| |: 0x080484fb 89442408 mov dword [esp + 8], eax
| |: 0x080484ff c74424046886. mov dword [esp + 4], 0x8048668 ; [0x8048668:4]=0x50006425
| |: 0x08048507 8d45f3 lea eax, [a]
| |: 0x0804850a 890424 mov dword [esp], eax
| |: 0x0804850d e892feffff call sym.imp.sscanf ; int sscanf(const char *s, const char *format, ...)
| |: 0x08048512 8b55fc mov edx, dword [d]
| |: 0x08048515 8d45f8 lea eax, [c]
| |: 0x08048518 0110 add dword [eax], edx
| |: 0x0804851a 837df810 cmp dword [c], 0x10 ; [0x10:4]=-1 ; 16
| ,===< 0x0804851e 750b jne 0x804852b
| ||: 0x08048520 8b4508 mov eax, dword [input] ; [0x8:4]=-1 ; 8
| ||: 0x08048523 890424 mov dword [esp], eax
| ||: 0x08048526 e859ffffff call sym.parell
| `---> 0x0804852b 8d45f4 lea eax, [b]
| |: 0x0804852e ff00 inc dword [eax]
| |`=< 0x08048530 ebaa jmp 0x80484dc
| `--> 0x08048532 c70424798604. mov dword [esp], str.Password_Incorrect ; [0x8048679:4]=0x73736150 ; "Password Incorrect!\n"
| 0x08048539 e856feffff call sym.imp.printf ; int printf(const char *format)
| 0x0804853e c9 leave
\ 0x0804853f c3 ret
看到有个反向的跳转, 所以b应该是个循环变量, 重命名为i, 写下伪代码:
int c = 0;
int i = 0;
int d;
char a[2];
while(1) {
if (i >= strlen(input)) {
printf("Password Incorrect!\n");
return;
}
(char*)a[0] = input[i];
(char*)a[1] = 0;
sscanf(a,"%d",&d);
c += d;
if (c==0x10) {
parell(input)
}
i++;
continue;
}
呃…写得有点渣, 不过能看明白逻辑就行了, 意思就是将输入的每个字符转为整数并累加, 如果累加的结果等于16(0x10)则调用parell函数, 前面分析了parrel的作用是将整个字符串 转换为整数, 并判断其最低有效位是否是0(即该数字是否为偶数), 是偶数则通过. 所以我们要输入的密码应该是个偶数, 而且前X位加起来是16就可以了:
$ ./crackme0x05
IOLI Crackme Level 0x05
Password: 88
Password OK!
$ ./crackme0x05
IOLI Crackme Level 0x05
Password: 88666
Password OK!
完美解决!
后记
说实话我一开始对汇编还不是很熟悉, 但动手写了几个writeup之后也逐渐有了点感觉.
对于不熟悉的指令, 比如movzx
等可以查看X86的手册, 比如这里:Opcode of programming language,
而对于不熟悉的语法, 比如Size Directives
或者Calling Conventions
, 可以参考x86 Assembly Guide
以及维基百科. 总之, 熟能生巧, 汇编也不是那么可怕嘛!