the PIEtrix: follow the offset

i recently participated in a small CTF. the topic was binary exploitation and the actual program ran on a server. when it was started, it displayed the starting address in memory, and you could make an input specifying what the next address should be. if the correct address was entered, the flag was shown. if an incorrect memory address was entered, the program terminated. you could download and inspect both the source code file and a precompiled binary.

to be honest…i had absolutely no idea what was going on. the first thing I did was throw the compiled file into ghidra to get at least some kind of overview of what was happening in this program. i started by looking for a main function since that’s usually the entry point of a program. so I spent several hours looking at this code and trying to understand it. first, there was a print command that output the starting address in memory. it looked something like this: address of main: 0x783783cd983

so, i already had a starting point, but I have to admit I had no clue at this point how that could help me. the next line then asked for the next memory address. if the correct address was entered, a win function was triggered, which displayed the flag. at some point, i noticed that the starting address of main changed every time the program was restarted.

i had never seen that before. what is going on? i should mention that this CTF was labeled as easy. for me, this was already some crazy brainfuck. i mean…i had dealt with memory addresses, virtual memory, and things like that a while ago and already had trouble understanding it all. so… what does „trouble understanding“ mean… it’s just really complex, and if you don’t deal with it every day, it all eventually fades away… :/

okay, enough whining. what did I know at this point? the starting memory address changes every time the program starts, and there’s a win function. it seemed logical that if you enter the memory address of the win function, you’ll get the flag… so far, so good…

but now I had to figure out what this random starting point was all about. and with the help of my good buddy ChatGPT, I quickly came across the keyword PIE.

so what is PIE?

i can already tell you it’s not about dessert. PIE stands for position independent executable. this means that the file is loaded at a different memory address every time it is executed. these types of executables are based on relative rather than absolute addresses, meaning that while the memory locations are fairly random, the offsets between different parts of the binary remain constant.

for example, if it’s known that the function main() is located 0x98 bytes after the base address of the binary in memory, and somehow the position of main() can be determined, you can simply subtract 0x98 from it to get the base address—and from that, calculate the addresses of all other functions. it’s kind of like parking your car in a parking lot as seen from the entrance of a shopping mall. the distance from the car to the entrance is always different, but the distance inside the car, say from the driver’s seat to the passenger seat, is always the same… hmm… was that a good analogy? you get what i mean.

what does this mean for our ctf?

now we need to figure out how far the win function is from the main function. once we know that, we can calculate the starting address of the win function and bam… we’ve got the flag. so I kept analyzing the file and found out that the main function starts at 0x10133d and the win function at 0x1012a7. after some rocket-science-level calculations, it became clear that there’s a difference of 0x96 bytes. And then it hit with crashing force… the flag.

now what?

looking back, the CTF is actually easy. at first, everything was new to me, but honestly… that’s the case with every CTF for me. but once you figure out what’s going on, things start to fall into place. sure, it wasn’t super complex, and you didn’t have to write a fancy exploit or anything like that, but it was still very interesting. personally, I didn’t know that this kind of executable existed and that it’s now standard practice. in the Linux kernel world, this technique (together with ASLR) has been used since 2005. on Windows, there’s a similar mechanism.

at this point, I want to thank the creators of the CTF. i learned something new again, and one more blank spot in the world of IT security has been filled… 🙂 …luckily, there are still about 1000000000000000000000 other blank spots out there waiting to be explored…

sources