[TOC]
1.前言
一道bamboofoxctf上的中等难度逆向题。
主要想讲讲通过这道题学到的东西,看完这篇文章,你可以:
- 一般逆向题的静态分析技术;
- Windows上IDA结合Linux虚拟机远程动态调试技术;
- Linux上初级
expect
编程技术
ltrace
动态调试技术(更适合这道题)
题目描述:
2.静态分析
题目给了一个文件pro
,使用file
命令查看文件格式:
spwpun@ubuntu:~/Documents/20200102$ file pro
pro: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=7bfce31b622a4e5bd9db43154888a3e1891ccac9, stripped
spwpun@ubuntu:~/Documents/20200102$
是一个ELF64位文件,在Linux虚拟机下执行该文件,首先需要输入password,随便输入之后验证错误就结束了:
spwpun@ubuntu:~/Documents/20200102$ ./pro
First give me your password: 2312
You don't know static analysis !
spwpun@ubuntu:~/Documents/20200102$
提示需要静态分析,使用IDA64位打开,程序没有混淆,在反编译后的main函数中很容易就能看清楚程序的逻辑:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int v4; // [rsp+8h] [rbp-38h]
int i; // [rsp+Ch] [rbp-34h]
char s2; // [rsp+10h] [rbp-30h]
unsigned __int64 v7; // [rsp+38h] [rbp-8h]
v7 = __readfsqword(0x28u);
i = 0;
printf("First give me your password: ", a2, a3);
__isoc99_scanf("%d", &v4);
if ( v4 != 98416 )
{
puts("You don't know static analysis !");
exit(0);
}
printf("Second give me your key: ");
__isoc99_scanf("%d", &v4);
v4 -= 49;
for ( i = 0; i <= 11; ++i )
*((_BYTE *)&loc_201020 + i) += v4;
((void (__fastcall *)(char *))loc_201020)(s1);
printf("Then Verify your flag: ");
__isoc99_scanf("%s", &s2);
if ( !strcmp(s1, &s2) )
puts("You are right. Congratulations !!");
else
puts("You don't know dynamic analysis !");
return 0LL;
}
总的来说,程序的逻辑如下:
- 首先获取用户输入,验证password
- 然后继续获取用户输入,验证key
- key减去49得到新的key
- 修改
loc_201020
处的前11个字节的数据,在原始的数据上加上新的key的值
- 然后把
loc_201020
当做函数来执行,参数为s1
,应该是修改s1
的内容
- 再次获取用户输入,验证flag值
- 最后比较输入的flag和s1,相同则验证成功
从上面的反编译伪代码中可以很容易知道password的值为98416
, 但是key
的值却不知道。貌似key
是为了解码出正确的函数,然后利用该函数再解码真正的flag:s1
,下面我使用动态调试来验证一下。
3.动态调试
gdb
首先在IDA中查看main
函数的地址,下图中为0x00000000000007FA
:
看上去这个地址是有点奇怪,暂时不管,先用gdb试试。
使用gdb启动该程序,设置断点,然后执行:
spwpun@ubuntu:~/Documents/20200102$ gdb ./pro
GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-git
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./pro...(no debugging symbols found)...done.
gdb-peda$ b *0x7fa
Breakpoint 1 at 0x7fa
gdb-peda$ r
Starting program: /home/spwpun/Documents/20200102/pro
Warning:
Cannot insert breakpoint 1.
Cannot access memory at address 0x7fa
gdb-peda$
上面的代码中提示不能设置断点,因为内存地址不可用。原来是地址随机化保护,以为是逆向题就没在意这个,使用checksec
命令查看:
gdb-peda$ checksec pro
CANARY : ENABLED
FORTIFY : disabled
NX : disabled
PIE : ENABLED
RELRO : FULL
gdb-peda$
果然开启了PIE。不设断点,直接执行,输入password和key之后,程序会报段错误:
gdb-peda$ r
Starting program: /home/spwpun/Documents/20200102/pro
First give me your password: 98416
Second give me your key: 31
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x0
RCX: 0x70 ('p')
RDX: 0x555555755020 --> 0x6deeb47035141c6d
RSI: 0x1
RDI: 0x555555755100 ("iAxU.|&30\f) (Heh2G:bdyRF;\nOYn=l%")
RBP: 0x7fffffffdd50 --> 0x555555554960 (push r15)
RSP: 0x7fffffffdd08 --> 0x5555555548e2 (lea rdi,[rip+0x162] # 0x555555554a4b)
RIP: 0x555555755020 --> 0x6deeb47035141c6d
R8 : 0x0
R9 : 0x0
R10: 0x7ffff7b82cc0 --> 0x2000200020002
R11: 0x555555554a08 --> 0x0
R12: 0x5555555546f0 (xor ebp,ebp)
R13: 0x7fffffffde30 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x55555575501a: add BYTE PTR [rax],al
0x55555575501c: add BYTE PTR [rax],al
0x55555575501e: add BYTE PTR [rax],al
=> 0x555555755020: ins DWORD PTR es:[rdi],dx
0x555555755021: sbb al,0x14
0x555555755023: xor eax,0x6deeb470
0x555555755028: hlt
0x555555755029: or eax,0x1c77035
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdd08 --> 0x5555555548e2 (lea rdi,[rip+0x162] # 0x555555554a4b)
0008| 0x7fffffffdd10 --> 0x1
0016| 0x7fffffffdd18 --> 0xcffffffee
0024| 0x7fffffffdd20 --> 0x7ffff7de59a0 (<_dl_fini>: push rbp)
0032| 0x7fffffffdd28 --> 0x0
0040| 0x7fffffffdd30 --> 0x555555554960 (push r15)
0048| 0x7fffffffdd38 --> 0x5555555546f0 (xor ebp,ebp)
0056| 0x7fffffffdd40 --> 0x7fffffffde30 --> 0x1
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000555555755020 in ?? ()
gdb-peda$
且根据gdb的gdb-peda
插件,可以看到,执行的地址(0x0000555555755020
)确实和IDA中显示的不一样,不过有一个地方需要注意,地址的后3位数是一样的。
gdb我现在用得还不是太熟,还是习惯OD之类的图形化调试工具,知道IDA有一个功能可以远程调试Linux上的程序,之前也没试过,就趁这次学习记录一下吧。
IDA远程调试Linux程序
首先将Windows上IDA安装目录下的调试程序复制到虚拟机中:
查看Linux虚拟机的IP,并运行该程序:
spwpun@ubuntu:~/Documents/20200102$ ifconfig
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.119.132 netmask 255.255.255.0 broadcast 192.168.119.255
inet6 fe80::9d14:35b9:dc2a:66c6 prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:fe:9a:13 txqueuelen 1000 (Ethernet)
RX packets 49088 bytes 61213168 (61.2 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 13714 bytes 1047395 (1.0 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 678 bytes 60507 (60.5 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 678 bytes 60507 (60.5 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
spwpun@ubuntu:~/Documents/20200102$ ./linux_server64
IDA Linux 64-bit remote debug server(ST) v1.22. Hex-Rays (c) 2004-2017
Listening on 0.0.0.0:23946...
然后到IDA中设置调试器信息,Debugger>Select debugger>Remote Linux Debugger:
确定之后,再设置进程信息,Debugger>Process options:
文件路径要设置为Linux上的路径,IP为虚拟机的IP,端口就用默认的就行。
确定之后,点击上方的绿色三角按钮或者按下F9
启动调试:
弹出下面的警告,大意就是小心代码执行所带来的的危害,确定就行,不是恶意软件:
由于我没有设置任何断点,所以程序还是没能按照我想的情况走下去,在Linux上提示输入Password了,但是IDA中的寄存器却什么信息也没有:
这时结束掉进程,在main
函数设置断点:
这时我们也可以看到,指令前面的地址是真实的内存地址了。
继续执行,程序断在了刚才的地方:
先来简单看一下调试界面,左上方是汇编代码的窗口,左下方是内存区域的数据显示,右上方是三个小窗口(寄存器、已加载模块、线程),右下方是堆栈窗口。基本布局和OD中的一样,习惯了OD,这样看起来特别舒服。
基本的调试快捷键如下:
- F7:单步步进(进入调用内部)
- F8:单步步过(不进入)
- Ctrl+F7:执行到返回
- F4:执行到光标
- F2:设置断点
回到刚才说的,要验证Key对loc_201020区域的作用,在这里这片区域的地址和刚才是是不一样的,来看一下反编译的伪码:
printf("Second give me your key: ", &v4);
__isoc99_scanf("%d", &v4);
v4 -= 49;
for ( i = 0; i <= 11; ++i )
*((_BYTE *)&loc_56290DD48020 + i) += v4;
((void (__fastcall *)(char *, int *))loc_56290DD48020)(s1, &v4);
printf("Then Verify your flag: ");
在“输入Key之后调用scanf
函数”处设置断点:
然后执行,转到Linux上输入password之后,断在了此处:
spwpun@ubuntu:~/Documents/20200102$ ./linux_server64
IDA Linux 64-bit remote debug server(ST) v1.22. Hex-Rays (c) 2004-2017
Listening on 0.0.0.0:23946...
=========================================================
[1] Accepting connection from 192.168.119.1...
First give me your password: 98416
Second give me your key: [1] Closing connection from 192.168.119.1...
=========================================================
[2] Accepting connection from 192.168.119.1...
First give me your password: 98416
仔细分析:
.text:000056290DB47872 lea rax, [rbp+var_38]
.text:000056290DB47876 mov rsi, rax ;传参&var_38,var_38就是伪码中的v4
.text:000056290DB47879 lea rdi, aD ; "%d"
.text:000056290DB47880 mov eax, 0
.text:000056290DB47885 call ___isoc99_scanf
.text:000056290DB4788A mov eax, [rbp+var_38] ;将获取到的key赋值给eax
.text:000056290DB4788D sub eax, 49 ;然后再减去49
.text:000056290DB47890 mov [rbp+var_38], eax ;再重新赋值给var_38
在这里单步步过,转到Linux上随便输入一个Key:
=========================================================
[2] Accepting connection from 192.168.119.1...
First give me your password: 98416
Second give me your key: 78
返回IDA,随后进入修改数据的for
循环:
详细分析一下循环中的汇编代码:
.text:000056290DB4789C loc_56290DB4789C:
.text:000056290DB4789C mov eax, [rbp+var_34] ;var_34在循环开始前设置为0,是索引
.text:000056290DB4789F movsxd rdx, eax
.text:000056290DB478A2 lea rax, loc_56290DD48020 ;需要修改的数据的基地址
.text:000056290DB478A9 movzx eax, byte ptr [rdx+rax];根据索引找到的本次循环需要修改的数据data[i]
.text:000056290DB478AD mov edx, [rbp+var_38] ;key
.text:000056290DB478B0 lea ecx, [rax+rdx] ;相加的结果放到ecx中
.text:000056290DB478B3 mov eax, [rbp+var_34] ;索引i
.text:000056290DB478B6 movsxd rdx, eax
.text:000056290DB478B9 lea rax, loc_56290DD48020 ;数据的基地址
.text:000056290DB478C0 mov [rdx+rax], cl ;最后存放的数据只存放了CL寄存器中的,也就是只取最低的字节
.text:000056290DB478C3 add [rbp+var_34], 1 ;i+1
关键的是最后存放数据的时候只取了key和data相加之后结果的低8位,意思就是如果key的值超过了0xFF,其结果仍然会在0-0xFF中重复,这和之后我写代码爆破可用的Key有关。
循环执行完,会将刚才那一部分区域的数据当做代码执行,这里看到的是call rdx
:
.text:000056290DB478CD lea rdx, loc_56290DD48020
.text:000056290DB478D4 lea rdi, s1 ; "iAxU.|&30"
.text:000056290DB478DB mov eax, 0
.text:000056290DB478E0 call rdx ; loc_56290DD48020
.text:000056290DB478E2 lea rdi, aThenVerifyYour ; "Then Verify your flag: "
.text:000056290DB478E9 mov eax, 0
.text:000056290DB478EE call _printf
跟踪进去看了之后,确实是将修改后的数据当做代码来执行:
但是由于key不正确,所以解码出来的汇编代码是会出大问题的,继续执行了几步之后程序就崩溃了:
而根据上面的汇编代码,如果解码之后的代码能够正常执行的话,应该会继续提示让输入flag验证,根据这个思路,我们可以写一段简单的代码来爆破可能的key,这里我用到的是expect
。
4.expect编程
expect
是一个用来实现自动化交互功能的软件套件,基于tcl
包。安装命令可以使用下面的,这里我已经安装过了:
spwpun@ubuntu:~/Documents/20200102$ sudo apt install tcl expect
[sudo] password for spwpun:
Reading package lists... Done
Building dependency tree
Reading state information... Done
expect is already the newest version (5.45.4-1).
tcl is already the newest version (8.6.0+9).
0 upgraded, 0 newly installed, 0 to remove and 130 not upgraded.
spwpun@ubuntu:~/Documents/20200102$
我的基本思路是运行题目给出的pro
程序,使用expect自动填入password和key,通过一个循环控制key的值来测试,如果接收到"Then Verify your flag: ",就说明key解码后的代码是可以正常执行的,最后输出所有的key。代码很简单,简单借鉴一下网上的一些基础脚本就可以写出来:
#!/usr/bin/expect
# For bamboofoxctf-Move or not
# filename:crack.sh
set time 30
set keys ""
for {set key 0} {$key<=255} {incr key} { #incr在这里是增加1
spawn ./pro #spawn是另起一个子进程
expect "*password: " {send "98416\r"} #如果收到子进程的结果为*password: ,则发送98416\r,这里可以使用正则来匹配,发送的数据最后要加一个换行符
expect "*key: " {send "$key\r"}
expect "*flag: " {
send "Test_flag\r"
set keys "$keys $key"
}
}
puts "All keys: $keys\r"
执行结果:
spwpun@ubuntu:~/Documents/20200102$ ./crack.sh
spawn ./pro
First give me your password: 98416
Second give me your key: 0
spawn ./pro
......
First give me your password: 98416
Second give me your key: 254
spawn ./pro
First give me your password: 98416
Second give me your key: 255
All keys: 39 43 48 50 114 117 206
spwpun@ubuntu:~/Documents/20200102$
到此知道了所有可能的key,就可以使用IDA动态调试一波,最后在测试50的时候顺利在比对字符串的时候拿到了flag,其中正确解码后的汇编代码如下:
rdi是传入变量s1的地址,可以看到,依次对s1的数据进行sub操作,得到正确的flag,strcmp函数比较时可以清楚看到正确的flag:
5.ltrace动态调试
ltrace可以跟踪程序执行时库函数的调用(包括参数),所以也会跟踪strcmp函数的调用,在上面得到所有的keys之后,可以使用它来测试,测试的过程如下,很轻松就得到了flag:
spwpun@ubuntu:~/Documents/20200102$ ltrace ./pro
printf("First give me your password: ") = 29
__isoc99_scanf(0x5563a643ea06, 0x7fffd7e61358, 0, 0First give me your password: 98416
) = 1
printf("Second give me your key: ") = 25
__isoc99_scanf(0x5563a643ea06, 0x7fffd7e61358, 0, 0Second give me your key: 50
) = 1
printf("Then Verify your flag: ") = 23
__isoc99_scanf(0x5563a643ea63, 0x7fffd7e61360, 0, 0Then Verify your flag: aaa
) = 1
strcmp("BambooFox{dyn4mic_1s_4ls0_gr34t}"..., "aaa") = -31
puts("You don't know dynamic analysis "...You don't know dynamic analysis !
) = 34
+++ exited (status 0) +++
spwpun@ubuntu:~/Documents/20200102$
- flag: BambooFox{dyn4mic_1s_4ls0_gr34t}
6.总结
元旦那晚为了这道题肝了一晚,一直在尝试些angr的脚本来爆破,无奈之前没有接触过,最后没有写出来,分析出key的范围之后,手工解出了这道题,实在是菜。看到赛后各位大佬wp中的轻描淡写,我觉得我和他们真是差了不是一点半点。希望看到这篇文章的师傅们不吝建议!
道阻且长,Happy New Year!