---
# System prepended metadata

title: CrewCTF 2024 Writeup
tags: [Reverse Engineering, CTF]

---

# CrewCTF 2024 Writeup

## Ta, Jr. Skid

```bash!
> ./main
Enter the flag: crew{0123456789}
Incorrect :(
```

IDA cho mã giả như sau:

```cpp=
__int64 __fastcall start(double a1)
{
  signed __int64 v1; // rax
  signed __int64 v2; // rax
  __int64 v3; // r10
  __int64 *v4; // r11
  __int64 *v5; // r12
  __int64 v6; // r13
  __m128i v7; // xmm1
  __int64 v8; // r10
  __int64 *v9; // r11
  unsigned __int64 v10; // rax
  __m128i v11; // xmm1
  signed __int64 v12; // rax
  signed __int64 v13; // rax
  signed __int64 v14; // rax
  signed __int64 v15; // rax
  __int64 *v17; // [rsp-10h] [rbp-10h]
  __int64 v18; // [rsp-8h] [rbp-8h]

  v1 = sys_write(1u, buf, 0x10uLL);
  v2 = sys_read(0, xmmword_426D40, 0x10uLL);
  sub_401159(a1, *(double *)xmmword_426D40, 0.0);
  while ( 1 )
  {
    v3 = 1024LL;
    v4 = (__int64 *)&unk_409040;
    v5 = 0LL;
    v6 = -1LL;
    do
    {
      if ( (unsigned __int64)v6 > *(_OWORD *)v4 )
      {
        v5 = v4;
        v6 = *v4;
      }
      v4 += 2 * v4[3] + 6;
      --v3;
    }
    while ( v3 );
    v7 = *((__m128i *)v5 + 2);
    v5[1] = 1LL;
    *((_OWORD *)v5 + 2) = 0LL;
    if ( v5 == (__int64 *)&unk_40A490 )
      break;
    v8 = v5[3];
    v9 = v5 + 6;
    do
    {
      v18 = v8;
      v17 = v9;
      v10 = v6 + v9[1];
      if ( *(_QWORD *)*v9 > v10 )
      {
        *(_QWORD *)*v9 = v10;
        sub_401159(a1, *(double *)v7.m128i_i64, 0.0);
      }
      v9 = v17 + 2;
      v8 = v18 - 1;
    }
    while ( v18 != 1 );
  }
  v11 = _mm_xor_si128(v7, (__m128i)xmmword_404030);
  if ( !_mm_testz_si128(v11, v11) )
  {
    v12 = sys_write(1u, &buf[25], 0xDuLL);
    v13 = sys_exit(1);
  }
  v14 = sys_write(1u, &buf[16], 9uLL);
  v15 = sys_exit(0);
  return sub_401159(a1, *(double *)v11.m128i_i64, 0.0);
}
```

Chương trình nhận input, sau khi biến đổi khá lằng nhằng thì so với `xmmword_404030`, nếu bằng nhau thì input đúng.

Hàm `sub_401159`

```cpp=
signed __int64 __fastcall sub_401159(double a1, double a2)
{
  __int64 v2; // r14
  void (__fastcall *v3)(double); // rbx
  signed __int64 v4; // rax
  __int64 v5; // rbx
  signed __int64 v6; // rax
  signed __int64 v7; // rax
  signed __int64 v8; // rax
  signed __int64 v9; // rax
  signed __int64 v10; // rax
  signed __int64 v11; // rax
  signed __int64 v12; // rax
  signed __int64 v14; // rax
  signed __int64 v15; // rax

  v3 = *(void (__fastcall **)(double))(v2 + 16);
  v4 = sys_fork();
  if ( !v4 )
  {
    v14 = sys_ptrace(PTRACE_TRACEME, 0LL, 0LL, 0LL);
    v15 = sys_kill(sys_getpid(), SIGTRAP);
    v3(a2);
    JUMPOUT(0x4036CCLL);
  }
  v5 = v4;
  v6 = sys_wait4(v4, 0LL, 0, 0LL);
  v7 = sys_ptrace(PTRACE_SINGLESTEP, v5, 0LL, 0LL);
  v8 = sys_wait4(v5, 0LL, 0, 0LL);
  v9 = sys_ptrace(PTRACE_SINGLESTEP, v5, 0LL, 0LL);
  v10 = sys_wait4(v5, 0LL, 0, 0LL);
  v11 = sys_ptrace(PTRACE_GETFPREGS, v5, 0LL, (unsigned __int64)&unk_426D50);
  *(_OWORD *)(v2 + 32) = xmmword_426DF0;
  v12 = sys_kill(v5, SIGKILL);
  return sys_wait4(v5, 0LL, 0, 0LL);
}
```

Hàm này `fork` ra một tiến trình con, cho chạy vào luồng `if`, sau đó dùng tiến trình cha để điểu khiển.

Cũng may hàm này không khó lắm.

Sau khi tiến trình con gọi `sys_kill(sys_getpid(), SIGTRAP)`, nó tạm dừng hoạt động.

Tiến trình cha sau đó `sys_ptrace` với tham số `PTRACE_SINGLESTEP` 2 lần, bắt tiến trình con thực thi thêm 2 dòng code asm nữa.
Tiếp theo, gọi `sys_ptrace` tham số `PTRACE_GETFPREGS`, lưu giá trị các thanh ghi dấu phẩy động vào `unk_426D50`.

`sys_kill` sau đó kill tiến trình con.
`sys_wait4` dùng để đợi tiến trình con hoàn thành công việc, giúp đồng bộ hóa 2 tiến trình.

Xem mã asm tương ứng.

`call rbx` sẽ được thực thi khi `PTRACE_SINGLESTEP` lần 1

![image](https://hackmd.io/_uploads/Sy3F0D0tR.png)

Không debug được tiến trình con, nhưng dễ truy được giá trị `rbx` ở đoạn code ngay bên trên

![image](https://hackmd.io/_uploads/rk2slO0FR.png)

Sau vài lần tiến hành debug, xem giá trị `rbx` thì thấy nó luôn trỏ đến các instruction trông như thế này

![image](https://hackmd.io/_uploads/ByDsG_RtA.png)

Có rất nhiều instruction mà `rbx` có thể trỏ đến, từ `.text:0000000000401281` đến `.text:00000000004036C3`

Đó cũng là dòng code mà `PTRACE_SINGLESTEP` lần 2 sẽ thực thi.

Xem giá trị `r15`, nó trỏ đến một mảng gồm 1024 'số' 128 bit

![image](https://hackmd.io/_uploads/r1akHu0Y0.png)

Vậy tóm lại hàm `sub_401159`:

- Move giá trị từ xmm1 sang xmm0
- Thực thi một trong các instruction, tính toán trên xmm0 và mảng dữ liệu cho sẵn
- Kết quả cuối cùng được lưu vào xmm0

Trở lại với hàm `start`

ChatGPT cho biết đây 'đây có vẻ là thuật toán tìm đường đi ngắn nhất trên graph, cụ thể hơn là thuật toán Dijkstra'

Có vẻ hợp lý, nhưng nhìn đống code lằng nhằng kia vẫn hơi khó hình dung.

Tôi chắc rằng phải có một kiểu dữ liệu nào đó cho các node trong cái graph kia để code nó dễ đọc hơn.

Nếu đây là Dijkstra thật thì `unk_409040` hẳn là node đầu của graph, `unk_40A490` là node cuối.

Sau khi ngồi debug, phân tích, dần thấy được cấu trúc của các node trong graph.

![image](https://hackmd.io/_uploads/HJUD3uCYC.png)

Không chắc 100%, nhưng đại khái:
- `qword` đầu tiên là khoảng cách đến node nguồn
- `qword` thứ 2 xác định node được ghé hay chưa
- `qword` thứ 3 là địa chỉ của một trong các instruction nói tới trong hàm `sub_401159`
- `qword` thứ 4 là số neighbor của node
- `oword` kế là giá trị được tính bởi `sub_401159`, sẽ được lưu vào `xmm1`
- Sau đó là mảng gồm link và weight đến các neighbor node

Từ đó tạo struct mới với ida

```asm!
00000000 node            struc ; (sizeof=0x60, mappedto_14)
00000000 path_len        dq ?
00000008 visited         dq ?
00000010 instruction     dq ?
00000018 num_links       dq ?
00000020 value           xmmword ?
00000030 neighbor1       dq ?                    ; offset
00000038 weight1         dq ?
00000040 neighbor2       dq ?                    ; offset
00000048 weight2         dq ?
00000050 neighbor3       dq ?                    ; offset
00000058 weight3         dq ?
00000060 node            ends
00000060
```

Sau khi define lại kiểu dữ liệu, rename biến, code dễ đọc hơn rất nhiều.

```cpp=
__int64 __fastcall start(double xmm_0)
{
  signed __int64 v1; // rax
  signed __int64 v2; // rax
  __int64 v3; // r10
  node *v4; // r11
  node *nearest; // r12
  __int64 total_len; // r13
  __int128 xmm_1; // xmm1
  __int64 num_of_links; // r10
  node *p_node1; // r11
  unsigned __int64 v10; // rax
  __m128i v11; // xmm1
  signed __int64 v12; // rax
  signed __int64 v13; // rax
  signed __int64 v14; // rax
  signed __int64 v15; // rax
  node *v17; // [rsp-10h] [rbp-10h]
  __int64 num_of_links_1; // [rsp-8h] [rbp-8h]

  v1 = sys_write(1u, buf, 0x10uLL);
  v2 = sys_read(0, input, 0x10uLL);
  run_instruction(xmm_0, *(double *)input);
  while ( 1 )
  {
    v3 = 1024LL;
    v4 = (node *)&start_node;
    nearest = 0LL;
    total_len = -1LL;
    do                                          // Find the nearest node that hasn't been visited
    {
      if ( (unsigned __int64)total_len > *(_OWORD *)&v4->path_len )
      {
        nearest = v4;
        total_len = v4->path_len;
      }
      v4 = (node *)((char *)v4 + 16 * v4->num_links + 48);// Below node
      --v3;
    }
    while ( v3 );
    xmm_1 = nearest->value;
    nearest->visited = 1LL;
    nearest->value = 0LL;
    if ( nearest == (node *)&end_node )         // Check if the nearest node is the end node
      break;
    num_of_links = nearest->num_links;
    p_node1 = (node *)&nearest->neighbor1;
    do                                          // Traverse all links of the nearest node
    {
      num_of_links_1 = num_of_links;
      v17 = p_node1;
      v10 = total_len + p_node1->visited;
      if ( *(_QWORD *)p_node1->path_len > v10 ) // Update the path length if the new path is shorter
      {
        *(_QWORD *)p_node1->path_len = v10;
        run_instruction(xmm_0, *(double *)&xmm_1);
      }
      p_node1 = (node *)&v17->instruction;
      num_of_links = num_of_links_1 - 1;
    }
    while ( num_of_links_1 != 1 );
  }
  v11 = _mm_xor_si128((__m128i)xmm_1, (__m128i)target);
  if ( !_mm_testz_si128(v11, v11) )
  {
    v12 = sys_write(1u, &buf[25], 0xDuLL);
    v13 = sys_exit(1);
  }
  v14 = sys_write(1u, &buf[16], 9uLL);
  v15 = sys_exit(0);
  return run_instruction(xmm_0, *(double *)v11.m128i_i64);
}
```

Tầm này chắc đúng là thuật toán tìm đường thật, hoặc là dạng custom một chút.

Rename `sub_401159`, thành `run_instruction`. Đặt lại kiểu dữ liệu cũng khiến code trông 'hợp lý' hơn.

```cpp=
signed __int64 __fastcall run_instruction(double a1, double a2)
{
  node *node; // r14
  void (__fastcall *instruction)(double); // rbx
  signed __int64 v4; // rax
  __int64 v5; // rbx
  signed __int64 v6; // rax
  signed __int64 v7; // rax
  signed __int64 v8; // rax
  signed __int64 v9; // rax
  signed __int64 v10; // rax
  signed __int64 v11; // rax
  signed __int64 v12; // rax
  signed __int64 v14; // rax
  signed __int64 v15; // rax

  instruction = (void (__fastcall *)(double))node->instruction;
  v4 = sys_fork();
  if ( !v4 )
  {
    v14 = sys_ptrace(PTRACE_TRACEME, 0LL, 0LL, 0LL);
    v15 = sys_kill(sys_getpid(), SIGTRAP);
    instruction(a2);
    JUMPOUT(0x4036CCLL);
  }
  v5 = v4;
  v6 = sys_wait4(v4, 0LL, 0, 0LL);
  v7 = sys_ptrace(PTRACE_SINGLESTEP, v5, 0LL, 0LL);
  v8 = sys_wait4(v5, 0LL, 0, 0LL);
  v9 = sys_ptrace(PTRACE_SINGLESTEP, v5, 0LL, 0LL);
  v10 = sys_wait4(v5, 0LL, 0, 0LL);
  v11 = sys_ptrace(PTRACE_GETFPREGS, v5, 0LL, (unsigned __int64)&unk_426D50);
  node->value = temp;
  v12 = sys_kill(v5, SIGKILL);
  return sys_wait4(v5, 0LL, 0, 0LL);
}
```

Do toàn bộ đồ thị đã được định sẵn từ đầu, quá trình tìm đường luôn xảy ra giống nhau, không cần biết input nhập vào là gì.

Vậy giờ tôi không quan tâm nó tìm đường như nào nữa.
Tập trung vào hoạt động cốt yếu nhất của chương trình, cách nó tính toán, kiểm tra kết quả.

```cpp=
__int64 __fastcall start(double xmm_0)
{
    xmm0 = input;
    xmm1 = input;
    xmm0 = xmm1;
    instruction xmm0, [r15 + offset];
    node->value = xmm0;
    while ( 1 )
    {
        xmm1 = nearest->value;
        if ( nearest == &end_node ) break;
        do                              // Traverse all links of the nearest node
        {
            if ( node->path_len > len ) // Update the path length if the new path is shorter
            {
                xmm0 = xmm1;
                instruction xmm0, [r15 + offset];
                node->value = xmm0;
            }
        }
        while ( num_of_links != 1 );
    }
    return xmm_1 == target;
}
```

Tóm lại:
- Từ `input`, khởi tạo `xmm0`, `xmm1`
- Thực thi instruction lần 1
- Trong những vòng lặp sau đó, nhiều lần chương trình thực thi instruction để tính `xmm0` dựa trên giá trị tại thanh ghi `xmm1`, nhưng chỉ có một lần `xmm1` được cập nhật mới, bằng một trong các giá trị `xmm0` mới đã được tính ra.

Đây là sơ đồ mô tả tổng quát cách chương trình biến đổi từ input thành kết quả.

![image](https://hackmd.io/_uploads/SyHQM9AFR.png)

Giờ việc cần làm là tìm ra chuỗi các instruction mà chương trình đã dùng.

Ý tưởng là cung cấp input cho chương trình, trong lúc thực thi lấy tất cả các giá trị `xmm1` trong chuỗi biến đổi.

Breakpoint tại `.text:00000000004010A6`, ngay sau khi `xmm1` được cập nhật mới.

![image](https://hackmd.io/_uploads/rkLDHcRKA.png)


```python
import idaapi
import idc
import ida_dbg

BREAKPOINT_ADDRESS = 0x00000000004010A6

def monitor():
    xmm1 = idc.get_reg_value("xmm1")
    xmm1 = int.from_bytes(xmm1, byteorder='little')

    print(hex(xmm1), end = ", ")
    
    idaapi.continue_process()

ida_dbg.add_bpt(BREAKPOINT_ADDRESS, 0, idaapi.BPT_SOFT)

class MyHandler(idaapi.DBG_Hooks):
    def dbg_bpt(self, tid, ea):
        monitor()
        return 0

debugger = MyHandler()
debugger.hook()

print(f"Breakpoint set at {BREAKPOINT_ADDRESS:X}")
print("Run the debugger to start monitoring")
```

Đến đây đã có được toàn bộ giá trị trong chuỗi biến đổi.

Nhưng chưa biết instruction nào đã được thực thi.

Tôi lại nhờ ida thực hiện breakpoint `.text:000000000040121B`, trong hàm `run_instruction`

![image](https://hackmd.io/_uploads/BJ39Bc0FA.png)

Lấy giá trị `xmm0`, lúc này đã được tính toán xong bằng instruction. Nếu `xmm0` thuộc chuỗi biến đổi, tìm instruction đã làm việc đó với `idc.generate_disasm_line` tại `[r14 + 10h]`

Vậy là đã có chuỗi các instruction chương trình đã dùng.

Có thể kiểm tra bằng cách code lại chương trình theo hướng đang làm.

Đến đây thì có thể tìm flag bằng cách đảo ngược quá trình.

Nếu muốn bạn có thể xem code tại [đây](https://github.com/scrymastic/CTFs/tree/main/CrewCTF-2024/Ta%2C%20Jr.%20Skid).

> Flag: crew{a6b4291e38}
