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) |
看汇编代码,主要存在三个函数main、vuln、read_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 --> 0x10032| 0x7fffffffdbb0 --> 0x00040| 0x7fffffffdbb8 --> 0x403d98 --> 0x401190 (endbr64)0048| 0x7fffffffdbc0 --> 0x7fffffffdc30 --> 0x7fffffffdcb0 --> 0x7fffffffdd50 --> 0x7fffffffddb0 --> 0x00056| 0x7fffffffdbc8 --> 0x40131c (mov rax,QWORD PTR fs:0x28)[------------------------------------------------------------------------------]Legend: code, data, rodata, value0x00007ffff7d6982b 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.6gdb-peda$ find aaaaaaaaaaaaaaaaSearching for 'aaaaaaaaaaaaaaaa' in: None rangesFound 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/