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
andunicode
versions of a function- unicode |
w
appended to the name - ansi |
a
appended to the name
- unicode |
- 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
- API call we used so far | userland |
- Install ProcessHacker | to verify memory allocation
- a more robust version of the task manager
- url | https://processhacker.sourceforge.io/
- 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
- leverage python and the
- 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
- Step-1 | write the
-
Debugger Software |
x64dbg
- url | https://x64dbg.com/
- install | extract it -->
release > x64 > x96dbg.exe
-
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
- we are able to write
-
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 -> rundepends.exe
- using it | open the compiled dll
- "dll.c" | needs to be compiled with visual studio
-
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 --------------- ##