---
# System prepended metadata

title: SekaiCTF

---

# 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: 