A RABBIT HOLE, CLEANUP, AUGMENTATION AND REPLACEMENT JOSTALY TECHNOLOGIES John SETH Thielemann AUGUST, 2X[] 2024 aeb0818e0167c74f17e34f1eac4827c4d7459a82a958c93051a2223cae40fc88 zlib.git.ygg.tgz 0) NEWS OF INTEREST A few particular articles/news pieces that caught my eye during the month: 09 AUG: ADT hacked, AMD sinkclose 12 AUG: RISC-VUZZ 14 AUG: Microsoft Remote code execution vulnerability 15 AUG: Solar Winds CVE 18 AUG: Microsoft Authenticator overwriting MFA accounts 21 AUG: SLACK Exfiltration 26 AUG: Solar Winds CVE-2024-28987 27 AUG: Malware Infiltrates Pidgin messenger's repository Versa Director, suspect: Volt Typhoon 1) ARTICLE This article explores what started out as an augmentation to zlib and how it transformed to a replacement of standard C with a toggle switch. Nothing is perfect, the descriptions, etc. are not intended in a negative/positive respect, it's objective. Even though C is a subset of C++, there's still some challenges to this platonic description. A perfect example is K&R C syntax for function declaration and the resulting errors with a 'bleeding edge' version of GNUC toolchain. I wound up going in a number of loops with this particular codebase. 'make test' might work on a release, a tool within tests might not, or an external verification of the compressed data with gzip fails to decompress, etc. The particular master clone branch available did happen to pass make tests, test applications worked, and external tools could also successfully verify generated content. This is a good starting point. Configuration macros are insane. I think most programmers would generally agree that is a common pain point when working with an unfamiliar code base (and in all honesty, a familiar one too). These also open up code paths that might not fall in the general testing release cycle, depending upon how your package is configured. From an attack surface perspective it's a nightmare. Regardless as to if the code is flawless or not. I spent a significant amount of time reducing macros, which has implications for dependent software as well as potential subversive replacement build with code paths turned on, etc. The resulting attached codebase has the .git metadata and all modifications. At this point I wanted to go further than just augmentation of memory management. Many embedded environments have their own libc implementation, there's various libc implementations out there was well (Each one generally has it's own set of challenges to build). The makefile has a toggle to turn on fortifications / library use of JOSTALY Technology code and switch from the system libc, toolchain libc, etc. to our backend libc. This initially posed some challenges: what about the less desirable functions like printf, fprintf, etc.? What's in a name? What's in a function? The general description of a libc function is such that it winds up doing a system call(s) to perform the requested operation. A deep dive into this is probably interesting, how many reading have dived in this far? It's worthwhile to support older systems, many libcs are built against older headers for forward compatibility. Two particular considerations must be taken into account: What's the syscall number and how do registers need to be setup to do the system call? To be compatible with linux there's two files of interest: asm/unistd_64.h and kernel/entry_64.S. The former just providing a numerical enumeration of the system call numbers, the latter providing the register state upon entry to the system. The entry happens with the syscall instruction, classic-style is int 0x80. From entry_64.S: * 64bit SYSCALL saves rip to rcx, clears rflags.RF, then saves rflags to r11, * then loads new ss, cs, and rip from previously programmed MSRs. * rflags gets masked by a value from another MSR (so CLD and CLAC * are not needed). SYSCALL does not save anything on the stack * and does not change rsp. * * Registers on entry: * rax system call number * rcx return address * r11 saved rflags (note: r11 is callee-clobbered register in C ABI) * rdi arg0 * rsi arg1 * rdx arg2 * r10 arg3 (needs to be moved to rcx to conform to C ABI) * r8 arg4 * r9 arg5 * (note: r12-r15,rbp,rbx are callee-preserved in C ABI) * * Only called from user space. * * When user can change pt_regs->foo always force IRET. That is because * it deals with uncanonical addresses better. SYSRET has trouble * with them due to bugs in both AMD and Intel CPUs. What does a putchar('A') syscall look like then? All the way down on a 64-bit x86 platform: rax = 1, rdi = &char variable, rsi = 1, syscall You can find glibc's implementation within: sysdeps/unix/sysv/linux/x86_64/sysdep.h What does this look like for us currently? 78 static NOINLINE_NOCLONE addr_t syscall( 79 addr_t number, addr_t a0, addr_t a1 = 0, addr_t a2 = 0, addr_t a3 = 0, addr_t a4 = 0, addr_t a5 = 0) noexcept { 80 +------ 27 lines: (linux kernel support for >= v2.6.38) kernel/entry_64.S--------------------------------------------- 107 addr_t result = number; 108 register addr_t ra3 asm("r10") = a3; 109 register addr_t ra4 asm("r8") = a4; 110 register addr_t ra5 asm("r9") = a5; 111 asm volatile("syscall\n\t" 112 : "+a" (result) 113 : "D"(a0), "S"(a1), "d"(a2), "r"(ra3), "r"(ra4), "r"(ra5) 114 : "rcx"); 115 return result; 116 } constexpr static addr_t NR_write = 1; static ssize_t sys_write(int fd, const void* buff, size_t count) noexcept { return syscall(NR_write, fd, reinterpret_cast(buff), count); } Back to zlib. A toggle for libc replacement also requires replacing the libc startup functions generally within the crt[*].o object files. A little bit of extra code was added, the yggCrt1.o object. I'd like my cake (augmentation), still eat it too (replacement), and with a toggle during the build have the original to compare against. I haven't removed all macros. The attachment is set to have the toggle off, such that a `make clean; make test` will build. The binaries sitting in the directory is the ygg fortified binary, stripped, and minigzip works with compression/decompression with a simple test. What remains? It's nice to see the simplified version of the source. It's probably hardened up a bit leaving little room for compile time switch modifications, feature set reporting is bogus, unused code removed, headers unrecognizable. Now the question is: Is this just comparing apples to oranges? The interesting deep dive and real-world practical application is now a revert, bare-minimal set of changes to build, add the toggle. From a gaming perspective and adversarial mindset, with tests/utils installed side-by-side with various applications, that are not tested themselves, could the trojans already be inside the gates in many cases?