Over the course of creating these tutorials, I have been confronted with attempting to make the compiled binaries small. Usually, after entering a three line program in C++, Visual Studio will assume I would like every DLL, API and function ever created by Microsoft to be included in my binary, and I end up having something close to a 6 meg file. (Don’t even get me started on the fact that you can open a new Word document, type one letter, and the file no longer fits on a 32Gig USB key!)
Because you don’t want the binary filled with a bunch of useless crap to detract from the learning process, the binary should ONLY contain the instructions you want used, and nothing else. You would think this would be easy- perhaps a button somewhere that says “De-crapify” or something, but this is Microsoft, so you actually have to do quite a bit of experimenting in Visual Studio to get the binary size even close to what it should actually be.
Over the weekend I did some experimenting, attempting to get the binary as small as possible and trying to figure out what all this crap is that gets inserted into our binary, and this tutorial covers what I learned. A lot of this info was performed by Zer0Flag
, so many thanks (and kudos) go out to him for his hard work. If you would rather have the PDF of this tutorial, you can download it on the tutorials page. Otherwise, read on…
First off, here is the source code. It simply opens a message box with a string in it, then closes the app:
Because this is basically two lines of code, you would guess that it should be like, oh, maybe 50 bytes? You guessed wrong:
Yes, try 30,000 bytes. For two lines of code? Oh, come on! Let’s see what’s going on in Olly:
This is where Olly first breaks, at address 71B00220. After a little digging, though, I found this is not the real EP. Looking in the PE header, the real entry point is at107114A:
This jumps to our initialization code. Interestingly, after we perform this code, the jump at 107114A will be dynamically changed to point to CRTMain later on. But for now, this jumps to the code in the picture above, starting at address 71B00220.
This initialization code looks for command line arguments and loads in DLLs for the application. At the end, we return to our original jump that is now changed to point to WinMainCRTStartup:
CRTStartup is used for loading the C RunTime libraries. The CRT provides the fundamental C++ runtime support, including:
- setup the C++ exception model
- making sure the constructor of global variables get called before entering main function
- parse command line arguments, and call the main function
- initialize the heap
- setup the atexit chain
After the runtime is initialized, CRTStartup calls the __security_init_cookie function:
This function detects some buffer overruns that overwrite a function’s return address, exception handler address, or certain types of parameters. Causing a buffer overrun is a technique used by hackers to exploit code that does not enforce buffer size restrictions.
After this function checks the code for potential buffer overruns, we finally get to our actual code:
Changing the Build
The first thing we should notice is that Visual Studio defaults to debug mode, so we should definitely change to Release:
Now when we check the size, we see already a big difference:
Wow, that debug information was almost 75% of the binary’s size! Loading this in CFF Explorer, we see that we lost the .textbss and .idata sections, and the other sections have been reduced drastically.
Debug version:
Release version:
Removing the C Runtime
Of course, 8,000 bytes is already pretty good, but who wants to stop at “pretty good”?
Next, we have to take a step backward in order to take a couple steps forward. Right-clicking the main project’s name in the Project Explorer and selecting Properties, we have the main properties window. Open the C/C++ tree and select the “Code Generation” item. We want to change the “Runtime Library” to “Multi-threaded (/MT)”. This will make the binary load the C++ runtime files when the executable is loaded. The reason we want to do this is so we can manually delete it later.
Changing this adds a significant amount back in, but will allow us to delete it (and more) later: One thing you will notice in OllyDBG is that our jump table has all but disappeared:
This is because our DLLs have been inserted into our binary, so they will be called directly.
Ignore Default Libraries
Clicking on the “Input” label under the “Linker” tree, we can force Visual Studio to ignore all the default libraries usually automatically loaded.
Changing this to “Yes” and trying to build the program gives us an error though:
To fix this we must change the entry point of our program. The reason for this is that Visual Studio incorporates several function calls before our program actually starts, namely the CRTStartup and security_cookie calls. That means the entry point is set to these functions instead of the true beginning of our app. Since we just told Visual Studio to ignore these functions, if we don’t change the entry point it is still pointing to these functions, that are now being ignored. Clicking on the “Advanced” label under “Linker” we can change this to our actual entry point, WinMain:
*** You may also need to change the “Buffer Security Check” option to “No (/GS-)” under C/C++ in the Code Generation tab to make it build properly. ***
Now when we build it we get no errors and also a file size of 3,000 bytes:
Now we’re talking! Loading this in Olly, we start to see some improvements:
The setup code has also shrunk:
Removing the Manifest
Next we want to ditch the manifest as it’s never used (at least not in our case). Under Linker, click Manifest File and change “Generate Manifest” to “No”:
Doing this only saves about 200 bytes, but hey, that’s something Here we can see exactly what the manifest looks like (in CFF Explorer):
The next thing we may notice is that our binary has four sections:
One that we could potentially lose is the .reloc section…
Removing Randomized Base Addresses
We don’t need a relocations section if we never relocate code, so let’s turn random relocations off:
Doing that and rebuilding automatically removes our .reloc section, shaving off another 1,000 bytes:
This also has the nice quality of loading our binary in at the usual address of 401000:Combining Sections
Next, we don’t necessarily need both sections, especially since the second section only needs a couple dozen bytes but takes up 1,000. Here, we can set the .rdata section to share the .text section by merging them. Still in the Advanced tab, enter this for “Merge Sections”:
Here is our original .rdata section:
and after combining sections, we can see that this data was inserted into the beginning of our .text section in the binary:
and that our entry point has been changed to 4010D0:
Changing Optimizations
Lastly, we can change the optimizations that Visual Studio uses, telling it to optimize for size over speed. Under C/C++, in the Optimization tab, change these four settings:
One last look at our file size and we see we’ve done quite a nice job:
And this is the complete disassembly in Olly (the RETN instruction is a little cut off at the bottom):
From 31,000 bytes to less than 1,000 (620 bytes to be exact). I guess the real question we should be asking is “Why didn’t Microsoft just start here and then add things as we need them?” I’m sure they’re crying all the way to the bank.
R4ndom
September 11th, 2012 on 3:16 am
we can’t use float numbers if we reomve c runtime!!! any solutions?
September 11th, 2012 on 4:37 am
Move to fixed point numbers. Other than that, you’re out of luck.
September 24th, 2012 on 4:34 am
No, you can. If necessary, link to msvcrt.lib or the similar import libraries.
September 11th, 2012 on 7:43 am
This is an exciting topic.
Thank you for doing and sharing that with us.
I am very interested about this too.
I totally agree with all what you said!
September 11th, 2012 on 9:14 am
Funny how the author claims that “we don’t need relocation” however in real world applications and in general idea I think this is a wrong idea to think so.
Most Operating System mitigation tools (DEP, ASLR, etc) use relocation in order to mitigate classical buffer overflow attacks that you’ve read when you were a kid. The idea to lose them is wrong. The same goes to killing the Exception Handler .
The author might think back that “this post is size oriented and thus we take those measures and think that the programmer knew how to write code”, this is an incorrect assumption.
If the author would suggest a different way to generate relocations (for ex, compressed relocation which relocated the program on the fly ) it would have been acceptable, just like many stubs around the 90s have done.
September 11th, 2012 on 10:19 am
Not once did the author say these techniques are for realworld production code.
September 11th, 2012 on 11:27 pm
Few things that are very wrong with this post that ignores history and flames Microsoft unfairly.
1. The PE file format is based on the COFF file format. Microsoft did not create the COFF file format, this was originally a Unix based format. They just used it as a standard.
2. Microsoft uses shared code (dlls) to reduce binary size when shipping applications.
3. Stripping out all security measures so you can prove a binary can be smaller is useless. I can hand build a PE with a optimized opcode set by hand that will be really tiny (exploits and malware do this) but will also require a lot of things we have developed over the course of history to improve performance and security to go away.
Really not sure why you are pressing on size anyways. Size is not what matters over performance and security. When you can get a terabyte of storage for almost nothing anymore and 64 bit systems becoming the standard making an app run quick, effective, and safe is way more important than making it fast to download and taking up less disk/memory. If this is just an interesting experiment that is fine but trying to criticize people in the process without consideration why things are the way they are is ignorant at best.
Things need to and can be improved but jumping on a Microsoft bashing bandwagon doesn’t help anyone. Come out with constructive ideas to make changes if you want to comment on problems.
September 13th, 2012 on 3:13 am
You don’t get it.
Some applications REQUIRE such size contraints.
Demoscene anyone?
September 13th, 2012 on 3:55 am
quote “From 31,000 bytes to less than 1,000 (620 bytes to be exact). I guess the real question we should be asking is “Why didn’t Microsoft just start here and then add things as we need them?” I’m sure they’re crying all the way to the bank.”
You don’t get it, I like experiments like this that can show a way to get to this goal if needed or “REQUIRED”. The issue is criticizing Microsoft for having default settings that improve performance and security over a smaller image.
September 13th, 2012 on 7:30 am
I hope the people who left bad comments regarding this tutorial doesn’t get to you, because we would love to see more of your tutorials. I will try this tutorial in due time, and I understand that we might not use it IRL but people, or at least me, wants to know how things work.
Thanks!
September 13th, 2012 on 8:40 pm
Another good way to decrease size of your binary is using a WinAPI calls only. And as you can see in MSDN, WinAPI supports huge amount of possibilities to replace C++ std. libs. And your binary will have a great portability, because in Windows we have variety of msvcr*.dll’s, so with WinAPI you can avoid this “DLL Hell”
September 13th, 2012 on 8:41 pm
BTW, thnx to author. Your place many known tricks in one article.
June 22nd, 2013 on 4:58 am
We stumbled over here different web page and thought I might check things out. I like what I see so i am just following you. Look forward to exploring your web page for a second time.
July 7th, 2013 on 9:52 am
I’m gone to inform my little brother, that he should also go to see this web site on regular basis to get updated from newest gossip.
July 9th, 2013 on 2:50 pm
Ordinarily I don’t study post on sites, however wish to state that this kind of write-up quite required myself to see and also accomplish that! Your own way of writing continues to be surprised me. Appreciate it, quite good content.