Showing posts with label programming. Show all posts
Showing posts with label programming. Show all posts

Monday, March 10, 2025

Subshells in Powershell

Previously, I wrote a post about how it's possible to create a "subshell" in Windows analogous to the subshell feature available in Bash on Linux—because Microsoft Windows doesn't actually have native subshell capability the same way that Linux does. The script below is an improvement on the same previous method of using the .NET System.Diagnostics trick. But this new version correctly redirects the standard output:

$x = New-Object System.Diagnostics.ProcessStartInfo
$x.FileName = "cmd.exe"
$x.Arguments = "/c echo %PATH%"
$x.UseShellExecute = $false
$x.RedirectStandardOutput = $true  
$x.EnvironmentVariables.Remove("Path")
$x.EnvironmentVariables.Add("PATH", "C:\custom\path")
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $x
$p.Start() | Out-Null
$output = $p.StandardOutput.ReadToEnd()
$p.WaitForExit()
Write-Output $output

Real-World Example

$customPath2 = "C:\custom\path\2"

$data = @{
    Path = $customPath2  
    Timestamp = Get-Date
    ProcessID = $PID  
}

$x = New-Object System.Diagnostics.ProcessStartInfo
$x.FileName = "cmd.exe"
$x.Arguments = "/c echo %PATH%"
$x.UseShellExecute = $false
$x.RedirectStandardOutput = $true
$x.RedirectStandardError = $true

$data["SubshellError"] = $stderr

$x.EnvironmentVariables.Remove("Path")
$x.EnvironmentVariables.Add("PATH", $customPath2)

$p = New-Object System.Diagnostics.Process
$p.StartInfo = $x
$p.Start() | Out-Null

$output = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd() 
$p.WaitForExit()

$data["SubshellOutput"] = $output
$data["SubshellError"] = $stderr

$data
$data

Name                           Value
----                           -----
ProcessID                      11852
Path                           C:\custom\path\2
SubshellOutput                 C:\custom\path\2...
SubshellError
Timestamp                      3/10/2025 7:05:01 PM

Wednesday, February 19, 2025

Searching for Elf Magic

Elfland

Just as Windows has its various executable formats, so too does Linux. In this land, there are Elfs, also known as executable and linkable format files. If we look at elf.h, we can see the structures which constitute the ELF format:

#define EI_NIDENT       16
 
typedef struct {
        unsigned char   e_ident[EI_NIDENT]; 
        Elf32_Half      e_type;
        Elf32_Half      e_machine;
        Elf32_Word      e_version;
        Elf32_Addr      e_entry;
        Elf32_Off       e_phoff;
        Elf32_Off       e_shoff;
        Elf32_Word      e_flags;
        Elf32_Half      e_ehsize;
        Elf32_Half      e_phentsize;
        Elf32_Half      e_phnum;
        Elf32_Half      e_shentsize;
        Elf32_Half      e_shnum;
        Elf32_Half      e_shstrndx;
} Elf32_Ehdr;

typedef struct {
        unsigned char   e_ident[EI_NIDENT]; 
        Elf64_Half      e_type;
        Elf64_Half      e_machine;
        Elf64_Word      e_version;
        Elf64_Addr      e_entry;
        Elf64_Off       e_phoff;
        Elf64_Off       e_shoff;
        Elf64_Word      e_flags;
        Elf64_Half      e_ehsize;
        Elf64_Half      e_phentsize;
        Elf64_Half      e_phnum;
        Elf64_Half      e_shentsize;
        Elf64_Half      e_shnum;
        Elf64_Half      e_shstrndx;
} Elf64_Ehdr;
e_ident

Straightforward enough? This is how the kernel sees Executable Linux Files. Here's a quick rundown of what each of these field names formally represent within the ELF format:

  • e_ident: stores the file's identification info, like magic number, class, and endianness.
  • e_type: tells the file's type (e.g., executable, shared library).
  • e_machine: describes the architecture (e.g., x86, ARM).
  • e_version: version of the ELF format.
  • e_entry: address where the program starts running.
  • e_phoff: offset to the program header table.
  • e_shoff: offset to the section header table.
  • e_flags: flags for specific machine behaviors.
  • e_ehsize: size of the ELF header.
  • e_phentsize: size of each program header entry.
  • e_phnum: number of program header entries.
  • e_shentsize: size of each section header entry.
  • e_shnum: number of section headers.
  • e_shstrndx: index to the section name string table.

If we use readelf we can see this for ourselves, along with program and section headers, offsets, relocations, symbol tables, and more.

$ readelf -a /usr/bin/gzip 
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0x11fe0
  Start of program headers:          52 (bytes into file)
  Start of section headers:          71084 (bytes into file)
  Flags:                             0x5000400, Version5 EABI, hard-float ABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         9
  Size of section headers:           40 (bytes)
  Number of section headers:         28
  Section header string table index: 27
$ readelf -l /usr/bin/ls

Elf file type is DYN (Position-Independent Executable file)
Entry point 0x6d30
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000002d8 0x00000000000002d8  R      0x8
  INTERP         0x0000000000000318 0x0000000000000318 0x0000000000000318
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000000036f8 0x00000000000036f8  R      0x1000
  LOAD           0x0000000000004000 0x0000000000004000 0x0000000000004000
                 0x0000000000014db1 0x0000000000014db1  R E    0x1000
  LOAD           0x0000000000019000 0x0000000000019000 0x0000000000019000
                 0x00000000000071b8 0x00000000000071b8  R      0x1000
  LOAD           0x0000000000020f30 0x0000000000021f30 0x0000000000021f30
                 0x0000000000001348 0x00000000000025e8  RW     0x1000
  DYNAMIC        0x0000000000021a38 0x0000000000022a38 0x0000000000022a38
                 0x0000000000000200 0x0000000000000200  RW     0x8
  NOTE           0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000030 0x0000000000000030  R      0x8
  NOTE           0x0000000000000368 0x0000000000000368 0x0000000000000368
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_PROPERTY   0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000030 0x0000000000000030  R      0x8
  GNU_EH_FRAME   0x000000000001e170 0x000000000001e170 0x000000000001e170
                 0x00000000000005ec 0x00000000000005ec  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000020f30 0x0000000000021f30 0x0000000000021f30
                 0x00000000000010d0 0x00000000000010d0  R      0x1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 
   03     .init .plt .plt.got .plt.sec .text .fini 
   04     .rodata .eh_frame_hdr .eh_frame 
   05     .init_array .fini_array .data.rel.ro .dynamic .got .data .bss 
   06     .dynamic 
   07     .note.gnu.property 
   08     .note.gnu.build-id .note.ABI-tag 
   09     .note.gnu.property 
   10     .eh_frame_hdr 
   11     
   12     .init_array .fini_array .data.rel.ro .dynamic .got 

I won't be covering all of the ELF sections in this blog post. For a comprehensive breakdown of Linux sections, I recommend this page: https://stevens.netmeister.org/631/elf.html

In this post, we're going to write x64 assembly to search for ELF magic numbers: 7f 45 4c 46.

So, we'll begin by loading all of the strings we need for our program into the .data section. We'll setup variables for the magic number, success message, usage message, error message, and a fail message.

section .data
    elf_magic db 0x7F, 'E', 'L', 'F'   ; ELF magic number
    msg db "[+] ELF magic detected", 10 ; msg to print
    msg_len equ $ - msg
    usage_msg db "Usage: ./elf_check <filename>", 10
    usage_msg_len equ $ - usage_msg
    error_msg db "Error opening file. Please supply a valid file path.", 10
    error_msg_len equ $ - error_msg
    not_elf db "[-] No ELF magic detected", 10
    not_elf_len equ $ - not_elf

section .bss
    buffer resb 4            ; allocate 4 bytes for our read buffer

section .text
    global _start

We'll set the strings and their lengths. elf_magic db defines our byte signature, while the correlated msg db holds our success msg string and an ASCII newline (10). We also define its length with equ, defining the msg_len as a constant. The $ delimiter indicates the current location and subtracts it from the correlating string, e.g. msg_len, yielding the length of the string of the msg variable. We repeat this design pattern for the other strings we use in our program.

We also create a .bss section -- the block starting symbols -- which hold our statically allocated variable resb. This value in particular allocates a single byte -- which we allocate four of -- for the purpose of reading and looping through a buffer, byte by byte, later in our program.

Each of these directives communicate to the compiler and linker the structure of our executable. For example, the .text subsection global _start tells the linker (ld) where our program actually begins.

Next we'll use x86_64 instructives to communicate that we would like to open a file. If no file path is detected, we create a jump via jl (jump if less) to a usage message indicating that the program requires a valid file path.

With a valid file path supplied, we handle its file descriptor and prepare to process it. Some familiarity with asm is assumed. But I've tried to make the comments clear:

We use the open system call and the O_RDONLY flag to open our file. After setting the arguments, we invoke the call and test if it was successful. If the test is negative, we head for the exit, calling another variation of jump (js; jump if sign flag is set) and bailing out to the .error_opening message.

But if a valid file descriptor is found, we begin processing it and setup to enter a loop to compare the bytes of the supplied file to the ELF magic byte array we stashed in our .data section.

_start:
    mov rdi, [rsp]          ; stack pointer to rdi for our argument
    cmp rdi, 2              ; compare argc to 2 (our executable + 1 argument)
    jl .usage_msg           ; no arg? jump to usage_msg; else, get the filename from argv[1]
    mov rdi, [rsp + 16]     ; rsp+16 (argv1) to rdi

    ; open file (open syscall)
    mov rsi, 0x0            ; rsi, O_RDONLY
    mov rdx, 0              ; rdx for mode, unused
    mov rax, 2              ; syscall number for open
    syscall                 ; open(argv[1], O_RDONLY)

    ; success? check file descriptor, rax 
    test rax, rax           ; check if fd is valid
    js .error_opening       ; jump to .error_opening if open failed

    ; save file descriptor in rbx
    mov rbx, rax            ; rbx file descriptor from rax to rbx

    ; read first 4 bytes from the file (read syscall)
    mov rdi, rbx            ; rdi, file descriptor
    lea rsi, [buffer]       ; load buffer to rsi to store bytes
    mov rdx, 4              ; arg to read four bytes
    mov rax, 0              ; syscall number for read
    syscall                 ; read(file_desc, buffer, 4)
    mov rdi, elf_magic      ; rdi to point to the ELF magic number
    mov rcx, 4              ; set loop counter to 4 bytes
    jmp .compare_loop       ; jump to .compare_loop

When we begin the "read first 4 bytes" portion of the code, we're setting up the arguments, which adheres to the Linux x86_64 calling convention.

Afterward, we invoke a syscall. This is a call to the read() function which does read(fd, buffer, 4).

The system call to read() is really doing this:

read(rdi, rsi, rdx) <------> read(file_descriptor, buffer, 4)

Lastly, we do mov rdi, elf_magic to move the byte signature we're looking for, e.g. elf_magic, to the rdi register, and prepare the rcx register as a loop counter by setting it to 4 just before jumping into .compare_loop.

If you're not familiar with calling conventions, you can read more about them here: "Arguments Passing in Linux"

Register Argument User Space Argument Kernel Space
%rax Not Used System Call Number
%rdi Argument 1 Argument 1
%rsi Argument 2 Argument 2
%rdx Argument 3 Argument 3
%r10 Not Used Argument 4
%r8 Argument 5 Argument 5
%r9 Argument 6 Argument 6
%rcx Argument 4 Destroyed
%r11 Not Used Destroyed

Next, we want to compare the bytes that we have read from the buffer to the ELF magic bytes we have stored in the .data section of our program. Note that registers such as al and bl are registers for accessing single bytes.

Here's a chart of the registers and their related counterparts. Note: these registers can also be accessed and used independently. One need not use rsi and sil together to access single bytes. One can imagine zig-zagging across the chart below for reads, writes, compares, etc.

For example, if I want to move a byte from rsi to the al register for use in a loop, that is acceptable. But one can of course just use the default associated sil register.

8-byte Register Bytes 0-3 Bytes 0-1 Byte 0
%rax %eax %ax %al
%rcx %ecx %cx %cl
%rdx %edx %dx %dl
%rbx %ebx %bx %bl
%rsi %esi %si %sil
%rdi %edi %di %dil
%rsp %esp %sp %spl
%rbp %ebp %bp %bpl
%r8 %r8d %r8w %r8b
%r9 %r9d %r9w %r9b
%r10 %r10d %r10w %r10b
%r11 %r11d %r11w %r11b
%r12 %r12d %r12w %r12b
%r13 %r13d %r13w %r13b
%r14 %r14d %r14w %r14b
%r15 %r15d %r15w %r15b

Our compare loop will use the default associated registers which are shown the chart, e.g. rsi and sil -- as well as rdi and dil:

.compare_loop:
    mov sil, byte [rsi]     ; load byte from buffer
    mov dil, byte [rdi]     ; load byte from elf_magic
    cmp sil, dil            ; compare bytes
    jne .not_elf            ; not equal, not ELF
    inc rsi                 ; move to next byte in buffer
    inc rdi                 ; move to next byte in elf_magic
    loop .compare_loop      ; repeat until counter is 0

    ; if compare_loop completes, the file has ELF magic
    ; print msg
    mov rdi, 1              ; file descriptor 1, stdout, to rdi
    lea rsi, [msg]          ; load address of msg to rsi 
    mov rdx, msg_len        ; length of msg to rdx
    mov rax, 1              ; syscall number for write
    syscall                 ; write(1, msg, msg_len)

    ; exit syscall
    mov rax, 60             ; syscall number for exit
    xor rdi, rdi            ; exit code 0, success
    syscall                 ; exit(0)

This recursively iterates through the bytes -- looping four times courtesy of the rcx counter we set in the _start function. As the loop runs, it calls the inc, e.g. increment, to move forward through the bytes pointed at by rsi and rdi

If the bytes that we load from the buffer match the bytes in elf_magic, then we go forward -- setting up to print the success message by copying the file descriptor for standard output to the rdi register, calling lea to load the effective address of our msg in bracket notation, and the corresponding msg_len we set in the .data section earlier. Last, we invoke our syscall. If all goes right, we should see the message: "[+] ELF magic detected"

However, if .compare_loop iterates and a byte doesn't match the ELF magic signature, then we jump via jne (jump-if-not-equal) register to the .not_elf function.

Below is our .not_elf function. We'll reuse this epilogue design pattern for exiting out of our program two more times, for both our .error_opening and .usage_message functions.

.not_elf:
    ; print .not_elf message
    mov rdi, 1              ; file descriptor 1, stdout, to rdi
    lea rsi, [not_elf]      ; load address of not_elf msg to rsi
    mov rdx, not_elf_len    ; not_elf msg length to rdx
    mov rax, 1              ; syscall number for write
    syscall                 ; write(1, "not_elf", len)

    ; exit syscall
    mov rax, 60             ; syscall number for exit
    mov rdi, 1              ; exit code 1, failure
    syscall                 ; exit(1)

If we use nasm to compile, and then use ld to link our executable, we can test to see if it successfully finds ELF file signatures.

$ nasm -f elf64 -o elfCheck.o elfCheck.asm 
$ ld -s -o elfCheck elfCheck.o
$ ./elfCheck /etc/hostname
[-] No ELF magic detected
$ ./elfCheck /usr/bin/gzip
[+] ELF magic detected

It works! But wait, what if we spoof an ELF file? Then our ELF magic checker has been foiled!

$ echo -n -e '\x7f\x45\x4c\x46' > spoofed_elf
$ xxd spoofed_elf
00000000: 7f45 4c46                                .ELF
$ ./elfCheck spoofed_elf 
[+] ELF magic detected

Rats. We'll have to build an ELF validator that formally verifies Executable Linux Files instead of just searching for magic numbers.

Searching for ELF magic with assembly

Monday, December 16, 2024

Inlined vs Non-inlined Code

In various programming languages, it is possible to inline code. For example, with C++, one can use design patterns or keywords like extern, static, or inline to suggest to the compiler that code should be inlined. Non-inlined code can generate extra, unnecessary function calls, as well as calls to the global offset table (GOT) or procedure linkage table (PLT).

Inlining can reduce runtime overhead and eliminate function call indirection, though it may increase code size due to duplication, since it must be copied across all translation units where it is used.

Consider the following non-inlined code:

int g;

int foo() { 
    return g; 
    }

int bar() {
    g = 1;
    foo(); 
    return g;
}

If we compile the code like so with gcc -O3 -Wall -fPIC -m32, we can observe in the assembly that indeed, this code was not inlined. There are still explicit function calls, and extra calls to the GOT and PLT.

However, it's important to note that at higher optimization levels like -O2 or -O3, the compiler may inline small functions automatically—even without the inline keyword.

foo():
        call    __x86.get_pc_thunk.ax
        add     eax, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
        mov     eax, DWORD PTR g@GOT[eax]
        mov     eax, DWORD PTR [eax]
        ret
bar():
        push    esi
        push    ebx
        call    __x86.get_pc_thunk.bx
        add     ebx, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
        sub     esp, 4
        mov     esi, DWORD PTR g@GOT[ebx]
        mov     DWORD PTR [esi], 1
        call    foo()@PLT
        mov     eax, DWORD PTR [esi]
        add     esp, 4
        pop     ebx
        pop     esi
        ret
g:
        .zero   4
__x86.get_pc_thunk.ax:
        mov     eax, DWORD PTR [esp]
        ret
__x86.get_pc_thunk.bx:
        mov     ebx, DWORD PTR [esp]
        ret

Now consider the use of the inline keyword.

int g;

inline int foo() { 
    return g; 
    }

int bar() {
    g = 1;
    foo(); 
    return g;
}

In this example, the aforementioned code significantly reduces the amount of instructions generated by GCC—the function foo has essentially been merged into bar.

If this were a small program that made repeated calls to these functions, this optimization could reduce overhead and increase performance.

bar():
        call    __x86.get_pc_thunk.ax
        add     eax, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
        mov     eax, DWORD PTR g@GOT[eax]
        mov     DWORD PTR [eax], 1
        mov     eax, 1
        ret
g:
        .zero   4
__x86.get_pc_thunk.ax:
        mov     eax, DWORD PTR [esp]
        ret

The inline keyword is a hint to the compiler. The compiler may not always follow such suggestions—it is context dependent. Other functions may be inlined and optimized by default even without such hints.

But if you're experimenting with inline code, caveats may apply[1][2][3].

Sunday, May 26, 2024

Using Reflection in Go

Have you ever been writing Go and needed to quickly find all the possible methods or fields you can use with a particular function?

Tuesday, November 28, 2023

mapcidr patch

Project Discovery’s mapcidr had a bug when converting IP addresses. The “-ip-format” flag did not properly work for one of the cases. For example, echo '127.0.0.1' | mapcidr -ip-format 5 would incorrectly return the integer representation or decimal value 281472812449793, when it should have returned the decimal value 2130706433. The problem could be seen in the Go function here which uses functionality imported from the math library.

func IPToInteger(ip net.IP) (*big.Int, int, error) {
	val := &big.Int{}
	val.SetBytes([]byte(ip))

	if len(ip) == net.IPv4len {
		return val, 32, nil //nolint
	} else if len(ip) == net.IPv6len {
		return val, 128, nil //nolint
	} else {
		return nil, 0, fmt.Errorf("unsupported address length %d", len(ip))

The function was easily fixed by removing the early "setBytes" value and rewriting it to correctly set the bytes conditionally for each if-statement, depending on the IP type.

func IPToInteger(ip net.IP) (*big.Int, int, error) {

	val := new(big.Int)

	// check if the ip is v4 => convert to 4 bytes representation
	if ipv4 := ip.To4(); ipv4 != nil {
		val.SetBytes(ipv4)
		return val, 32, nil
	}

	// check if the ip is v6 => convert to 16 bytes representation
	if ipv6 := ip.To16(); ipv6 != nil {
		val.SetBytes(ipv6)
		return val, 128, nil
	}

	return nil, 0, fmt.Errorf("unsupported IP address format")
}

Pull request #258.

Monday, October 09, 2023

Enumerating TLS Certificates with jq and Bash

Doubling back to share some more notes about web application security adjacent stuff. This is a bash script for reconnaissance that uses some tooling from Project Discovery - mapcidr and tlsx - in combination with jq and Bash, to enumerate TLS certificates.

Tuesday, August 15, 2023

Windows

From "user space and system space":

Windows gives each user-mode application a block of virtual addresses. This is known as the user space of that application. The other large block of addresses, known as system space or kernel space, cannot be directly accessed by the application.

More or less everything in the user space talks to NTDLL.DLL to make appropriate calls to hand off work to the Windows kernel, effectively context-switching. While some other software calls are diverted to libraries such as:

  • MSVCRT.DLL: the standard C library
  • MSVCP*.DLL: the standard C++ library
  • CRTDLL.DLL.: library for multithreaded support
  • All code that runs in kernel mode shares a single virtual address space. Therefore, a kernel-mode driver isn't isolated from other drivers and the operating system itself. If a kernel-mode driver accidentally writes to the wrong virtual address, data that belongs to the operating system or another driver could be compromised. If a kernel-mode driver crashes, the entire operating system crashes.

    Windows Architecture Overview


    User Space:
    • System Processes
      • Session manager
      • LSASS
      • Winlogon
      • Session Manager
    • Services
      • Service control manager
      • SvcHost.exe
      • WinMgt.exe
      • SpoolSv.exe
      • Services exe
    • Applications
      • Task Manager
      • Explorer
      • User apps
      • Subsystem DLLs
    • Environment Subsystems
      • Win32
      • POSIX
      • OS/2
    Kernel Space:
    • Kernel Mode
      • Kernel mode drivers
      • Hardware Abstraction Layer (HAL)
    • System Threads
      • System Service Dispatcher
      • Virtual Memory
      • Processes and Threads
    • Security
      • Security Reference Monitor
    • Device & File Systems
      • Device & File System cache
      • Kernel Drivers
      • I/O manager
      • Plug and play manager
      • Local procedure call
      • Graphics drivers
    • Hardware Abstraction Layer (HAL)
      • Hardware interfaces

    Windows Ecosystem

    OK, so of course the real question is, how do we interact with Windows ecosystem to actually do things? Like other software ecosystems, we have some set of libraries which we can use to implement functions which return values. Consider the CreateFileA API. Per Microsoft's documentation, here is the prototype for this interface:

    HANDLE CreateFileA(
      [in]           LPCSTR                lpFileName,
      [in]           DWORD                 dwDesiredAccess,
      [in]           DWORD                 dwShareMode,
      [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
      [in]           DWORD                 dwCreationDisposition,
      [in]           DWORD                 dwFlagsAndAttributes,
      [in, optional] HANDLE                hTemplateFile
    );

    A file name, access, share mode, security attributes (optional), a disposition, flags, and a template (optional). We'll also use printf and scanf to read some inputs. First we'll get a file path, and then a name for our new file. We'll concatenate the two into a full path, and call it with hFile on the CreateFileA API. And we'll define a constant to point to the content we wish to write to our text file.

    We'll use FormatMessageA, as listed in Microsoft's documentation, to obtain possible error messages in case of failure. And check for errors against the WriteFile API with our if(!WriteFile statement - that is, if our write fails, let us know that it failed, close our handle, and return a fail status. Else, if our file has been created, close our handle and let us know by printing a message and the conjoined fullPath of our file, then exit cleanly with 0:

    #include <stdio.h>
    #include <windows.h>
    
    int main() {
        char path[MAX_PATH];
        char filename[MAX_PATH];
        HANDLE hFile;
        DWORD bytesWritten;
    
        // Get user input for path and filename
        printf("Enter the path: ");
        scanf("%s", path);
    
        printf("Enter the filename: ");
        scanf("%s", filename);
    
        char fullPath[MAX_PATH];
        snprintf(fullPath, sizeof(fullPath), "%s\\%s", path, filename);
    
        hFile = CreateFileA(fullPath, GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
    
        if (hFile == INVALID_HANDLE_VALUE) {
            DWORD error = GetLastError();
            LPVOID errorMsg;
            FormatMessageA(
                FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
                NULL,
                error,
                0, // Default language
                (LPSTR)&errorMsg,
                0,
                NULL
            );
            printf("Failed to create the file: %s\n", (char*)errorMsg);
            LocalFree(errorMsg);
            return 1;
        }
    
        const char* content = "Noted";
        if (!WriteFile(hFile, content, strlen(content), &bytesWritten, NULL)) 
    {
            printf("Failed to write to the file.\n");
            CloseHandle(hFile);
            return 1;
        }
    
        CloseHandle(hFile);
    
        printf("File created successfully: %s\n", fullPath);
    
        return 0;
    }
    

    Just a Prologue

    After compiling, we can test and observe this to make some observations about Windows system behavior. The prologue, stack unwinding, and it's use of undocumented calls, which happen abstracted and hidden away from the user.

    C:\Users\User\Downloads>.\createfile.exe
    Enter the path: C:\Users\User
    Enter the filename: ts
    File created successfully: C:\Users\User\text.txt

    For example, when we first run our program, we immediately observe calls to NTDLL, which negotiates a thread and begins the work of running and executing our file. We can see this here:

    0, ntdll.dll!RtlUserThreadStart

    After hitting the first return, we can pull a stack trace to see our thread has now unwound a bit, and we've initiated contact with the kernel at KERNEL32.DLL, which is home to x64 function calls.

    0, ntdll.dll!NtWaitForWorkViaWorkerFactory+0x14
    1, ntdll.dll!RtlClearThreadWorkOnBehalfTicket+0x35e
    2, kernel32.dll!BaseThreadInitThunk+0x1d
    3, ntdll.dll!RtlUserThreadStart+0x28

    During this time, we see multiple calls to LdrpInitializeProcess which initialize the structures in our process. Then we see our BaseThreadInitThunk, a similar kernel mode callback like LdrInitializeThunk, and a call to RtlNtImageHeader to get the image headers for our process.

    Skipping forward a bit, later, when we enter our path and filename, those values are moved into the registers, like so. And following this, many cmp comparisons are made, checking the path to see that it is ok:

    mov rbx,qword ptr ss:[rsp+70] | __pioinfo
    mov rsi,qword ptr ss:[rsp+78] | Users\\User\n\n 

    After a very long dance handling the file path, we finally see assembly calls involving our filename emerge. The filename is effectively loaded into a register like so:

    push rbx                        | rbx:&"ts\n\nsers\\User\n\n"
    sub rsp,20                      |
    mov rbx,rcx                     | rbx:&"ts\n\nsers\\User\n\n"
    lea rcx,qword ptr ds:[<_iob>]   | 
    cmp rbx,rcx                     | 
    jb msvcrt.7FFF040306F5          |
    lea rax,qword ptr ds:[7FFF04088 |
    cmp rbx,rax                     | rbx:&"ts\n\nsers\\User\n\n"
    ja msvcrt.7FFF040306F5          |

    Much later on when our file is created, we see that this file creation likely could have been logged by Event Tracing For Windows.

    call createfile.7FF60B1C6D00    |
    jmp createfile.7FF60B1C860C     |
    sub r10d,2                      |
    mov rcx,qword ptr ds:[r13]      | rcx:"ts", [r13]:"ts"
    lea rbx,qword ptr ds:[r13+8]    | [r13+8]:EtwEventWriteTransfer+260

    And after many assembly instructions later, we finally see our text get the lea, load effective address, containing our message for the text file we're writing. "Noted":

    call rax                        |
    mov eax,1                       |
    jmp createfile.7FF60B1C1743     |
    lea rax,qword ptr ds:[7FF60B1D1 | 00007FF60B1D104F:"Noted"
    mov qword ptr ss:[rbp+300],rax  |
    mov rax,qword ptr ss:[rbp+300]  |

    And a syscall for NtWriteFile:

    mov r10,rcx                     | NtWriteFile
    mov eax,8                       |
    test byte ptr ds:[7FFE0308],1   |
    jne ntdll.7FFF055AEE55          |
    syscall                         |
    ret                             |

    And lastly, our call to closeHandle:

    mov rax,qword ptr ds:[<CloseHandle>] | rax:CloseHandle
    call rax                             | rax:CloseHandle

    Though, much more happens - this is the gist of it.

    Most of the stuff in the Microsoft API is well documented. Some of the code is even partially compatible with Unix systems. But other things in the Microsoft ecosystem however, are not officially documented. Microsoft gives us some public APIs. Some of which are just wrappers that call undocumented features under the hood. In a future post, we'll use an undocumented API to talk to the Windows kernel.

Thursday, July 13, 2023

Compression Schemes as Prediction Schemes

Awhile ago I mentioned inference in a Github post on my other blog in reference to security. But when we talk about inference in a wider informational sense, we often talk about complexity.

Monday, July 03, 2023

Euler's Continued Fraction in Lisp

In 1748, Leonhard Euler published a formula describing an identity that connected and generalized an infinite series and infinite continued fraction.

Sunday, July 02, 2023

Notes on Compilers

Lately I've been revisiting compilers and linguistics. It reminded me of Herbert Gross' lectures on calculus.

Sunday, February 26, 2023

Use xargs

Get out of the habit of using while read as an idiom and instead use xargs to process arguments when you're doing batch compute stuff.

Saturday, August 13, 2022

WHOIS, TLS, and Recon

While doing reconnaissance against web applications, I wanted to speed up the process of finding new attack surfaces that some subdomain tools might miss.

Using Python To Access archive.today, July 2025

It seems like a lot of the previous software wrappers to interact with archive.today (and archive.is, archive.ph, etc) via the command-line ...