Hello fellow pwners!

Let us continue our journey in Reverse Engineering and Binary Exploitation. So far, we have seen how an executable is generated, it’s internal structure and x86 assembly. In this post, let us see how a program looks like when it is being executed.

This is the 4th post in this series. So, create a directory post_4 in the rev_eng_series directory and store all the work done in this directory.

1. What all does a program have?

Consider this program code1.c .

#include<stdio.h>
#include<stdlib.h>

int gv1;
int gv2 = 123;

void func1();

int main() {

    int lv = 222, *dyn;

    dyn = (int *)malloc(sizeof(int));

    printf("Address of lv, a local variable = %p\n", &lv);
    printf("Value of dyn(dynamically allocated memory) = %p\n", dyn);
    printf("Address of gv1, an uninitialized global variable = %p\n", &gv1);
    printf("Address of gv2, an initialized global variable = %p\n", &gv2);
    printf("Address of func1 = %p\n", func1);
    printf("Address of main = %p\n", main);

    printf("\nPID = %d\n", getpid());

    func1();

    while(1);

    return 0;
}

void func1() {

    int lv2 = 999;
    printf("Address of lv2, a local variable of func1 = %p\n", &lv2);
    return; 
}

a. In general, a program can have these elements:

  • Global Variables - gv1 and gv2
  • Functions - main and func1
  • Every function will have it’s own set of private / local variables - lv1 for main and lv2 in func1
  • Might have dynamically allocated variables - dyn
  • Functions from external / Shared Libraries - printf(), getpid()

b. Every element in the above list is stored in a particular location based on what they are. Eg: Machine code is stored in a particular memory location. Global Variables are stored in a different memory location. In this article, we will see how a program’s memory is divided into different locations where all these elements are stored.

2. Program Vs Process

Sometimes, we use the words Program and Process interchangbly but they definitely not the same. It is important to understand the difference.

  • A Program is typically an executable stored in the Hard-Disk. It is an ELF File with segments, sections, headers etc.,

  • When you run a program, you spawn a process. You can think of a process as a Program under Execution . A process lives in the main memory, but a program just exists dead in Hard Disk.

  • A process lives because it is being executed, every instruction in it is being executed, every variable is allocated some memory, values are changing, functions are being called etc., There is so much happening when a program is executed, but a program by itself is just a file sitting in the Hard-Disk waiting to be executed.

3. How does a process look like in memory?

We said that a process is program under execution. We have seem how a program looks like. It has ELF Header, Program Header Table, Section Header table, Segments, sections etc.,

The Program Headers will decide the Memory Layout of a process.

Now, let us look at the process. This is rough Memory Layout of any process. This is how a process lives in Main Memory.

memory_layout

Let us start discussing from bottom to top of that image.

a. Text Section : This section has pure machine code in it. It contains all the functions we would have written in the program in binary form. Note that starts from the Lowest Address .

b. Data section : This section is where Global Initialized Variables are stored. It is right above the Text section.

c. BSS section : BSS section contains all Global Uninitialized Variables. As far as I know, BSS is know as Better Save Space because in the object file, it does not take any space except the name and size of the variable. It is given actual memory only when the program is executed - BSS variables come alive :P

d. Heap : First thing I want to clarify is that this Heap has no relation with the Heap Data Structure . This Heap refers to a Heap of Memory like Heap of Clothes . Just a huge pile of memory which we can allocate for our variables and use at runtime.

  • If memory is allocated dynamically at runtime, then that memory belongs to Heap . malloc() , calloc() functions in C and new function in C++ allocate memory from heap.

Eg: In code1.c program, dyn is a pointer to an integer. malloc() takes in an arguments of size of memory we want and returns a pointer to that memory. This memory is taken from Heap. You will understand this clearly when we go through the example.

  • Observe the arrow mark from Heap pointing upwards. Let us take an example to understand what this pointing upwards means. Consider this program code2.c .

    ~/rev_eng_series/post_4$ cat code2.c
    #include<stdio.h>
    #include<stdlib.h>
        
    int main() {
        
    int *ptr, i = 0;
        
    while(i < 1000) {
        ptr = (int *)malloc(sizeof(int));
        printf("%p\n", ptr);
        i++;
    }
        
    return 0;
    } 
    
  • It is a pretty straight forward program. It allocates memory using malloc and stores that Address return by malloc in ptr. The value of ptr is printed. So, we are printing the address of that memory chunk allocated by malloc. The same process is being repeated for 1000 times.

  • Point of this program is, more and more memory is allocated by malloc. Look at the following output.

    ----------
    ----------
    0x1a05f00
    0x1a05f20
    0x1a05f40
    0x1a05f60
    0x1a05f80
    0x1a05fa0
    0x1a05fc0
    0x1a05fe0
    0x1a06000
    0x1a06020
    0x1a06040
    0x1a06060
    0x1a06080
    0x1a060a0
    0x1a060c0
    0x1a060e0
    0x1a06100
    ---------
    ---------
    
  • Observe the addresses. The Address Value is increasing for every new malloc you do. So, when you allocate more and more memory, memory of higher addresses get allocated. Refer to that diagram of ours. The Top of the Diagram denoted High addresses and this arrow was pointing upwards. In other words, Heap grows upwards.

  • As an experiment, in code2.c , remove that condition i < 1000 and replace it with an infinite loop and see what happens.

e. Shared Libraries : The most commonly used Shared Library by a C program is the libc . C++ programs use libc++ .

f. Stack : This part of process memory is used to store Local Variables of a function. Stack is the Back-Bone of any program because Control Flow (Function Call and Return) is maintained with the help of the stack. We will see how exactly Stack behaves as a back-bone of a program in upcoming posts.

  • Note that Stack grows downwards. That is, It starts with Higher Addresses and continues to grow towards lower addresses - just opposite of Heap.

Now that theory is done, let us dive into practicals part and experiment with what all we have discussed so far.

4. Practicals

Consider code1.c . Compile it and get a program named code1 program.

a. This is the output of code1 is something like this.

~/rev_eng_series/post_4$ ./code1
Address of lv, a local variable = 0x7ffef69b45dc
Value of dyn(dynamically allocated memory) = 0x1e91010
Address of gv1, an uninitialized global variable = 0x601058
Address of gv2, an initialized global variable = 0x601050
Address of func1 = 0x4006f5
Address of main = 0x400626

PID = 5667
Address of lv2, a local variable of func1 = 0x7ffef69b45b4

b. Compare the Output of the code1.c with the diagram which we discussed earlier. Compare the Addresses of every segment. It should perfectly match what we have discussed.

  • Address of lv(Local variable - Stack) > Value of dyn(Dynamically allocated - Heap) > Address of gv1(Uninitialized variable - BSS) > Address of gv2(Initialized variable - Data Section) > Address of main(Machine code - Text Section)

NOTE : The program comes to a still state because of that while(1) . This Infinite Loop is put just to keep the process alive in the main memory and we can do our analysis with ease. Open up a new terminal for further analysis.

  • In an Operating System, there can be hundreds of processes running. There should be some method to identify a process in a unique manner(like the way you have your Social Security Number or Aadhar Number in India) . The OS assigns a Process ID to every process. As our code1 is still running, we can get it’s Process ID / PID .

  • The PID = 5667 . I have put a printf() statement in code1.c which prints the PID just to make our analysis easy. Another way to get PID of a process is to use ps command.

  • Once we have the PID of any process, the best way to learn about that particular process is to go to the /proc directory. proc is short form for process. This has details of every process running in the OS. Let’s go to that directory.

    /proc$ ls
    1     1638  230   28    3307  3824  48    99           mdstat
    10    1709  236   290   3317  385   49    acpi         meminfo
    100   1757  2380  2999  3318  3859  4983  asound       misc
    1035  18    24    30    333   3886  50    buddyinfo    modules
    105   180   2416  3001  3341  39    51    bus          mounts
    106   181   2442  3012  3365  3903  5151  cgroups      mtrr
    1068  182   2443  3019  3375  393   5152  cmdline      net
    11    183   2449  3045  3380  3960  5178  consoles     pagetypeinfo
    1111  184   2451  3088  3385  4     5188  cpuinfo      partitions
    1113  185   25    3089  3394  40    53    crypto       sched_debug
    1148  1859  2504  3090  34    4090  5353  devices      schedstat
    115   186   2566  3091  3405  41    54    diskstats    scsi
    1153  1860  2578  3092  3418  4155  5449  dma          self
    1155  1861  26    3094  3420  4158  5454  driver       slabinfo
    1157  1862  2601  3096  3439  4170  5456  execdomains  softirqs
    1159  1863  2615  3097  3485  4193  5528  fb           stat
    1166  187   2616  31    3507  42    5531  filesystems  swaps
    1168  1870  2620  3128  3514  4218  5552  fs           sys
    1173  19    2621  3191  3518  4221  5556  i8k          sysrq-trigger
    12    191   2630  3197  3525  4224  5557  interrupts   sysvipc
    13    192   2635  32    3638  4228  56    iomem        thread-self
    132   1923  2647  321   3648  4232  5627  ioports      timer_list
    1364  193   2653  3213  3650  4242  5644  irq          tty
    1399  194   2663  3217  3681  43    5667  kallsyms     uptime
    14    199   2675  3239  3694  4334  5694  kcore        version
    1405  2     2680  325   3706  4391  5695  keys         version_signature
    1430  20    2682  326   373   44    57    key-users    vmallocinfo
    15    201   2691  3280  38    45    5727  kmsg         vmstat
    1565  21    2696  3294  3811  46    6     kpagecgroup  zoneinfo
    1566  22    27    3296  3816  4618  7     kpagecount
    16    227   270   3299  3817  4620  8     kpageflags
    1614  2276  2706  33    3820  4677  855   loadavg
    1627  229   2721  3302  3821  4783  9     locks
    /proc$ 
    
  • See, it has directories named after processes’ PIDs. I want to explore about PID = 5667 . So, I will navigate into that directory.

    /proc/5667$ ls
    attr             exe        mounts         projid_map    status
    autogroup        fd         mountstats     root          syscall
    auxv             fdinfo     net            sched         task
    cgroup           gid_map    ns             schedstat     timers
    clear_refs       io         numa_maps      sessionid     timerslack_ns
    cmdline          limits     oom_adj        setgroups     uid_map
    comm             loginuid   oom_score      smaps         wchan
    coredump_filter  map_files  oom_score_adj  smaps_rollup
    cpuset           maps       pagemap        stack
    cwd              mem        patch_state    stat
    environ          mountinfo  personality    statm
    /proc/5667$ 
    
  • It has a lot of information about the process. As we are discussing about the memory layout of the process, we will look into a memory-related of files present here.

  • First, let us look into the map_files directory.

    /proc/5667/map_files$ ls -l
    total 0
    lr-------- 1 adwi adwi 64 Aug 18 17:30 400000-401000 -> /home/adwi/rev_eng_series/post_4/code1
    lr-------- 1 adwi adwi 64 Aug 18 17:30 600000-601000 -> /home/adwi/rev_eng_series/post_4/code1
    lr-------- 1 adwi adwi 64 Aug 18 17:30 601000-602000 -> /home/adwi/rev_eng_series/post_4/code1
    lr-------- 1 adwi adwi 64 Aug 18 17:30 7f4e217a0000-7f4e21960000 -> /lib/x86_64-linux-gnu/libc-2.23.so
    lr-------- 1 adwi adwi 64 Aug 18 17:30 7f4e21960000-7f4e21b60000 -> /lib/x86_64-linux-gnu/libc-2.23.so
    lr-------- 1 adwi adwi 64 Aug 18 17:30 7f4e21b60000-7f4e21b64000 -> /lib/x86_64-linux-gnu/libc-2.23.so
    lr-------- 1 adwi adwi 64 Aug 18 17:30 7f4e21b64000-7f4e21b66000 -> /lib/x86_64-linux-gnu/libc-2.23.so
    lr-------- 1 adwi adwi 64 Aug 18 17:30 7f4e21b6a000-7f4e21b90000 -> /lib/x86_64-linux-gnu/ld-2.23.so
    lr-------- 1 adwi adwi 64 Aug 18 17:30 7f4e21d8f000-7f4e21d90000 -> /lib/x86_64-linux-gnu/ld-2.23.so
    lr-------- 1 adwi adwi 64 Aug 18 17:30 7f4e21d90000-7f4e21d91000 -> /lib/x86_64-linux-gnu/ld-2.23.so
    /proc/5667/map_files$ 
    
    • This directory has a list of all the files(in the Hard-Disk) which are mapped into the main memory. Mapping simply means that copying the file on a Hard-Disk to Main Memory. This mapping is done by a function called mmap . To learn more about mmap, refer to it’s manual page.

    • You can see that there are 10 entries here. The first 3 Entries are mappings of code1 / executable.

    • The next 4 entries are mapping of something called libc-2.23.so . libc is the name given to the Standard C Library . 2.23 is the version number. so stands for Shared Object. This Shared Object is an Object file with all the C Library Functions in a compiled form. It has all our printf(), scanf(), fopen(), fclose() etc., in machine code form. As our program is using C library functions, it is mapped to the process’s memory. To know more about libc, refer it’s manpage.

    • The next 3 entries are mapping of ld-2.23.so .This is the ELF Dynamic Linker. This program will find, load / copy the shared libraries used by the executable onto it’s memory. As our program is using C library functions, it is ld-2.23.so’s responsibility to copy libc-2.23.so onto main memory.

    • Here, those 0x400000 - 0x401000 , 7f4e21d90000-7f4e21d91000 etc., are all address spaces. These address spaces are range of Addresses used to store something related to the process.

    • One question should arise here. Why is the code1 mapped 3 times in 3 different address spaces? or libc-2.23.so mapped into 4 address spaces? Let us dig more to understand this.

  • Let us see the maps file.

    00400000-00401000 r-xp 00000000 08:02 3805083                            /home/adwi/rev_eng_series/post_4/code1
    00600000-00601000 r--p 00000000 08:02 3805083                            /home/adwi/rev_eng_series/post_4/code1
    00601000-00602000 rw-p 00001000 08:02 3805083                            /home/adwi/rev_eng_series/post_4/code1
    01e91000-01eb2000 rw-p 00000000 00:00 0                                  [heap]
    7f4e217a0000-7f4e21960000 r-xp 00000000 08:02 25694643                   /lib/x86_64-linux-gnu/libc-2.23.so
    7f4e21960000-7f4e21b60000 ---p 001c0000 08:02 25694643                   /lib/x86_64-linux-gnu/libc-2.23.so
    7f4e21b60000-7f4e21b64000 r--p 001c0000 08:02 25694643                   /lib/x86_64-linux-gnu/libc-2.23.so
    7f4e21b64000-7f4e21b66000 rw-p 001c4000 08:02 25694643                   /lib/x86_64-linux-gnu/libc-2.23.so
    7f4e21b66000-7f4e21b6a000 rw-p 00000000 00:00 0
    7f4e21b6a000-7f4e21b90000 r-xp 00000000 08:02 25694615                   /lib/x86_64-linux-gnu/ld-2.23.so
    7f4e21d71000-7f4e21d74000 rw-p 00000000 00:00 0
    7f4e21d8f000-7f4e21d90000 r--p 00025000 08:02 25694615                   /lib/x86_64-linux-gnu/ld-2.23.so
    7f4e21d90000-7f4e21d91000 rw-p 00026000 08:02 25694615                   /lib/x86_64-linux-gnu/ld-2.23.so
    7f4e21d91000-7f4e21d92000 rw-p 00000000 00:00 0
    7ffef6996000-7ffef69b7000 rw-p 00000000 00:00 0                          [stack]
    7ffef69bd000-7ffef69c0000 r--p 00000000 00:00 0                          [vvar]
    7ffef69c0000-7ffef69c2000 r-xp 00000000 00:00 0                          [vdso]
    ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
    
  • Entry 1:

    • Address space : 0x400000 - 0x401000
    • The Text Section of a program is mapped onto this address space.The Text Segment contains Header details and the C program we wrote in form of machine language.
    • Second column is r-xp . r stands for Read Permission, w stands for Write Permission and x stands for Executable Permission. This Address space has Read and Executable Permissions. It means, it can be read and it can be executed also. Generally, any segment which has Executable Permissions will also have Read Permissions because you have to read and process the segment to execute code in it.
  • Entry 2:

    • Address space : 0x600000 - 0x601000
    • The read-only section is mapped onto this address space. This Section consists of Hard-coded strings etc., It’s permissions is r–p as expected.
  • Entry 3:

    • Address space : 0x601000 - 0x602000
    • This is the Data Section. It has all the global variables in it. Check out the addresses of gv1 and gv2 of code1 .
    • It’s permissions is rw-p .
  • The point to understand is, all these 3 sections belong to the same file - /rev_eng_series/post_4/code1 . So, the same file is not being mapped thrice, but 3 different sections(Text, Read-Only, Data) of the same executable file are mapped to 3 different address spaces.

  • Entry 4:

    • Address space : 01e91000-01eb2000
    • This is the heap . This whole Address space is given to this process and the process can use it whenever it wants. Check out the value of dyn . Whenever the process wants memory at runtime(Dynamic allocation), a piece of memory of requested size is given to the process.
    • It’s permissions is rw-p .
  • Entry 5, 6, 7 & 8:

    • There are 4 Address spaces.
    • Notice that each Address space has a different set of permissions. This means the Shared Library libc-2.23.so has segments of those permissions. Each of those segments is mapped in a different Address space.
  • Entry 10, 12 & 13:

    • There are 3 Address spaces.
    • ld-2.23.so has 3 parts with different permissions. So, each part is mapped into a different address space.
  • Entry 14:

    • Address space: 7f4e21d91000-7f4e21d92000
    • This is the stack Address space. This is the place where all the local variables, arguments to functions, control information are stored.
    • It’s permissions are rw-p .
  • Entry 15, 16 and 17 require a bit of Operating System Internals which we have not discussed as of now. We will probably discuss in later posts, but will discuss it for sure!

  • We just had a look at how a process looks like in memory. There are different address spaces, each address space serving a particular purpose.

With this, I hope you have understood how a program looks like in memory when it is being executed.

5. A few more important things!

a. Why does the heap grow upward and stack downward?

  • There is no rule that heap should grow upward and stack downward. The growth of heap and stack depends on the processor . We are using x86 where stack is designed to grow down.

  • There are architectures like SPARC , ARM(Famous mobile-phone architecture) where the growth direction can be chosen. So, stack can either grow down or up.

  • This stackoverflow answer should give you more insight: Link

b. Do we always need to map shared libraries?

  • Library functions like printf(), scanf() etc., are present in libc-2.23.so . When they are called, the Dynamic Linker - ld-2.23.so gives an address to the Library function called(Dynamically Links it) and after the function is given an address, it works like any other normal function.

  • The C / C++ programs we compile are Dynamically Linked by default. That is, the functions they use (say printf) are given addresses when it is called and then it is executed.

  • If shared libraries are not mapped onto the process’s memory, we cannot use the library functions if the programs are dynamically linked .

  • We can also do this. Suppose an executable is using printf. I can copy the complete sourcecode of printf into my executable. Now, even if I don’t map libc, I will be able to run printf because it is present in my executable itself.

  • In general, suppose a program uses 10 C library functions. Copy sourcecode of all those 10 library functions from libc into the executable while compiling. So, I can run the executable independent of libc.

  • This concept of copying sourcecode of functions being used by a program into it’s executable is known as Static Linking .

  • The sourcecode mentioned above refers to machine code of those functions.

  • Let us take an example to understand this better. Consider a simple hello.c .

    #include<stdio.h>
    int main() {
        printf("Hello world!\n");
        return 0;
    }
    
    • This program uses 1 C Library function - printf() .
    • Let us compile it in the following manners.

      ~/rev_eng_series/post_4$ gcc hello.c -o hello_dynamic 
      ~/rev_eng_series/post_4$ gcc hello.c -o hello_static --static
      
    • The first line, you get hello_dynamic which is a dynamically linked executable - It does not have the sourcecode of printf() in it. It just has information which will help in dynamic linking.

    • The second will result in hello_static . This will have the whole sourcecode of printf() .

    • Check this out:

      ~/rev_eng_series/post_4$ ls -l hello_dynamic hello_static -rwxrwxr-x 1 adwi adwi 8600 Aug 18 22:22 hello_dynamic -rwxrwxr-x 1 adwi adwi 912664 Aug 18 22:22 hello_static ~/rev_eng_series/post_4$

    • Just observe the size difference. I hope you are able to catch the point.

    • Let us use the file command and analyze these.

      ~/rev_eng_series/post_4$ file hello_dynamic hello_dynamic: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=0d3706238a91f3ab4323e48c5a2971f5950aa7a3, not stripped 
      ~/rev_eng_series/post_4$ file hello_static hello_static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=d8e808a57289057b01ffd14f67cb8cc887b0e615, not stripped ~/rev_eng_series/post_4$
      
    • The point of focus is that hello_dynamic is dynamically linked and it has interpreter = /lib64/ld-linux-x86-64.so.2 .

    • On the other hand, hello_static is statically linked and does not have the dynamic linker specified because it does not need one.
  • Statically Linked executables are Independent executables . They have no dependencies. They can run on a machine which does not have libc in it.

  • This article should give you more insight on when to use dynamic linking and when to use static linking.

c. Why are there different permissions for different address spaces? Can’t all have a simple rwxp rather than each one having a different one?

  • There is a long history behind this. And when it comes to permissions or allowing/disallowing someone to do something security comes into picture.

  • In late 1990s, there was a lot software being written and used. Then, permissions were not this strict. All address spaces were rwx . That is, every address space could be read, executed and written into.

  • Bad Guys started finding bugs / security holes in programs. There were holes which allowed them to execute whatever they wanted. They could inject / write code into the stack and execute it. If they are able to execute anything, they could bring the huge servers down, all user machines to crash or install Malicious Software like viruses, worms, trojans onto machines.

  • That’s when it struck that we need to have a strong control on what address space is being used for. Not all address spaces should be executable and writable.

  • So, Stack and Heap - Wherever data is stored, these were made rw-p . So, even if a bad guy is able to write code into stack / heap, he/she cannot execute it simply your OS doesn’t let you to.

  • In a similar manner, all executable address spaces were made r-xp . The Write permission was removed to make sure no bad guy should be able to change the sourcecode of a running program.

  • Notice that, If you able to write into an address space, you are not able to execute it or if you are executing instructions in that address space, you cannot write into it. It is either Write or Execute but never both for an address space. This Security Technique came to be known as W^X / Write XOR Execute .

  • It became a very successful security technique that it was implemented at the hardware level in all major processors like Intel, AMD .It is one bit of information. Intel guys called it WriteDisabled / WD bit. If this bit is set to 1, then you cannot write into it. AMD guys called it NoExecute / NX bit. If this bit is set to 0, you cannot write into it, you can execute it. Microsoft called it Data Execution Prevention .

  • In later posts, we will talk about what exactly those security holes are, how a bad guy can execute whatever he wants, how we can mitigate them, how to break W^X Security Technique and more.

d. To know more about /proc , best place is to visit it’s man page.

With this, I will end this post. I hope you learned something out of this post.

Thank you!


Go to next post: Program Execution Internals - Part1
Go to previous post: Introduction to x86 Assembly Programming