C++ strings类的应用

首先感谢starrysky师傅的帮助,师傅人美心善

先看一道题,是SICTF ROUND#3的C++ 栈溢出

1
2
3
4
5
6
7
8
u24@u24-VMware-Virtual-Platform:~/桌面/sictf$ pwn checksec overflow
[!] Could not populate PLT: No module named 'distutils'
[*] '/home/u24/桌面/sictf/overflow'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

看汇编代码,主要存在三个函数mainvulnread_flag(自己定义的);

main

主要是创建一个string对象v7,然后传到vuln函数处理,在之后就是输出和析构对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall main(int a1, char **a2, char **a3)
{
  __int64 v4; // [rsp+10h] [rbp-60h]
  __int64 v5; // [rsp+18h] [rbp-58h]
  __int64 v6; // [rsp+20h] [rbp-50h]
  _BYTE v7[32]; // [rsp+48h] [rbp-28h] BYREF
  unsigned __int64 v8; // [rsp+68h] [rbp-8h]
 
  v8 = __readfsqword(0x28u);
  std::string::basic_string(v7, a2, a3);
  vuln((__int64)v7);
  v6 = std::operator<<<std::char_traits<char>>(&std::cout, "You said: ");
  v5 = std::operator<<<char>(v6, v7);
  std::operator<<<std::char_traits<char>>(v5, "\n");
  v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Bye~");
  std::operator<<<std::char_traits<char>>(v4, "\n");
  std::string::~string(v7);
  return 0LL;
}

vuln

vuln函数不难看出会接收字符数组,然后将接受的赋值给字符串对象v7,但是问题就出现在这个std::string::operator=函数上

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned __int64 __fastcall sub_4012C0(__int64 a1)
{
  __int64 v1; // rax
  char v3[72]; // [rsp+10h] [rbp-50h] BYREF
  unsigned __int64 v4; // [rsp+58h] [rbp-8h]
 
  v4 = __readfsqword(0x28u);
  v1 = std::operator<<<std::char_traits<char>>(&std::cout, "What do you want to say?");
  std::operator<<<std::char_traits<char>>(v1, "\n");
  std::operator>><char,std::char_traits<char>>(&std::cin);
  std::string::operator=(a1, v3);
  return __readfsqword(0x28u);
}

read_flag

后门函数,不多讲

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
unsigned __int64 read_flag()
{
  __int64 v0; // rax
  __int64 v1; // rax
  __int64 v2; // rax
  __int64 v3; // rax
  FILE *stream; // [rsp+8h] [rbp-58h]
  char s[72]; // [rsp+10h] [rbp-50h] BYREF
  unsigned __int64 v7; // [rsp+58h] [rbp-8h]
 
  v7 = __readfsqword(0x28u);
  v0 = std::operator<<<std::char_traits<char>>(&std::cout, "Success!");
  std::operator<<<std::char_traits<char>>(v0, "\n");
  stream = fopen("flag", "r");
  if ( stream )
  {
    fgets(s, 64, stream);
    v2 = std::operator<<<std::char_traits<char>>(&std::cout, "Flag: ");
    v3 = std::operator<<<std::char_traits<char>>(v2, s);
    std::operator<<<std::char_traits<char>>(v3, "\n");
  }
  else
  {
    v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Flag is missing. Please contact an admin.");
    std::operator<<<std::char_traits<char>>(v1, "\n");
  }
  return __readfsqword(0x28u);
}

调试部分

在调试过程中,发现了std::string::operator=函数的调用过程。
给出他的汇编指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Dump of assembler code for function _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEaSEPKc:
=> 0x00007ffff7d699a0 <+0>:    endbr64
   0x00007ffff7d699a4 <+4>:   push   rbp
   0x00007ffff7d699a5 <+5>:   mov    rbp,rsp
   0x00007ffff7d699a8 <+8>:   push   r12
   0x00007ffff7d699aa <+10>:  mov    r12,rsi
   0x00007ffff7d699ad <+13>:  push   rbx
   0x00007ffff7d699ae <+14>:  mov    rbx,rdi
   0x00007ffff7d699b1 <+17>:  mov    rdi,rsi
   0x00007ffff7d699b4 <+20>:  call   0x7ffff7ca2cf0 <strlen@plt>
   0x00007ffff7d699b9 <+25>:  mov    rdx,QWORD PTR [rbx+0x8]
   0x00007ffff7d699bd <+29>:  mov    rcx,r12
   0x00007ffff7d699c0 <+32>:  mov    rdi,rbx
   0x00007ffff7d699c3 <+35>:  mov    r8,rax
   0x00007ffff7d699c6 <+38>:  pop    rbx
   0x00007ffff7d699c7 <+39>:  xor    esi,esi
   0x00007ffff7d699c9 <+41>:  pop    r12
   0x00007ffff7d699cb <+43>:  pop    rbp
   0x00007ffff7d699cc <+44>:  jmp    0x7ffff7ca1560 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long)@plt>

可见它是先调用strlen函数计算rsi也就是用户输入的内容(PS:strlen存在\x00截断)。然后在进入_M_replace函数,该函数会将先将用户输入的字符串长度与0xf就行比较,如果大于就会创建一个堆来存储字符串;反之就会放在dst地址中。
图片描述
调用创建堆的过程如下,会调用一个_M_mutate函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
   0x7ffff7d69820 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long)+240>:  mov    rcx,r13
   0x7ffff7d69823 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long)+243>:  mov    rdi,rbx
   0x7ffff7d69826 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long)+246>:  call   0x7ffff7ca5150 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_mutate(unsigned long, unsigned long, char const*, unsigned long)@plt>
=> 0x7ffff7d6982b <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long)+251>:   jmp    0x7ffff7d697e3 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long)+179>
   0x7ffff7d6982d <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long)+253>:  nop    DWORD PTR [rax]
   0x7ffff7d69830 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long)+256>:  mov    edi,0xf
   0x7ffff7d69835 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long)+261>:  jmp    0x7ffff7d69783 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long)+83>
   0x7ffff7d6983a <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long)+266>:  nop    WORD PTR [rax+rax*1+0x0]
                                                              JUMP is NOT taken
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdb90 --> 0x403d98 --> 0x401190 (endbr64)
0008| 0x7fffffffdb98 --> 0x7ffff7e7a6a0 (:cin>:   0x00007ffff7e73870)
0016| 0x7fffffffdba0 --> 0x7fffffffddd8 --> 0x7fffffffe121 ("/home/u24/桌面/sictf/overflow")
0024| 0x7fffffffdba8 --> 0x1
0032| 0x7fffffffdbb0 --> 0x0
0040| 0x7fffffffdbb8 --> 0x403d98 --> 0x401190 (endbr64)
0048| 0x7fffffffdbc0 --> 0x7fffffffdc30 --> 0x7fffffffdcb0 --> 0x7fffffffdd50 --> 0x7fffffffddb0 --> 0x0
0056| 0x7fffffffdbc8 --> 0x40131c (mov    rax,QWORD PTR fs:0x28)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x00007ffff7d6982b in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long) () from /lib/x86_64-linux-gnu/libstdc++.so.6
gdb-peda$ find aaaaaaaaaaaaaaaa
Searching for 'aaaaaaaaaaaaaaaa' in: None ranges
Found 2 results, display max 2 items:
 [heap] : 0x4172b0 ('a' <repeats 16 times>)
[stack] : 0x7fffffffdbe0 ('a' <repeats 16 times>)

漏洞点

1.前面铺垫了那么多,其实它是根据strlen函数获取输入的长度,又由于strlen存在\x00截断,那么我们可以构造一个payload,在0-15字节之间填充一个\x00截断,就可以造成长度小于等于15;然后将内容直接复制到dst中而不是堆中。
2.由于输入也没有限制长度,所以可以溢出到dst修改他的指针值为stack_chk_fail@got,再把最开始的8字节修改为read_flag地址,那么就会把劫持stack_chk_fail到后门函数上,获取flag

exp在下面自取
之后又在网上找了一个C++学习网站,感觉还行
https://legacy.cplusplus.com/reference/string/basic_string/

上传的附件 exp.py
flag
overflow

最新回复:
wandering 2024-12-08 21:57:43
strlen存在的\x00截断,在特征码包括0x00的时候就坑了!
GUANZHI_ 2024-12-10 10:40:37
wandering strlen存在的\x00截断,在特征码包括0x00的时候就坑了!
关于这个特征码,师傅可不可以详细讲一下,我没太明白你的意思
GUANZHI_ 2025-01-04 23:13:24
后期回复下,这个机制是SSO机制。判断长度为16