Skip to main content

0404 | The Windows API

Introduction

  • API | Application Programming Interface
  • Mechanism to interface with the Windows operating system
  • "Mostly" described by the Microsoft Developer Network (MSDN)
    • Some functions are not officially documented
  • Comprised of functions, structures and constants
    • Including Windows defined data types
  • NOT static | Dynamic entity which can change and expand between releases
  • Official implementation is on your Windows machine (in DDLs)
    • ""kernel32.ddl""
    • "ntdll.dll"
  • Ctypes enables us to wrap Python around C
    • "Speaking C" lets us interface with the Windows API
  • Hacker focused API calls
    • OpenProcess, CreateRemoteThread, WriteProcessMemory

C Data Types and Structures

  • C Primer

    • C is a lower level programming language
    • C is compiled (and faster), while Python is interpreted (and slower)
    • C requires you to specify the types of data
    • C has fewer built in standard functions
    • C is comparatively more difficult than Python
  • Pointers and Structs

    • When you create a variable, that variable has a memory address
    • The variable has a value, but it also has a memory address
    • Pointers are just variables that store addresses, not values
    • Structures (structs) are collections of grouped variables
    • A structure groups variables under a single type
  • CTypes module | included in the standard library

    • a foreign function library for python
    • provides c-compatible data types | allows us to directly call functions in DLLs or shared libraries directly from python
  • Passing by Reference | when a function is expecting a pointer to a specific data type as an input parameter

    • if the function wants to write to that location
    • if the data being passed it too large to be passed by it's value
  • Structures | like classes

    • structures are derived from the structure base class defined within the ctypes module
    • each subclass must define a fields attribute | a list of tuples containing the fields name and fields type
      • _fields_ = [("name", c_char_p),("age", c_int)]
  • Lists | like arrays

    • setting up the space beforehand is needed
    • multiply the data type with the number of elements you want in the array
    • person_array_t = PERSON * 3
  • demo-example | "c_types_structs.py" | (play around with commenting/uncommenting as you see fit)

    from ctypes import *

    print("-"*50)
    ## --------------- C DATA TYPES -- BOOLEAN --------------- ##

    # boolean
    b0 = c_bool(0)
    b1 = c_bool(1)

    print(b0)
    print(type(b0))
    print(b0.value)

    print(b1)
    print(type(b1))
    print(b1.value)

    print("-"*50)
    ## --------------- C DATA TYPES -- INTEGER --------------- ##

    # unsigned integer
    i0 = c_uint(-1)
    print(i0.value)

    print("-"*50)
    ## --------------- C DATA TYPES -- STRINGS --------------- #

    # strings -- null terminated char pointers
    c0 = c_char_p(b"test")
    print(c0.value)

    # when changing the values of pointer type instances, we are actually changing
    # the memory location the variable is pointing to and NOT the actual contents
    # of that memory block
    print(c0)
    c0 = c_char_p(b"test2")
    print(c0)
    print(c0.value)

    print("-"*50)
    ## --------------- C DATA TYPES -- STRING BUFFER --------------- #

    # reserve a buffer -- changing the value will not change the memory address
    # string buffer -- 5 byte buffer -- initialized with null bytes -- \x00
    p0 = create_string_buffer(5)
    print(p0)

    # check the raw memory contents
    print(p0.raw)
    print(p0.value)

    # changing the value at the address
    p0.value = b"a"
    print(p0.raw)
    print(p0.value)
    print(p0) # the same memory address

    print("-"*50)
    ## --------------- C DATA TYPES -- UNICODE BUFFER --------------- #

    # unicode buffer is expected --> use unicode buffer
    u0 = create_unicode_buffer(5)
    print(u0)
    print(u0.value)
    u0.value = "a"
    print(u0)
    print(u0.value)

    print("-"*50)
    ## --------------- C DATA TYPES -- POINTERS -- INTEGER --------------- #
    # creating pointers -- pointer function on a c data type
    i = c_int(42)
    pi = pointer(i)

    print(i)
    print(pi)
    print(pi.contents)

    print("-"*50)
    ## --------------- C DATA TYPES -- POINTERS -- BUFFER --------------- #

    # string buffer
    p0 = create_string_buffer(5)
    print(p0)
    p0.value = b"a"
    # check the raw memory contents
    print(p0.value)

    # print it's address in memory
    print(hex(addressof(p0)))

    print("-"*50)
    ## --------------- C DATA TYPES -- REFERENCE --------------- ##

    # create a reference to p0
    pt = byref(p0)
    print(pt)

    # cast returns a new instance of type char pointer which points to pt
    # which can be used to look at the actual data stored at that memory address
    print(cast(pt, c_char_p).value)
    # casting as an integer
    print(cast(pt, POINTER(c_int)).contents)
    print(ord('a'))

    print("-"*50)
    ## --------------- C DATA TYPES -- STRUCTURES --------------- ##
    class PERSON(Structure):
    _fields_ = [("name", c_char_p),
    ("age", c_int)]

    bob = PERSON(b"bob", 30)
    print(bob.name)
    print(bob.age)

    # reusing the structure
    alice = PERSON(b"alice", 20)
    print(alice.name)
    print(alice.age)

    print("-"*50)
    ## --------------- C DATA TYPES -- LISTS/ARRAYS --------------- ##

    # setting up space for a list/array
    person_array_t = PERSON * 3
    print(person_array_t)

    # create the person array
    person_array = person_array_t()
    print(person_array)

    # adding people to the array
    person_array[0] = PERSON(b"bob", 30)
    person_array[1] = PERSON(b"alice", 20)
    person_array[2] = PERSON(b"mallory", 50)

    for person in person_array:
    print(person)
    print(person.name)
    print(person.age)

Interfacing with the Windows API

  • the win32 API often has both ansi and unicode versions of a function
    • unicode | w appended to the name
    • ansi | a appended to the name
  • Documentation | always "msdn"
  • demo-example | "windows_api_hello_world.py" | (play around with commenting/uncommenting as you see fit)
    from ctypes import *
    from ctypes.wintypes import HWND, LPCSTR, UINT, INT, LPSTR, LPDWORD, DWORD, HANDLE, BOOL

    print("-"*80)
    ## --------------- MESSAGE BOX -- ANSI --------------- ##
    # url:https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxa

    # MessageBox is defined in User32.dll with User32.lib as library
    MessageBoxA = windll.user32.MessageBoxA
    # define the argument types
    MessageBoxA.argtypes = (HWND, LPCSTR, LPCSTR, UINT)
    # define the return type
    MessageBoxA.restype = INT

    # function pointer object
    print(MessageBoxA)


    # specify the values for the parameters
    lpText = LPCSTR(b"World")
    lpCaption = LPCSTR(b"Hello")
    # type -- MB_OK - on push button (default) -- 0x00000000L -- minus the "L"
    MB_OK = 0x00000000
    # type -- MB_OKCANCEL - two push buttons -- 0x00000001L -- minus the "L"
    MB_OKCANCEL = 0x00000001

    # hWnd -- handle -- owner is null --> the box has no owner window
    # MessageBoxA(None, lpText, lpCaption, MB_OK)
    # MessageBoxA(None, lpText, lpCaption, MB_OKCANCEL)


    print("-"*80)
    ## --------------- GETUSERNAME -- ANSI --------------- ##
    # url:https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getusernamea

    GetUserNameA = windll.advapi32.GetUserNameA
    GetUserNameA.argtypes = (LPSTR, LPDWORD)
    GetUserNameA.restype = INT

    # buffer size -- dword = 32 bit unsigned int
    # if buffer is not big enough --> nothing is displayed
    # buffer_size = DWORD(8)
    buffer_size = DWORD(2) # error -- won't fit
    # create buffer
    buffer = create_string_buffer(buffer_size.value)

    # grab the username --
    # byref: lpdword is a pointer to a dword
    GetUserNameA(buffer, byref(buffer_size))
    # display the returned username
    print(buffer.value)


    print("-"*80)
    ## --------------- GETLASTERROR --------------- ##

    # identifying the error codes
    # 'msdn error codes' -- 122 --> The data area passed to a system call is too small
    error = GetLastError()

    if error:
    print(error)
    # the windows error message for this error code
    print(WinError(error))


    print("-"*80)
    ## --------------- WINDOWS SPECIFIC STRUCTURES --------------- ##
    # url:https://learn.microsoft.com/en-us/windows/win32/api/windef/ns-windef-rect

    # expects a pointer to a rect structure -- LPRECT lpRect

    # define the structure RECT
    class RECT(Structure):
    _fields_ = [("left", c_long),
    ("top", c_long),
    ("right", c_long),
    ("bottom", c_long)]

    rect = RECT()

    print(rect.left)
    print(rect.top)
    print(rect.right)
    print(rect.bottom)

    # changing values
    rect.left = 1
    print(rect.left)


    print("-"*80)
    ## --------------- GETWINDOWRECT --------------- ##
    # url:https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowrect

    GetWindowRect = windll.user32.GetWindowRect
    # specify argtypes -- handle and pointer
    GetWindowRect.argtypes = (HANDLE, POINTER(RECT))
    # return type
    GetWindowRect.restype = BOOL

    # get the handle -- use the GetForegroundWindow function
    # retrieves the handle to the foreground window
    hwnd = windll.user32.GetForegroundWindow()

    # call GetWindowRect
    GetWindowRect(hwnd, byref(rect))

    # changing the cmd window will change the results
    print(rect.left)
    print(rect.top)
    print(rect.right)
    print(rect.bottom)

Undocumented API Calls

  • Not all Windows APIs are documented on MSDN
  • The documented APIs we used so far operate in a user mode
  • When calling a user mode API, we eventually end up in kernel mode
  • Windows APIs are an abstraction layer over the native API
  • These native API calls we are interested in, are defined in NTDLL
  • Note | The native API can be used to evade endpoint detection and response tooling
    • A number of anti-virus and endpoint detection and response solutions make use of API hooking to gain telemetry for their analysis when trying to identify whether an application's behaviour is potentially malicious
    • Getting Around This --> Use a lower level API call (like ntdll) which may not be hooked by the security product
      • API call we used so far | userland | kernel32
      • behind the scenes they are translated into lower level API call in ntdll
      • APIs in ntdll are mostly undocumented and can change at any time with a new windows release
  • Install ProcessHacker | to verify memory allocation
  • demo-example | "undocumented_api.py" | (play around with commenting/uncommenting as you see fit)
    from ctypes import *
    from ctypes import wintypes

    kernel32 = windll.kernel32
    # cosmetic -- for using ctypes and windows types interchangeably
    SIZE_T = c_size_t

    print("-"*80)
    ## --------------- VIRTUAL ALLOC --------------- ##
    # url:https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc
    # virtualalloc -- tl;dr -> to allocate memory
    # reserves, commits, or changes the state of a region of pages in the virtual
    # address space of the calling process. Memory allocated by this function is
    # automatically initialized to zero

    VirtualAlloc = kernel32.VirtualAlloc
    # parameters
    VirtualAlloc.argtypes = (wintypes.LPVOID, SIZE_T, wintypes.DWORD, wintypes.DWORD)
    # return type
    VirtualAlloc.restype = wintypes.LPVOID

    ## --------------- VIRTUAL ALLOC -- PARAMETERS --------------- ##

    # lpAddress -- the starting address of the region to allocate
    # if NULL -> the system determines where to allocate the region

    # dwSize -- the size of the region in bytes

    # flAllocationType -- the type of memory allocation
    #-- MEM_COMMIT -- allocate memory charges
    MEM_COMMIT = 0x00001000

    #-- MEM_RESERVE -- reserve a range of the process's virtual address space
    # without allocating any actual physical storage in memory or in
    # the paging file on disk
    MEM_RESERVE = 0x00002000

    # flProtect -- the memory protection for the region of pages to be allocated
    # constants-url:https://learn.microsoft.com/en-us/windows/win32/Memory/memory-protection-constants
    #-- PAGE_EXECUTE_READWRITE -- enables execute, read-only, or read/write access to
    # the comitted region of pages
    PAGE_EXECUTE_READWRITE = 0x40

    ## --------------- USERLAND -- VIRTUAL ALLOC -- CALLING-IT --------------- ##

    # ptr -- base address of the allocated region of pages
    ptr = VirtualAlloc(None, 1024 * 4, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)

    # check for errors
    error = GetLastError()

    if error:
    print(error)
    print(WinError(error))

    # check where we allocated the memory
    print("VirtualAlloc: ", hex(ptr))

    # to check it -- use process hacker
    # -> filter for 'python' -> open it -> memory -> look for the memory addr
    # Type:Private; Size:4kB; Protection:RWX

    # IDEA
    # translate the same virtual alloc call, down a layer deeper,
    # and directly call the ntdll version of virtual alloc --> ntallocatevirtualmemory
    # documented on msdn

    print("-"*80)
    ## --------------- KERNEL -- NT ALLOCATE VIRTUALMEMORY --------------- ##
    # url:https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntallocatevirtualmemory
    nt = windll.ntdll
    NTSTATUS = wintypes.DWORD


    # function definition
    NtAllocateVirtualMemory = nt.NtAllocateVirtualMemory
    NtAllocateVirtualMemory.argtypes = (wintypes.HANDLE, POINTER(wintypes.LPVOID), wintypes.ULONG, POINTER(wintypes.ULONG), wintypes.ULONG, wintypes.ULONG)
    NtAllocateVirtualMemory.restype = NTSTATUS

    # getting a handle for the current process -> GetCurrentProcess()
    handle = 0xffffffffffffffff

    base_address = wintypes.LPVOID(0x0)
    zero_bits = wintypes.ULONG(0)
    size = wintypes.ULONG(1024 * 12)

    # allocation type
    # protection -- the same as before

    # calling the function
    ptr2 = NtAllocateVirtualMemory(handle, byref(base_address), zero_bits, byref(size), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)

    # check if succeeded or not
    if ptr2 != 0:
    print("error!")
    print(ptr2)

    print("NtAllocateVirtualMemory: ", hex(base_address.value))

    # specify input so that the python process does not die
    input()

Direct Syscalls

  • Assembly Primer

    • Assembly is a (very) low level programming language
    • Work directly with registers and interrupts
    • Each line (usually) translates to one processor instruction
    • Different processors use different assembly languages
  • Syscalls

    • Every native API call has a specific number that represents it (syscall = system call)
    • Syscall numbers differ between different versions of Windows
    • Numbers can be verified with a debugger, or just use public lists
    • To make a syscall, need to move the number to a register
    • In x64, the syscall instruction will then enter kernel mode
  • Notes

    • With direct syscalls in assembly we can completely remove any windows dll imports
    • instead of ntdll doing the work for us, we can just set up the call directly ourselves
      • when making a syscall, we need to set up some arguments on the stack
      • move the correct syscall for the operation we want to perform into the eax register
      • then use the syscall cpu instruction | will cause the syscall to be actually executed in kernel mode
    • if you want to use the same functionality as the win32 API and want to avoid directly interfacing with it entirely from user mode
      • leverage python and the ctypes library to directly perform system calls
    • use winver in [win+r] to verify the current windows version
    • syscall table by j00ru | google's project zero
  • Executing the system call | Steps

    • Step-1 | write the asmn as shellcode in order to execute it from within pyhton
    • Step-2 | put the shellcode somewhere in memory
    • Step-3 | update the memory for that location to enable execution (otherwise error)
      • trying to execute code from a non-executable memory location
    • Step-4 | call it
  • Debugger Software | x64dbg

  • Summary

    • we are able to write asm into memory
    • then change the memory protection of that location
    • define the location as a function
    • and then calling that function to directly trigger a syscall
    • basically | you can use python to make a win32 api call without directly using the win32 api at all
  • demo-example | "direct_syscalls.py" | (play around with commenting/uncommenting as you see fit)

    from ctypes import *
    from ctypes import wintypes

    print("-"*50)
    ## --------------- DEFINING CONSTANTS --------------- ##

    # cosmetic -- for using ctypes and windows types interchangeably
    SIZE_T = c_size_t
    NTSTATUS = wintypes.DWORD

    MEM_COMMIT = 0x00001000
    MEM_RESERVE = 0x00002000
    PAGE_EXECUTE_READWRITE = 0x40

    # NOTE
    # - executing the system call -- steps
    # - 1 -- write the asmn as shellcode in order to execute it from within pyhton
    # - 2 -- put the shellcode somewhere in memory
    # - 3 -- update the memory for that location to enable execution, otherwise error -- trying to execute code from a non-executable memory location
    # - 4 -- call it

    ## --------------- SYSCALL IN ASM --------------- ##

    # system call NtAllocateVirtualMemory -- syscall 0x0018 -- 24 in dec
    # -- this is how windows will always execute when we are using the win32 api

    """
    mov r10, rcx
    mov eax, 18h
    syscall
    ret
    """


    # check if a call succeeded or not
    def verify(x):
    if not x:
    raise WinError()

    ## --------------- EXAMPLE-FUNCTION -- STEP 1/2 -- SHELLCODE --------------- ##

    # NOTE:
    # start x64dbg
    # -- start a sacrificial process -- notepad -- [win+r] "notepad"
    # -- in x64dbg -- File>Attach>Select Notepad
    # -- in x64dbg -- right click>binary>fill with nops -- select 80
    # -- now we have the space where we can convert our assembly to shellcode

    # NOTE:
    # typically eax will contain the return value of a function
    # -- but it depends on the calling convention

    # NOTE:
    # testing it -- move the value "5" into eax and then return
    # -- if return value is "5" -- we know it's working
    # -- in x64dbg -- (6th entry) right click>Assemble> "mov eax, 5; ret"
    # -- will convert it into shellcode -- "B8 05000000"
    # -- add "ret" to the next line -- "C3"

    # corresponds to asm "mov eax, 5; ret"
    # buf = create_string_buffer(b"\xb8\x05\x00\x00\x00\xc3")
    ## change return value from 5 to 3
    buf = create_string_buffer(b"\xb8\x03\x00\x00\x00\xc3")
    # get the address where it is stored in memory
    buf_addr = addressof(buf)
    print(hex(buf_addr))

    # NOTE:
    # let's take a look with process hacker -- check out the memory location
    # -- process hacker>search for python> check the memory location
    # -- ISSUE-1: the shellcode is not page aligned
    # -- ISSUE-2: the memory protection of the page is "RW"

    ## --- EXAMPLE-FUNCTION -- STEP 3 -- ADJUST MEMORY PAGE PERMISSIONS -- ##
    # use the VirtualProtect function
    # url:https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect

    # define the VirtualProtect funtion
    VirtualProtect = windll.kernel32.VirtualProtect
    # define the argument types
    VirtualProtect.argtypes = (wintypes.LPVOID, SIZE_T, wintypes.DWORD, wintypes.LPDWORD)
    # define the return type
    VirtualProtect.restype = wintypes.INT

    # function parameters
    # -- lpAddress -- starting page of the region of pages -- our buffer address
    # -- dwSize -- size of the region -- in bytes
    # -- flNewProtect -- new memory protection option
    # ---- https://learn.microsoft.com/en-us/windows/win32/Memory/memory-protection-constants
    # -- lpflOldProtect -- reference to store the old memory protection
    old_protection = wintypes.DWORD(0)

    ## NOTE: if memory permission is not changed -- OSError: access violating writing <addr>
    # calling the function -- change the memory protection
    protect = VirtualProtect(buf_addr, len(buf), PAGE_EXECUTE_READWRITE, byref(old_protection))

    # check for errors
    verify(protect)

    # verify it with process hacker that the memory page has the correct permissions
    # -- this is where our shellcode is currently stored -- now we can execute it

    ## --------------- EXAMPLE-FUNCTION -- STEP 4 -- CALLING IT --------------- ##
    # use ctypes.cfunctype
    # url:https://docs.python.org/3/library/ctypes.html#ctypes.CFUNCTYPE

    # expecting number as the return type -- no input parameters
    # only expecting 5 as a response
    asm_type = CFUNCTYPE(c_int)
    # based on the address of our shellcode
    asm_function = asm_type(buf_addr)

    # calling the function
    r = asm_function()
    # checking the output
    print(hex(r))

    print("-"*50)
    ## --------------- SYSCALL -- STEP 1/2 -- SHELLCODE --------------- ##
    # convert asm function to shellcode -- like before
    # "mov r10, rcx" --> 49:89CA -- (in demo:"4c:8bd1")
    # "mov eax, 18h" --> B8 18000000
    # "syscall" --> 0F05
    # "ret" --> C3

    # create the buffer to store the shell code
    # -- demo version
    #buf2 = create_string_buffer(b"\x4c\x8b\xd1\xb8\x18\x00\x00\x00\x0f\x05\xc3")
    buf2 = create_string_buffer(b"\x49\x89\xca\xb8\x18\x00\x00\x00\x0f\x05\xc3")

    # get the address of where it is stored in memory
    buf_addr2 = addressof(buf2)
    print("Buffer address:", hex(buf_addr2))

    ## -- SYSCALL -- STEP 3 -- ADJUST MEMORY PAGE PERMISSIONS -- ##

    old_protection = wintypes.DWORD(0)
    protect = VirtualProtect(buf_addr2, len(buf2), PAGE_EXECUTE_READWRITE, byref(old_protection))
    verify(protect)

    ## --------------- SYSCALL -- STEP 4 -- CALLING IT --------------- ##
    # setting up the function for the syscall as we did with the test asm

    # NOTE: the syscall we are emulating is ntallocatevirtualmemory
    # url:https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntallocatevirtualmemory

    # return NTstatus and then the arguments
    syscall_type = CFUNCTYPE(NTSTATUS, wintypes.HANDLE, POINTER(wintypes.LPVOID), wintypes.ULONG, POINTER(wintypes.ULONG), wintypes.ULONG, wintypes.ULONG)
    # set up the function
    syscall_function = syscall_type(buf_addr2)

    # making the call -- just like in the undocumented api demo

    # getting a handle for the current process -> GetCurrentProcess()
    handle = 0xffffffffffffffff
    base_address = wintypes.LPVOID(0x0)
    zero_bits = wintypes.ULONG(0)
    # NOTE: change the size from 12kB to 24 for debugging
    # size = wintypes.ULONG(1024 * 12)
    size = wintypes.ULONG(1024 * 24)

    # calling the function
    # -- instead of NtAllocateVirtualMemory(...) use sycall_function(...)
    # -- with the same parameters
    ptr2 = syscall_function(handle, byref(base_address), zero_bits, byref(size), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)

    # verify if it works
    if ptr2 != 0:
    print("error!")
    print(prt2)

    # no errors
    print("Syscall allocation: ", hex(base_address.value))


    # halt execution
    input()

    print("-"*50)
    ## --------------- END --------------- ##

Execution from a DLL

  • Windows DLLs | General

    • Dynamic Link Libraries (DLLs) are similar to executable files
    • DLL files contain code and data that can be used by multiple programs
    • DLL files can't be directly executed, but they can be loaded and used
      • when loaded | a DLL can specify an entry point, which defines what happens, when the DLL is loaded
      • alternatively | it can just export functions, which can then be called, by the caller
    • DLL files can be loaded on program startup, or during execution
    • DLL files are not linked or loaded until run time
  • custom DLL

    • "dll.c" | needs to be compiled with visual studio
      #include "pch.h"
      #include <stdio.h>

      extern "C"
      {
      __declspec(dllexport) void hello()
      {
      puts("hello from the dll");
      }

      __declspec(dllexport) int length(char* input)
      {
      return strlen(input);
      }

      __declspec(dllexport) int add(int a, int b)
      {
      return a + b;
      }

      __declspec(dllexport) void add_p(int* a, int* b; int* result)
      {
      *result = *a + *b;
      }

      };
    • taking a look at the exported functions with Dependency Walker
      • url | https://dependencywalker.com/
      • install
        • download x64 version
        • run depends.exe -> extract all -> run depends.exe
      • using it | open the compiled dll
  • demo-example | "dll_execution" | (play around with commenting/uncommenting as you see fit)

    from ctypes import *

    print("-"*80)
    ## --------------- Windows Standard C Library -- MSVCRT --------------- ##

    # interfacing with the windows standard c library
    # -- printing out the time -- pass in None as a null pointer
    print(windll.msvcrt.time(None))

    # standard c functions -- memset and puts
    windll.msvcrt.puts(b"print this!")

    mut_str = create_string_buffer(10)
    print(mut_str.raw)

    # changing the values
    mut_str.value = b"AAAAA"
    print(mut_str.raw)

    # perform similar function with memset
    windll.msvcrt.memset(mut_str, c_char(b"X"), 5)
    windll.msvcrt.puts(mut_str)
    # raw value
    print(mut_str.raw)


    print("-"*80)
    ## -- Interacting with custom DLL -- Calling the functions directly -- ##
    # compiled dll

    # loading the dll -- need to include the full path
    lib = WinDLL("C:\\Users\\user\\Documents\\pyhton201\\Dll.dll")

    # calling the external hello function
    lib.hello()


    print("-"*80)
    ## -- Interacting with custom DLL -- Defining hello() -- ##
    # -- not required but useful to define the functions -- prototypes

    # define the length function from the dll
    lib.length.argtypes = (c_char_p, )
    lib.length.restype = c_int

    # test with str 1
    str1 = c_char_p(b"test")
    print(lib.length(str1))

    # test with str 2
    str2 = c_char_p(b"test1234")
    print(lib.length(str2))

    # Data type differences when interacting with python and c
    str3 = b"abc\x00123"
    # =7 -- \x00 is 1 byte -- via python
    print(len(str3))
    # =3 -- strlen counts the bytes up to a null terminator -- via c
    print(lib.length(c_char_p(str3)))


    print("-"*80)
    ## -- Interacting with custom DLL -- Defining add() -- ##

    lib.add.argtypes = (c_int, c_int)
    lib.add.restype = c_int

    # calling it
    print("2 + 2 = ", lib.add(2, 2))

    # benefit of defining the function -- recognizes type errors
    # print("2 + 2 = ", lib.add(2, 2.0)) # float instead of int

    print("-"*80)
    ## -- Interacting with custom DLL -- Defining add_p() -- ##

    lib.add_p.argtypes = (POINTER(c_int), POINTER(c_int), POINTER(c_int))
    # no return type -- result pointer is passed as an input

    # prepare the variables
    x = c_int(2)
    y = c_int(4)
    result = c_int(0)

    # check the values
    print(result)
    print(result.value)

    # calling the function
    lib.add_p(byref(x), byref(y), byref(result))

    # check the output
    print("2 + 4 = ", result.value)

    print("-"*80)
    ## --------------- END --------------- ##