Intro

In this post I’m gonna compare different security mitigations in Linux and different BSD flavours. Specifically it’s gonna be FreeBSD, OpenBSD, NetBSD and DragonflyBSD. If you expected MacOS I’m sorry not sorry but I do not have the budget for it. I could also include Windows there but honestly using Windows feels like a pain even for a small blogpost like this. Usually in my writing im assuming some experience in exploit development so I avoid explaining what ASLR and friends are but in this case I’m gonna make an exception so more people can read it somewhat comfortably. An important thing to note is that this is not supposed to be statement which OS is more secure. This is just an exploration of how different OS’s tackle the most common of mitigation techniques used. Whatever it will look like, if you want a secure operating system you probably should use OpenBSD as it includes a lot of additional security measures, for example my friend silt shared this the other day on our Discord server. Another important thing to note is that everything is done on default settings. I’m sure all the security mitigations can be improved upon by turning on some options but in my defense basic security should be on by default and not be opt-in.

How It Was Done

For compiling on every system I used gcc which as it turned out was in a lot of different versions, some were newer and some were very old. I also used clang just in case to compare but I did not notice any significant differences. The Linux testing was done on my Arch Linux machine but the behaviour shouldn’t differ between distros.

Write XOR Execute

This is not a chapter I expected to make but I was forced to. In every modern operating system there is this rule that a memory page shouldn’t be at the same time executable and writeable. Especially the stack where our local function’s data and different other data like the return addresses from function calls are stored on. When we ignore this rule an older than time itself exploitation technique can be used where we put the code we want to execute as data on the stack and then we return to it by overwriting the return address. Also called shellcoding. Of course it’s not a perfect mitigation, as a countermeasure we have return-oriented programming, but it’s a start. You would expect every operating system with an internet connection to adhere to this rule but no, seems like DragonflyBSD doesn’t care. Image At the beginning I thought gdb is playing tricks on me especially since on DragonflyBSD it didn’t support info proc mappings so I couldn’t check what’s the permissions of the memory segment, but no. I wrote a small code example and I’m perfectly able to overwrite the return address to the stack and execute my code. Image Explanation: at (&a)+2 there’s stored the return address on the stack, so I’m overwriting it with the address of a which is also stored on the stack. So after the main function returns, we’re executing cpu instructions that are encoded in the number 0x909090cccc909090. Specifically 0x90 is the opcode for NOP and 0xcc is the opcode for INT3 which is an instruction used for implementing software breakpoints. We can see that it’s executed correctly because of the Trace/BPT trap.

ASLR

Now we’re gonna explore everything related to ASLR which is just a mitigation that tries to make all the memory addresses random. For testing ASLR I ran a program like this or it’s variation multiple times:

int main() {
  int stack;
  printf("bin: %p heap: %p stack: %p libc: %p math: %p\n",
	 main, malloc(16), &stack, putchar, sin);
  for (int i = 0; i < 10; ++i) {
    void *p = mmap(NULL, 0x1000, 6, MAP_ANON | MAP_PRIVATE, -1, 0);
    printf("%p\n", p);
  }
  return 0;
}

This is not a white paper so I’m gonna spare you all the details.

ASLR on Linux

One thing that is unique to Linux is that (at least) glibc uses the brk syscall for creating the heap, while every BSD flavour seemed to use the mmap syscall. I’m not sure if it’s a well known thing but the brk syscall on Linux creates it’s memory segment after our binary and our ASLR only adds some small offset to it. So the security of heap addresses is mostly dependant on the security off the binary address.

i bin heap heap-bin diff
0 0x55e801709169 0x55e80349c2a0 0x1d93137
1 0x556e260b1169 0x556e267a72a0 0x6f6137
2 0x55a8f6002169 0x55a8f765f2a0 0x165d137
3 0x5643f11c2169 0x5643f2ef52a0 0x1d33137
4 0x563ac7313169 0x563ac7bfd2a0 0x8ea137
5 0x56143239d169 0x5614328fe2a0 0x561137

By looking at the table we can see that the first byte of the binary barely changes and the least significant byte and a half stays the same (this is done so the alignment to memory pages remains the same). By looking at this we can deduce that the randomness only applies to around 4 or 3 and a half bytes giving us 4/3.5 bytes of entropy. Except some very extreme cases this should be more than enough to protect us from bruteforcing the ASLR, especially done through an internet connection. However when we look at the difference between heap addresses and binary addresses only around 1 byte and a half change. In a scenario where the attacker has a leak of our binary it shouldn’t be hard to bruteforce the heap address since there is around 1 in 0xfff (1/4095) chance we will correctly guess the address. In a case where the binary doesnt have position independent code we can even try to guess the address without a leak.

i libc math math-libc 1st mmap libc-1st mmap
0 0x7f0d053ffb20 0x7f0d055da620 0x1dab00 0x7f0d05680000 -0x2804e0
1 0x7ff9b973bb20 0x7ff9b9916620 0x1dab00 0x7ff9b99bc000 -0x2804e0
2 0x7f51cb692b20 0x7f51cb86d620 0x1dab00 0x7f51cb913000 -0x2804e0
3 0x7f2503693b20 0x7f250386e620 0x1dab00 0x7f2503914000 -0x2804e0
4 0x7f16f6c12b20 0x7f16f6ded620 0x1dab00 0x7f16f6e93000 -0x2804e0
5 0x7fdbdc487b20 0x7fdbdc662620 0x1dab00 0x7fdbdc708000 -0x2804e0

In case of dynamically loaded libraries we can see that they have the same as our binary around 3.5 bytes of entropy. However different libraries and mmaped memory have the same offset from each other so if we leak one we can calculate the other ones. In this case our mmaped memory appeared after the libc, resulting in a negative number in our difference, but after we mmap enough memory it will start appearing before the libc. This is in fact used in the House of Muney which is one of the coolest if not the coolest “house of” glibc’s ptmalloc exploitation technique IMO.

i stack
0 0x7ffdeff02f48
1 0x7ffc6fbded58
2 0x7ffeb6ce9c48
3 0x7fffc896b688
4 0x7ffe5e2e1d08
5 0x7ffcbf1e6c78

Stack addresses are independent of any other ones. We can observe that there’s 4 bytes of entropy. Fun fact: the randomness at mask & 0xfff0 is done later by adding a random offset to the stack. The least significant nibble is not touched so the stack alignment to powers of 16 remains the same.

ASLR on DragonflyBSD

There by default ASLR doesn’t affect mmaped memory but we can use the sysctl vm.randomize_mmap=1 command to enable it, so I used it.

i bin
0 0x1021b3a
1 0x1021b3a
2 0x1021b3a
3 0x1021b3a
4 0x1021b3a
5 0x1021b3a

The binary address stays the same even though the binary is compiled as position independent.

i stack
0 0x7fffffdfd7bc
1 0x7fffffdfda0c
2 0x7fffffdfd76c
3 0x7fffffdfd86c
4 0x7fffffdfd84c
5 0x7fffffdfd8bc

The randomness of the stack is pathetic, combined with the fact that it’s executable, it’s a deadly combination.

i libc math math-libc
0 0xfdd675b05 0xfd6bdfdd0 -0x6a95d35
1 0xd4a9a9b05 0xf27e11dd0 0x1dd4682cb
2 0xfcd0b8b05 0xc64001dd0 -0x3690b6d35
3 0xf2f818b05 0xff688fdd0 0xc70772cb
4 0xfdf59cb05 0xfdc2fbdd0 -0x32a0d35
5 0xff261eb05 0x97ac4add0 -0x6779d3d35
i heap 1st mmap 2nd mmap 3rd mmap
0 0xc137d02c0 0xfb2fb5000 0xfe3938000 0xfe3939000
1 0xf832202c0 0xfb54fc000 0xfb54fd000 0xfb54fe000
2 0xe49b102c0 0xfff9d7000 0xfff9d8000 0xfff9d9000
3 0x1000cb02c0 0xf85e1f000 0xff79a3000 0xff79a4000
4 0xfad8202c0 0x8781cf000 0xfc7986000 0xfc7987000
5 0xd5a4402c0 0xf93bc2000 0xfd0f76000 0xfd0f77000

The ASLR of the rest looks pretty good.

ASLR on FreeBSD

I hope by now you already see the pattern in which I compare the values. So I will only show the tables without any additional commentary because I would only repeat myself.

i bin heap stack libc math
0 0x30fc604edaf0 0x50521f209000 0x3104808556ec 0x310483062fe0 0x3104821b0210
1 0xed517cbaf0 0x3faf8fa09000 0xf57237cb0c 0xf5736fafe0 0xf572cb6210
2 0x1eda82c45af0 0x39fc23a09000 0x1ee2a2f2663c 0x1ee2a3f2cfe0 0x1ee2a3846210
3 0x21c3261feaf0 0x23d3a0609000 0x21cb46afdcac 0x21cb48b5bfe0 0x21cb47b04210
4 0x21e5b4d1baf0 0x41c5b2a09000 0x21edd52db15c 0x21edd780bfe0 0x21edd6843210
5 0x1cf40b5c4af0 0x4d3a05e09000 0x1cfc2bd07a0c 0x1cfc2d6e9fe0 0x1cfc2c776210
i heap-bin stack-bin libc-stack libc-math
0 0x1f55bed1b510 0x820367bfc 0x280d8f4 0xeb2dd0
1 0x3ec23e23d510 0x820bb101c 0x137e4d4 0xa44dd0
2 0x1b21a0dc3510 0x8202e0b4c 0x10069a4 0x6e6dd0
3 0x2107a40a510 0x8208ff1bc 0x205e334 0x1057dd0
4 0x1fdffdced510 0x8205bf66c 0x2530e84 0xfc8dd0
5 0x3045fa844510 0x820742f1c 0x19e25d4 0xf73dd0
i heap 1st mmap 2nd mmap
0 0x4452c5a09000 0x4452c6000000 0x4452c6001000
1 0x5866d6809000 0x5866d6e00000 0x5866d6e01000

ASLR on OpenBSD

i bin heap libc math
0 0xeaa73ce7b00 0xeacecb7c2b0 0xead3c4151e0 0xead270dbdd0
1 0x22ecd4b00 0x504b68df0 0x4fc7a31e0 0x4fb2d3dd0
2 0xa0391ed1b00 0xa062f785630 0xa061b34c1e0 0xa067afcddd0
3 0x4b791e7b00 0x4e5fa60900 0x4e31c471e0 0x4d82904dd0
4 0x60e30f7db00 0x610a86e59d0 0x610b047d1e0 0x6107df94dd0
5 0xa958e007b00 0xa979f0821a0 0xa981f7791e0 0xa983111ddd0
i heap-bin libc-heap math-libc
0 0x278e947b0 0x4f898f30 -0x15339410
1 0x2d5e942f0 -0x83c5c10 -0x14cf410
2 0x29d8b3b30 -0x14439450 0x5fc81bf0
3 0x2e6878e00 -0x2de19720 -0xaf342410
4 0x277767ed0 0x7d97810 -0x324e8410
5 0x21107a6a0 0x806f7040 0x119a4bf0
i heap 1st mmap 2nd mmap 1st mmap-heap 2nd mmap-1st mmap
0 0x7bde1402210 0x7bdd019c000 0x7bdf10d4000 -0x11266210 0x20f38000
1 0xb3c8e37d680 0xb3c34247000 0xb3c7c6cd000 -0x5a136680 0x48486000
i stack
0 0x7f3d689ef9b4
1 0x74a568893c54
2 0x7c986d0dedc4
3 0x7ab552b59574
4 0x7dd201eb02a4
5 0x78bad47c0f74

ASLR on NetBSD

i bin heap stack libc math
0 0xb7c00b60 0x753a144fe020 0x7f7fff950cfc 0x753a13d4426e 0x753a1421c158
1 0xc6c00b60 0x6fa617e70020 0x7f7ffffe04fc 0x6fa61774426e 0x6fa617c1c158
2 0xcb200b60 0x76d22e0dc020 0x7f7fffff84dc 0x76d22d94426e 0x76d22de1c158
3 0xe0200b60 0x7eb1b3051020 0x7f7fff5b9b8c 0x7eb1b294426e 0x7eb1b2e1c158
i math-libc libc-heap
0 0x4d7eea -0x7b9db2
1 0x4d7eea -0x72bdb2
2 0x4d7eea -0x797db2
3 0x4d7eea -0x70cdb2
i heap 1st mmap 2nd mmap
0 0x753a144fe020 0x753a144ec000 0x753a144eb000
1 0x6fa617e70020 0x6fa617e5e000 0x6fa617e5d000

Stack Canaries

Now let’s talk about stack canaries. I really like the way Linux does them. The first byte of a stack canary is always equal to zero. On a 64-bit system 7 bytes is still more than enough entropy and what we get from the null byte is additional security. Because of it there are scenerios where we for example overwrite N bytes with the letter A, after the letters there’s the canary, and when the letters A are printed we won’t leak the canary because of it - cuz there’s a zero byte separating them. For some reasons only Linux does this. Every other canary didn’t included the null byte. Other thing I like about the way Linux does canaries is that they are located in a special register fs that stores the address of a random place in the memory. Even when he have a write-what-where condition, unless we already have a leak, we won’t be able to overwrite the original canary. Image Image Now, this is not true for all Linux systems. Seems like the GNU toolchain only does this on the x86 and x64 architectures. On the ARM architecture the canary is actually stored inside our binary. To be fair ARM doesn’t have the fs register, but there’s nothing stopping it from using some other one. The same applies for every non-Linux system (including x86 and x64). For example this is how we get the canary inside of x64 OpenBSD: Image Image