# SekaiCTF ## Algorthim multitools The challenge give us `libc.so.6`, a `.cpp` file and a binary This program can do some tasks ![](https://hackmd.io/_uploads/HJqZZnhah.png) Basically we will create some tasks, after that we choose `resume` to execute the algorthim ![](https://hackmd.io/_uploads/BkJfWn3an.png) This program uses coroutine in c++20 which is a function that can be stopped and be resumed * A function will be a coroutine when there're one of these in that function: `co_await`, `co_return`, `co_yield` Example: ```cpp= auto slow_algo_factory(SlowAlgo* algo) { algo->get_algo_params(); auto h = [algo_l = algo]() -> Task { co_await run_algo_async(algo_l); std::cout << "Result: " << algo_l->get_result() << std::endl; co_return; }; return h; } ``` * When calling `slow_algo_factory` it will run until meeting the `run_algo_async` function, it stops and will continue when it is resumed Um i don't understand how it works, everything in gg are confusing :face_with_head_bandage: so i use `ida` and `gdb` to debug myself ### Decompile program Since the program is kinda big, i will only explain about `slow_algo_factory`, but the `fast` is similar tho #### The coroutine Here the function in .cpp file ```cpp= if(choice == 1) { AES_CBC *cbc = new AES_CBC(); temp = new SavedTask(slow_algo_factory(cbc)(), cbc, cbc->get_name()); temp->call_count = 0; } ``` See it in `ida` ```cpp= case 1: v3 = (AES_CBC *)operator new(0xB8uLL); AES_CBC::AES_CBC(v3); v63 = v3; obj = slow_algo_factory((__int64)v3); hdl = slow_algo_factory(SlowAlgo *)::{lambda(void)#1}::operator()(&obj); Algo::get_name[abi:cxx11](name, v63); v4 = operator new(0x38uLL); SavedTask::SavedTask(v4, hdl, v63, name); v52 = v4; std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(name); *(_QWORD *)(v52 + 48) = 0LL; goto LABEL_20; ``` * First it just call constructor of `AES_CBC` class * Next, we will se the first `slow_algo_factory` ```cpp= __int64 __fastcall slow_algo_factory(__int64 a1) { (*(void (__fastcall **)(__int64))(*(_QWORD *)a1 + 16LL))(a1); return a1; } ``` * We pass the `AES_CBC` object to it before then it call `*(*(obj) + 16)(obj)` * It's the calling of `get_algo_params` function through vtable. ![](https://hackmd.io/_uploads/SygMS3h6n.png) * The next `slow_algo_factory(SlowAlgo *)::{lambda(void)#1}::operator()(&obj)` ```cpp= __int64 __fastcall slow_algo_factory(SlowAlgo *)::{lambda(void)#1}::operator()(__int64 obj_pointer) { __int64 return_object; // rbx __int64 v3; // [rsp+18h] [rbp-18h] v3 = operator new(0x40uLL); *(_BYTE *)(v3 + 42) = 1; *(_QWORD *)v3 = slow_algo_factory(SlowAlgo *)::{lambda(void)#1}::operator(); *(_QWORD *)(v3 + 8) = slow_algo_factory(SlowAlgo *)::{lambda(void)#1}::operator(); *(_QWORD *)(v3 + 32) = obj_pointer; return_object = promise::get_return_object((#3059 *)(v3 + 16)); *(_WORD *)(v3 + 40) = 0; slow_algo_factory(SlowAlgo *)::{lambda(void)#1}::operator()(v3); return return_object; } ``` * Here is how the coroutine work * First it will create an handler object which will handle the `resume` and `destroy` of a coroutine * We see that it sets the first `2` field to `2` functions, they have the same name but are different, i will call them `lambda1` and `lambda2` * Next it stores the address of object we pass to it which is a stack value * The `promise::get_return_object((#3059 *)(v3 + 16))` will return `v3`, nothing special * It stores some value such as `1`, `0` in object, they're will be used to determine the state of coroutine * Then it calls ` slow_algo_factory(SlowAlgo *)::{lambda(void)#1}::operator()(v3)`, it's `lambda1` * Before we go into `lambda1` i show you what the `handler` object looks like ![](https://hackmd.io/_uploads/HyH4vnnpn.png) * Pls don't notice to some garbage values which are in heap before :cry: ``` |lambda1 |lambda2 | |hdl |objaddr |0x0000000000010000 (state of coroutine) | | ``` `lambda1` function ```cpp= unsigned __int64 __fastcall slow_algo_factory(SlowAlgo *)::{lambda(void)#1}::operator()(_BYTE *a1) { int v1; // eax int v2; // eax __int64 v3; // rax __int64 v4; // rsi __int64 v5; // rbx __int64 v6; // rax __int64 v7; // rax _BYTE v9[40]; // [rsp+20h] [rbp-40h] BYREF unsigned __int64 v10; // [rsp+48h] [rbp-18h] v10 = __readfsqword(0x28u); if ( (*((_WORD *)a1 + 20) & 1) != 0 ) { v1 = *((unsigned __int16 *)a1 + 20); if ( v1 != 7 && (*((unsigned __int16 *)a1 + 20) > 7u || v1 != 5 && (*((unsigned __int16 *)a1 + 20) > 5u || v1 != 1 && v1 != 3)) ) { BUG(); } goto LABEL_28; } v2 = *((unsigned __int16 *)a1 + 20); if ( v2 != 6 ) { if ( *((unsigned __int16 *)a1 + 20) > 6u ) goto LABEL_18; if ( v2 != 4 ) { if ( *((unsigned __int16 *)a1 + 20) > 4u ) goto LABEL_18; if ( *((_WORD *)a1 + 20) ) { if ( v2 != 2 ) LABEL_18: BUG(); } else { // go to here if 0 // this just setup someting *((_QWORD *)a1 + 3) = std::__n4861::coroutine_handle<promise>::from_address(a1); a1[43] = 0; promise::initial_suspend((#3059 *)(a1 + 16)); if ( (unsigned __int8)std::__n4861::suspend_always::await_ready((#3048 *)(a1 + 44)) != 1 ) { *((_WORD *)a1 + 20) = 2; // set the state v3 = std::__n4861::coroutine_handle<promise>::operator std::__n4861::coroutine_handle<void>(a1 + 24); std::__n4861::suspend_always::await_suspend(a1 + 44, v3); return v10 - __readfsqword(0x28u); } } a1[43] = 1; // go to here if 2 std::__n4861::suspend_always::await_resume((#3048 *)(a1 + 44)); *((_QWORD *)a1 + 6) = run_algo_async(**((_QWORD **)a1 + 4));// set obj here, this will use the object stored in objaddress to call do_algo if ( (unsigned __int8)run_algo_async(Algo *)::awaitable::await_ready(a1 + 48) != 1 ) { *((_WORD *)a1 + 20) = 4; v4 = std::__n4861::coroutine_handle<promise>::operator std::__n4861::coroutine_handle<void>(a1 + 24); run_algo_async(Algo *)::awaitable::await_suspend(a1 + 48, v4);// call do_algo return v10 - __readfsqword(0x28u); } } // the second resume will go into here // it just print out the result and it still use the object stored in objaddr run_algo_async(Algo *)::awaitable::await_resume(a1 + 48); v5 = std::operator<<<std::char_traits<char>>(&std::cout, "Result: "); Algo::get_result[abi:cxx11](v9, **((_QWORD **)a1 + 4)); v6 = std::operator<<<char,std::char_traits<char>,std::allocator<char>>(v5, v9); std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>); std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v9); promise::return_void((#3059 *)(a1 + 16)); *(_QWORD *)a1 = 0LL; promise::final_suspend((#3059 *)(a1 + 16)); if ( (unsigned __int8)std::__n4861::suspend_always::await_ready((#3048 *)(a1 + 56)) != 1 ) { *((_WORD *)a1 + 20) = 6; v7 = std::__n4861::coroutine_handle<promise>::operator std::__n4861::coroutine_handle<void>(a1 + 24); std::__n4861::suspend_always::await_suspend(a1 + 56, v7); return v10 - __readfsqword(0x28u); } } std::__n4861::suspend_always::await_resume((#3048 *)(a1 + 56)); LABEL_28: // // go to here when user calls destroy if ( a1[42] ) operator delete(a1); return v10 - __readfsqword(0x28u); } ``` * To simplify it, i won't go detail into this code * The function will have `3` times called to be doned 1. First it will be called by the function `slow_algo_factory`. It just set up something in object 2. Second call when user resumes it will call `do_algo` by using the object stored in `objaddr` 3. Third call when user resumes again. It will call `get_result` (still uses the object in `objaddr`) and prints it 4. The `lambda2` sets up the `state` then call `lambda1` so that it will `delete` the `handler` object ```cpp= __int64 __fastcall slow_algo_factory(SlowAlgo *)::{lambda(void)#1}::operator()(__int64 a1) { *(_WORD *)(a1 + 40) |= 1u; return slow_algo_factory(SlowAlgo *)::{lambda(void)#1}::operator()(a1); } ``` * Now back to `main` ```cpp= Algo::get_name[abi:cxx11](name, v63); v4 = operator new(0x38uLL); SavedTask::SavedTask(v4, hdl, v63, name); v52 = v4; std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(name); *(_QWORD *)(v52 + 48) = 0LL; goto LABEL_20; ``` * It creates `SavedTask` object will `handler`, `AES_CBC`(a `Algo` object), and a `name` Here the constructor of `SavedTask` ```cpp= __int64 __fastcall SavedTask::SavedTask(__int64 a1, __int64 a2, __int64 a3, __int64 a4) { Task::Task((#3064 *)a1); std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(a1 + 16); *(_QWORD *)a1 = a2; *(_QWORD *)(a1 + 8) = a3; return std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator=(a1 + 16, a4); } ``` * The object will look like ``` |hdl |obj |nameobj | | | |0(count) | ``` * Last it will store the object in a vector ```cpp= tasks.push_back(temp); ``` #### The bug * The bug here is that every `slow_Algo` object (or `fast_Algo`) will be stored in the same address before passed to `slow_algo_factory` to create a `handler` ```cpp= v3 = (AES_CBC *)operator new(0xB8uLL); AES_CBC::AES_CBC(v3); v63 = v3; obj = slow_algo_factory((__int64)v3); ``` * The `obj` here you see that it's a value in stack. So the `objaddr` field in `handler` will be the same for all handlers of `slow_Algo`. When we `destroy` a task ```cpp= v43 = (_QWORD **)std::vector<SavedTask *,std::allocator<SavedTask *>>::operator[](v64, index); std::__n4861::coroutine_handle<promise>::destroy(*v43);// call lambda2 to delete hdl v44 = (std::stop_source **)std::vector<SavedTask *,std::allocator<SavedTask *>>::operator[](v64, index); v45 = *v44; if ( *v44 ) { SavedTask::~SavedTask(*v44); // call destructor obj operator delete(v45, 0x38uLL); } i_1 = index; v53 = std::vector<SavedTask *,std::allocator<SavedTask *>>::begin(v64); obj = __gnu_cxx::__normal_iterator<SavedTask **,std::vector<SavedTask *,std::allocator<SavedTask *>>>::operator+( &v53, i_1); // [1] __gnu_cxx::__normal_iterator<SavedTask * const*,std::vector<SavedTask *,std::allocator<SavedTask *>>>::__normal_iterator<SavedTask **>( &hdl, &obj); std::vector<SavedTask *,std::allocator<SavedTask *>>::erase(v64, hdl); v47 = std::operator<<<std::char_traits<char>>(&std::cout, "Done!"); ``` * It calls some `destructor` but look at `[1]`. The `obj` here is changed to a `SavedTask` object. It's supposed to be a `Algo` object. That's the first bug * The `obj` is set to `vector[i]` then the program erases the object in index `i`. What if we `destroy` the last object ? ``` Before erasing the n object which is the last vector[0] = savedtask vector[1] = savedtask . . . vecotr[n] = savetask <- obj After erasing vector[0] = savedtask vector[1] = savedtask . . . vecotr[n] = savedtask <- obj It's the same but savedtask[n] is now freed so that's use-after-free bug ``` * More interesting, if we create so many `SavedTask` so the old chunk vector points to won't have enough space so it will `free` the old chunk and `malloc` a new chunk but the `objaddr` still points to the old chunk ### Exploit #### Leak libc and heap * String object ``` 0 8 0x10 pointer to string |len of string |string if len < 0x10 ``` * The pointer will point to next `0x10` bytes if string's length <= `0x10` (include `null` byte) else it will point to an allocated chunk * When we use `cout << string_object` it will read the length of string then print that number of bytes. * To leak libc, i will fake the `len` field Here's how it works * First i create a `AES_CBC` object with a large string so the program will `malloc` a big chunk. This string will be freed somehow * Next i create many tasks so `vector` will allocate a new big chunk, it will use my previous chunk ![](https://hackmd.io/_uploads/rksCka2p2.png) * The `get_result` will use `string` object in `Algo + 0x48` so the `size` field here is `0x4b4b4b4b4b4b4b4b`. Here i `destroy(0)` so the `objaddr` is `0x5649ebdd040a0` * Abuse the `size` to a big numbers to print `libc` and `heap` ![](https://hackmd.io/_uploads/BkYIlThp3.png) ![](https://hackmd.io/_uploads/HyC8l6362.png) * I get libc since there's some unsorted bin in heap #### Fake vtable and one gadget * Use the previous method, i make `objaddr` points to a freed chunk * Now to craft this chunk, i will create a fast algo task to not change the value at `objaddr`. Calculate the right `size` to trigger the right chunk ```python= linear(0x20*2, list_) ``` * So to trigger code execution i craft a vtable to call `one_gadget` * The first `resume` will call `do_algo`. It's offset in vtable ![](https://hackmd.io/_uploads/SyJHzphp3.png) * When i write `one_gadget` to `do_algo` offset the constraints isn't met. Then i found a useful gadget ``` 0x000000000008ab82 : xor edx, edx ; mov rdi, rbp ; call qword ptr [rax + 0x80] ``` * Since the `rax` points to our crafted chunk i can use it * So here's the crafted chunk ![](https://hackmd.io/_uploads/Sk04mTnan.jpg) Then i get a shell ![](https://hackmd.io/_uploads/B13Om6hp2.png) But since the server is closed, i don't if it works in server :crying_cat_face: