CTF入门——从攻破简单CrackMe开始

Get Started CTF, DeadSimple CrackMe

Posted by Chinsyo on August 16, 2019

也许你还不知道什么是CTF和RE(Reverse Engineer 逆向工程),但你可能已经被韩商言和李现刷屏了几个轮回,最近人气和口碑双丰收的电视剧《亲爱的 热爱的》对CTF这个安全领域的概念做了全民科普。

某天我家那位不知防火墙是何物的人民教师和我闲聊时问起CTF,作为网安公司的研发工程师既诧异又欣慰,诧异的是CTF早于信息安全本身走近大众,欣慰的是在格子衫和秃头怪的调侃声中,电视剧居然率先为这个行业正名。

CTF(Capture The Flag 夺旗赛)是信息安全技术竞赛的一种形式,而逆向工程是信息安全技术的一个范畴,国内知名安全论坛看雪主办的KCTF就是由CrackMe攻防大赛发展而来。

CrackMe是一类供人尝试破解的程序,攻击者和发布者就软件保护和破解展开较量,通常以输入通过软件校验注册码为目标。

由于本职工作iOS研发工程师的缘故,本次示例以macOS系统为例。首先我们需要找到攻破的目标即CrackMe程序,访问 https://reverse.put.as/crackmes/ 找到页面最下方的DeadSimple进行下载,从未知来源下载软件要记得校验摘要和查杀病毒,下面是DeadSimple的摘要信息和查杀结果。

(吐槽一下腾讯,原本打算选择腾讯哈勃的扫描结果,可我一个macOS程序居然给我扫出C盘路径和写入注册表……拜拜了您呐。)

先双击运行程序看看效果,不出意外会弹出两个弹窗,分别如图。

首先提示不是通过AppStore下载,并且没有经过开发者证书签名。

接着提示尚未针对您的Mac优化,这是因为CrackMe程序不包含当前x86_64的架构。

前往 系统偏好设置->安全与隐私 允许安装,如下图,点击「仍要打开」。如果你的页面没有「允许从以下位置下载的应用」或运行时提醒「文件已损坏」,请参考 https://baijiahao.baidu.com/s?id=1594877309704492558 进行设置。

启动后界面包含Name和Code两个输入框,和常见的激活界面相似。

这时随便输入一些信息,如123,123。校验失败会听到错误提示音。

让我们开动起来尝试攻破第一个CrackMe,首先切换到可执行文件的工作路径,以桌面为例,路径为 ~/Desktop/DeadSimple.app/Contents/MacOS ,查看符号表寻找切入点。

1
2
3
4
5
6
$ file DeadSimple
DeadSimple: Mach-O universal binary with 2 architectures: [i386:Mach-O executable i386] [ppc:Mach-O executable ppc]
DeadSimple (for architecture i386):  Mach-O executable i386
DeadSimple (for architecture ppc):  Mach-O executable ppc
$ uname -m
x86_64

当前系统架构x86_64,而可执行文件架构为i386和ppc,可见这个CrackMe年久失修,ppc是苹果在采用intel处理器之前使用的IBM PowerPC架构,所以软件是以i386兼容模式运行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$ nm -arch i386 DeadSimple 
00001d13 t -[AppDelegate check:]
00001daa t -[AppDelegate checkCode:forName:]
00003020 S .objc_class_name_AppDelegate
         U .objc_class_name_NSObject
         U _NSApplicationMain
         U _NSBeep
         U _NSRunAlertPanelRelativeToWindow
0000200c D _NXArgc
00002008 D _NXArgv
         U ___CFConstantStringClassReference
         U ___keymgr_dwarf2_register_sections
00002000 D ___progname
         U __cthread_init_routine
00001d04 t __dyld_func_lookup
00001000 A __mh_execute_header
00001c16 t __start
         U _atexit
00002058 S _catch_exception_raise
0000205c S _catch_exception_raise_state
00002060 S _catch_exception_raise_state_identity
00002064 S _clock_alarm_reply
00002068 S _do_mach_notify_dead_name
0000206c S _do_mach_notify_no_senders
00002070 S _do_mach_notify_port_deleted
00002074 S _do_mach_notify_send_once
00002078 S _do_seqnos_mach_notify_dead_name
0000207c S _do_seqnos_mach_notify_no_senders
00002080 S _do_seqnos_mach_notify_port_deleted
00002084 S _do_seqnos_mach_notify_send_once
00002004 D _environ
         U _errno
         U _exit
         U _mach_init_routine
00001d0a t _main
         U _objc_msgSend
00002088 S _receive_samples
00001cf8 t dyld_stub_binding_helper
00001bec T start

macOS 平台编程语言 Objective-C 使用中括号表示方法,中括号前+表示类方法,-表示实例方法。看样子 -[AppDelegate check:] 就是我们要找的入手点,lldb挂载调试。

1
2
3
4
$ lldb DeadSimple 
(lldb) target create "DeadSimple"
Current executable set to 'DeadSimple' (i386).
(lldb)

根据前一步推理,查看对应的反汇编代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
(lldb) di -n "-[AppDelegate check:]"
DeadSimple`-[AppDelegate check:]:
DeadSimple[0x1d13] <+0>:   pushl  %ebp
DeadSimple[0x1d14] <+1>:   movl   %esp, %ebp
DeadSimple[0x1d16] <+3>:   pushl  %esi
DeadSimple[0x1d17] <+4>:   pushl  %ebx
DeadSimple[0x1d18] <+5>:   subl   $0x20, %esp
DeadSimple[0x1d1b] <+8>:   movl   0x8(%ebp), %esi
DeadSimple[0x1d1e] <+11>:  movl   0x3000, %eax
DeadSimple[0x1d23] <+16>:  movl   0x8(%esi), %edx
DeadSimple[0x1d26] <+19>:  movl   %eax, 0x4(%esp)
DeadSimple[0x1d2a] <+23>:  movl   %edx, (%esp)
DeadSimple[0x1d2d] <+26>:  calll  0x405e                    ; symbol stub for: objc_msgSend
DeadSimple[0x1d32] <+31>:  movl   0xc(%esi), %edx
DeadSimple[0x1d35] <+34>:  movl   %eax, %ebx
DeadSimple[0x1d37] <+36>:  movl   0x3000, %eax
DeadSimple[0x1d3c] <+41>:  movl   %edx, (%esp)
DeadSimple[0x1d3f] <+44>:  movl   %eax, 0x4(%esp)
DeadSimple[0x1d43] <+48>:  calll  0x405e                    ; symbol stub for: objc_msgSend
DeadSimple[0x1d48] <+53>:  movl   %ebx, 0xc(%esp)
DeadSimple[0x1d4c] <+57>:  movl   %eax, 0x8(%esp)
DeadSimple[0x1d50] <+61>:  movl   0x3004, %eax
DeadSimple[0x1d55] <+66>:  movl   %esi, (%esp)
DeadSimple[0x1d58] <+69>:  movl   %eax, 0x4(%esp)
DeadSimple[0x1d5c] <+73>:  calll  0x405e                    ; symbol stub for: objc_msgSend
DeadSimple[0x1d61] <+78>:  testb  %al, %al
DeadSimple[0x1d63] <+80>:  je     0x1d9f                    ; <+140>
DeadSimple[0x1d65] <+82>:  movl   0x4(%esi), %eax
DeadSimple[0x1d68] <+85>:  movl   $0x0, 0x10(%esp)
DeadSimple[0x1d70] <+93>:  movl   $0x0, 0xc(%esp)
DeadSimple[0x1d78] <+101>: movl   $0x2018, 0x8(%esp)        ; imm = 0x2018 
DeadSimple[0x1d80] <+109>: movl   %eax, 0x14(%esp)
DeadSimple[0x1d84] <+113>: movl   $0x2028, 0x4(%esp)        ; imm = 0x2028 
DeadSimple[0x1d8c] <+121>: movl   $0x2038, (%esp)           ; imm = 0x2038 
DeadSimple[0x1d93] <+128>: calll  0x404a                    ; symbol stub for: NSRunAlertPanelRelativeToWindow
DeadSimple[0x1d98] <+133>: addl   $0x20, %esp
DeadSimple[0x1d9b] <+136>: popl   %ebx
DeadSimple[0x1d9c] <+137>: popl   %esi
DeadSimple[0x1d9d] <+138>: leave  
DeadSimple[0x1d9e] <+139>: retl   
DeadSimple[0x1d9f] <+140>: addl   $0x20, %esp
DeadSimple[0x1da2] <+143>: popl   %ebx
DeadSimple[0x1da3] <+144>: popl   %esi
DeadSimple[0x1da4] <+145>: leave  
DeadSimple[0x1da5] <+146>: jmp    0x4045                    ; symbol stub for: NSBeep
(lldb)

输出的信息很长,鉴于可能不是每位读者都十分熟悉汇编语言,这里做简要的补充。retl表示返回,movl表示赋值,je(jump equal)表示当前一条指令的对比结果相同时跳转到指定地址继续执行,否则按顺序向下继续执行。

所以,简要的了解这段汇编的执行顺序后,发现这段汇编程序只有一个分支判断。

1
DeadSimple[0x1d63] <+80>:  je     0x1d9f      

再观察分支的执行情况,不难看出0x1d9f地址执行的是恢复栈底并蜂鸣报错。

所以,绕过这个检查的方式之一就是跳过0x1d63的判断逻辑。在汇编语言中,nop指令表示no operation即不做操作,opcode为0x90。je 0x1d63的下一条指令地址为0x1d65,0x1d65-0x1d63=2,所以我们接下来要做的就是对0x1d63开始的2个字节填充0x90。

使用vi以二进制方式打开可执行文件。

1
$ vi -b DeadSimple

放眼望去满是^和@组成的符号,这是因为二进制文件不是有效的ascii可视字符,在vi输入:%!xxd 进入hex编辑模式,这下顺眼多了。

在vi中移动光标到0x1d63对应的地址(搜索1d60,然后向后数3字节即6个hex字符),将 74 3a 修改为 90 90。输入:%xxd -r 退出hex模式并保存文件。

这时再次运行软件,随便输入内容,可以看到成功提示弹窗。

友情提示,修改完毕二进制后可以重新查看反汇编检验修改的结果再运行程序,由于篇幅不再展开。下一篇将围绕如何倒推校验逻辑,编写一个注册码生成程序。

转载请注明原始出处 CTF入门——从攻破简单CrackMe开始 © 晨晓 | Chinsyo