csegmentation-faultnasmcode-injectionvirus

Injection in an Elf x86-64 failed because of SEGV_ACCERR (gdb, nasm, c)


For a school project I have to inject any x86-64 Elf with a parasite which is basically an array of bytes (a string).

The basic behaviour is :

The method I choosed is a bit dirty. I inject a dynamically generated "shellcode" (printcode would fit better ^^) into the largest cave that I can find between two PT_LOAD segments. It is done throw a C program and nasm.

I make this segment readable/executable/writable, as well as the "real" executable PT_LOAD segment, which contains the .text section. I also change the p_filesz and p_memsz of the injected segment by adding the size of the shellcode to them.

Everything seems to work fine for 96% of the binaries inside the /bin/ directory of ubuntu (I tested it with docker).

All the faulty injections are because of lack of space between the different PT_LOAD segments, except for one program : python3.12.

With this program, when I run the resulting parasited program (I mean python3.12 with the injection code in it), I have a segmentation fault :

--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_ACCERR, si_addr=0xa27000} ---

Here the full readelf -Wl output for python3.12 :

$> readelf -Wl python3.12 

Type de fichier ELF est EXEC (fichier exécutable)
Point d'entrée 0x657cc0
Il y a 14 en-têtes de programme, débutant à l'adresse de décalage 64

En-têtes de programme :
  Type           Décalage Adr. vir.          Adr.phys.          T.Fich.  T.Mém.   Fan Alignement
  PHDR           0x000040 0x0000000000400040 0x0000000000400040 0x000310 0x000310 R   0x8
  INTERP         0x000350 0x0000000000400350 0x0000000000400350 0x00001c 0x00001c R   0x1
      [Réquisition de l'interpréteur de programme: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000000400000 0x0000000000400000 0x01f588 0x01f588 R   0x1000
  LOAD           0x020000 0x0000000000420000 0x0000000000420000 0x2e27dd 0x2e27dd R E 0x1000
  LOAD           0x303000 0x0000000000703000 0x0000000000703000 0x323fe0 0x323fe0 R   0x1000
  LOAD           0x627dc8 0x0000000000a27dc8 0x0000000000a27dc8 0x17d3b8 0x180bc0 RW  0x1000
  DYNAMIC        0x627dd8 0x0000000000a27dd8 0x0000000000a27dd8 0x000200 0x000200 RW  0x8
  NOTE           0x000370 0x0000000000400370 0x0000000000400370 0x000020 0x000020 R   0x8
  NOTE           0x000390 0x0000000000400390 0x0000000000400390 0x000044 0x000044 R   0x4
  TLS            0x627dc8 0x0000000000a27dc8 0x0000000000a27dc8 0x000000 0x000010 R   0x8
  GNU_PROPERTY   0x000370 0x0000000000400370 0x0000000000400370 0x000020 0x000020 R   0x8
  GNU_EH_FRAME   0x5b0d84 0x00000000009b0d84 0x00000000009b0d84 0x0164c4 0x0164c4 R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
  GNU_RELRO      0x627dc8 0x0000000000a27dc8 0x0000000000a27dc8 0x000238 0x000238 R   0x1

 Correspondance section/segment :
  Sections de segment...
   00     
   01     .interp 
   02     .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 
   03     .init .plt .text .fini 
   04     .rodata .stapsdt.base .eh_frame_hdr .eh_frame 
   05     .init_array .fini_array .dynamic .got .got.plt .data .PyRuntime .probes .bss 
   06     .dynamic 
   07     .note.gnu.property 
   08     .note.gnu.build-id .note.ABI-tag 
   09     .tbss 
   10     .note.gnu.property 
   11     .eh_frame_hdr 
   12     
   13     .init_array .fini_array .dynamic .got 

The same for the parasited program :

En-têtes de programme :
  Type           Décalage Adr. vir.          Adr.phys.          T.Fich.  T.Mém.   Fan Alignement
  PHDR           0x000040 0x0000000000400040 0x0000000000400040 0x000310 0x000310 R   0x8
  INTERP         0x000350 0x0000000000400350 0x0000000000400350 0x00001c 0x00001c R   0x1
      [Réquisition de l'interpréteur de programme: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000000400000 0x0000000000400000 0x01f588 0x01f588 RW  0x1000
  LOAD           0x020000 0x0000000000420000 0x0000000000420000 0x2e27dd 0x2e27dd RWE 0x1000
  LOAD  (HERE)   0x303000 0x0000000000703000 0x0000000000703000 0x324227 0x324227 RWE 0x1000
  LOAD           0x627dc8 0x0000000000a27dc8 0x0000000000a27dc8 0x17d3b8 0x180bc0 RW  0x1000
  DYNAMIC        0x627dd8 0x0000000000a27dd8 0x0000000000a27dd8 0x000200 0x000200 RW  0x8
  NOTE           0x000370 0x0000000000400370 0x0000000000400370 0x000020 0x000020 R   0x8
  NOTE           0x000390 0x0000000000400390 0x0000000000400390 0x000044 0x000044 R   0x4
  TLS            0x627dc8 0x0000000000a27dc8 0x0000000000a27dc8 0x000000 0x000010 R   0x8
  GNU_PROPERTY   0x000370 0x0000000000400370 0x0000000000400370 0x000020 0x000020 R   0x8
  GNU_EH_FRAME   0x5b0d84 0x00000000009b0d84 0x00000000009b0d84 0x0164c4 0x0164c4 R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
  GNU_RELRO      0x627dc8 0x0000000000a27dc8 0x0000000000a27dc8 0x000238 0x000238 R   0x1

 Correspondance section/segment :
  Sections de segment...
   00     
   01     .interp 
   02     .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 
   03     .init .plt .text .fini 
   04     .rodata .stapsdt.base .eh_frame_hdr .eh_frame 
   05     .init_array .fini_array .dynamic .got .got.plt .data .PyRuntime .probes .bss 
   06     .dynamic 
   07     .note.gnu.property 
   08     .note.gnu.build-id .note.ABI-tag 
   09     .tbss 
   10     .note.gnu.property 
   11     .eh_frame_hdr 
   12     
   13     .init_array .fini_array .dynamic .got

If I'm not wrong : 0x703000 + 0x324224 = 0xA27224 and 0xA27224 - 0xA27000 = 0x224

So this specific address is inside a writable/readable/executable segment.

But I have the previously mentionned SIGSEGV with the si_code SEGV_ACCERR, which means the region where this address is has invalid permissions for mapped object.

I don't understand why, because the segment has all the needed rights (and more) and, after many hours of debugging, I'm out of ideas. I'm a beginner at asm programming, so maybe I misunderstood something.

A part of the shellcode, with the interesting addresses :

BITS 64
section .text
    global inject
inject:
<SNIP>; before the next line, I save all the register except few temporary registers
    mov rdi, 1
    lea rsi, [rel msg] # 0xa26ffc <= 0x27000 is "inside" this instruction
    mov rax, 1
    mov rdx, end - msg
<SNIP>; before the next line, it's the rc4 logic, not needed for my problem
    msg db '....STRNG....',0x0a,0x0 # 0xa270f4
    end db 0x0
    k db <SNIP>
    S db <SNIP>
<EOF>

Interestingly, I'm sure that the address 0xa27000 is somehow in fault, because if I save the registers like this :

    sub rsp, 88
    mov [rsp], rbx  
    mov [rsp], rcx  
    mov [rsp], rdx  
    mov [rsp], rsi  
    mov [rsp], rdi  
    mov [rsp], rbp  
    mov [rsp], r8  
    mov [rsp], r9   
    mov [rsp], r13  
    mov [rsp], r14  
    mov [rsp], r15 

mov [rsp], r9 is at the address 0xa27000, and it segfaults too.

If someone have an idea, I will be glad to read it. Thanks.

Edit

It appears that if I compile python3.12 it works perfectly.

The "only" difference is that the file from /bin on ubuntu is much tiny that the resulting one when I compile it manually. But if I strip the resulting compiled file, its size is lower than the python3.12 from ubuntu. I'm really lost.

weird result

Edit²

Employed Russian found the culprit : it is indeed the RELRO.

The old program only tried to find the largest empty space between two PT_LOAD segments.

Now, it acts differently : it tries to find a segment where :

As is, python3.12 can be injected like practically every other binaries inside the /bin/ directory of ubuntu.

I tried to change the type of the PT_GNU_RELRO segment to PT_NOTE, but for an unknown reason, it doesn't work. I think that for my current knowledge, I found the best solution that I could think about.

Have a full RELRO, partial RELRO doesn't matter. The python3.12 program I compiled was full RELRO (the real python3.12 has partial RELRO) and was injected like any other. The crucial point is really to compute if the v_addr of the injected code is inside the RELRO range. That's it.

Thanks again to Employed Russian for his help.


Solution

  • If I'm not wrong : 0x703000 + 0x324224 = 0xA27224 and 0xA27224 - 0xA27000 = 0x224 So this specific address is inside a writable/readable/executable segment.

    Except the RELRO segment tells the runtime loader: after you have relocated this file, use mprotect to set the [0xa27dc8, 0xa27dc8+0x238) as R (i.e. read-only).

    The loader can't do exactly that since 0xa27dc8 is not page aligned. So the loader rounds down 0xa27dc8 to the page size (i.e. to 0xa27000), and mprotects that page. Which means that 0xa27000 is no longer executable!

    QED.

    You can read about the RELRO here.

    When you build your own version of Python, you probably end up with a binary without the RELRO segment, because you didn't configure that binary to use it.