Avoiding the JMP in the JMP CALL POP technique for shellcode NASM?
Asked Answered
P

1

5

Even though both the programs abide by relative addressing instructions required for shellcode execution, and both print the desired message on execution, the 2nd Sample fails when used as a shellcode. Can anyone explain the behavior? Strangely a 3rd sample identical to the 1st one fails as well.

Output: Sample 1 Hello World

Other Samples(2&3) print garbage values

Sample 1

global _start
section .text
        _start:
                jmp widen

        pHworld:
                pop rsi
                xor rax,rax
                mov al,1
                mov rdi,rax
                mov rdx,rdi
                add rdx,11
                syscall
                ;Exit
                xor rax,rax
                mov rax,60
                xor rdi,rdi
                syscall
        widen:
                call pHworld
                Hworld db "Hello World",0xa

Sample 2

global _start
section .text
        _start:
                call pHworld
                Hworld db "Hello World",0xa

        pHworld:
                pop rsi
                xor rax,rax
                mov al,1
                mov rdi,rax
                mov rdx,rdi
                add rdx,11
                syscall
                ;Exit
                xor rax,rax
                mov rax,60
                xor rdi,rdi
                syscall

Sample 3

global _start
section .text
        _start:
                jmp label1

        label1:
                call pHworld
                Hworld db "Hello World",0xa

        pHworld:
                pop rsi
                xor rax,rax
                mov al,1
                mov rdi,rax
                mov rdx,rdi
                add rdx,11
                syscall
                ;Exit
                xor rax,rax
                mov rax,60
                xor rdi,rdi
                syscall

Unable to rest my curiosity , I tried yet another variation , and this fails (prints garbage values) even though I the objdump does not have any 0x00. Sample 4

global _start
section .text
pHworld:
    pop rsi
    xor rax,rax
    mov al,1
    mov rdi,rax
    mov rdx,rdi
    add rdx,11
    syscall
    xor rax,rax
    xor rdi,rdi
    mov al,60
    syscall

l1:
    call pHworld
    Hworld db "Hello World", 0xa

_start:
    jmp l1
    enter code here

Objdump of sample4


    ./hworld2.s:     file format elf64-x86-64


    Disassembly of section .text:

    0000000000400080 :
      400080:       5e                      pop    rsi
      400081:       48 31 c0                xor    rax,rax
      400084:       b0 01                   mov    al,0x1
      400086:       48 89 c7                mov    rdi,rax
      400089:       48 89 fa                mov    rdx,rdi
      40008c:       48 83 c2 0b             add    rdx,0xb
      400090:       0f 05                   syscall 
      400092:       48 31 c0                xor    rax,rax
      400095:       48 31 ff                xor    rdi,rdi
      400098:       b0 3c                   mov    al,0x3c
      40009a:       0f 05                   syscall 

    000000000040009c :
      40009c:       e8 df ff ff ff          call   400080 

    00000000004000a1 :
      4000a1:       48                      rex.W
      4000a2:       65                      gs
      4000a3:       6c                      ins    BYTE PTR es:[rdi],dx
      4000a4:       6c                      ins    BYTE PTR es:[rdi],dx
      4000a5:       6f                      outs   dx,DWORD PTR ds:[rsi]
      4000a6:       20 57 6f                and    BYTE PTR [rdi+0x6f],dl
      4000a9:       72 6c                   jb     400117 
      4000ab:       64                      fs
      4000ac:       0a eb                   or     ch,bl

    00000000004000ad :
      4000ad:       eb ed                   jmp    40009c 

Pomp answered 11/12, 2017 at 21:19 Comment(2)
The 3rd obviously isn't identical to the first. It uses jmp/call but with a different layout.Lotuseater
Note that 64-bit code doesn't need call/pop to get a PC-relative address into a register. You can use RIP-relative LEA. e.g. Avoiding 0xFF bytes in shellcode using CALL to read RIP?, or just jmp forward over your string and use lea rdi, [rel msg] for a negative rel32. It's not actually smaller than call/pop, but fewer instructions.Lotuseater
M
8

TL;DR : With shellcode you want to avoid encoding 0x00 bytes, otherwise when the code is used as a string for an exploit they will truncated at the first 0x00. This effectively will cut your code short.

The extra 0x00 bytes don't cause an issue when running outside an exploit because they aren't being converted to strings. The instructions are executed as is like any normal executable.


The reason for the JMP instruction in the JMP/CALL/POP method is to eliminate the insertion of unwanted 0x00 bytes in the generated code. JMP in 64-bit code has a rel8 and rel32 encoding. In 64-bit code CALL only has a rel32 encoding. This means that if you use CALL in 64-bit code to make a small transfer forward in memory it will be encoded as a 32-bit zero extended target. That zero extension would cause unwanted 0x00 values to be placed in the shell code. In 64-bit code this CALL instruction:

    call next
    nop
next:

Would be encoded as:

e8 01 00 00 00

Since the JMP instruction supports rel8 (relative byte displacement) NASM can generate a JMP instruction forward in memory if the target is no more than 127 bytes away (signed byte is -128 to +127). This JMP instruction:

    jmp next
    nop
next:

Would be encoded as:

eb 01

So no extra zeroes. You may ask why the CALL instruction in the JMP/CALL/POP method works. The reasoning is that negative values have their sign extended across the upper bytes. Filling the upper bits with 1 will not produce extra 0x00 bytes and so it works. This CALL instruction:

prev:
    call prev

Would be encoded as:

e8 fb ff ff ff

Notice that the extra bytes are not 0. This is why calling to a spot earlier in memory can avoid generating zeroes.


Sample 2

If we keep the above in mind we only need to examine the generated code for Sample 2 to see where things go wrong. objdump -D ./sample2 -Mintel generates:

0000000000000000 <_start>:
   0:   e8 0c 00 00 00          call   11 <pHworld>   <---------- Extra zeros

0000000000000005 <Hworld>:
   5:   48                      rex.W
   6:   65 6c                   gs ins BYTE PTR es:[rdi],dx
   8:   6c                      ins    BYTE PTR es:[rdi],dx
   9:   6f                      outs   dx,DWORD PTR ds:[rsi]
   a:   20 57 6f                and    BYTE PTR [rdi+0x6f],dl
   d:   72 6c                   jb     7b <pHworld+0x6a>
   f:   64 0a 5e 48             or     bl,BYTE PTR fs:[rsi+0x48]

0000000000000011 <pHworld>:
  11:   5e                      pop    rsi
  12:   48 31 c0                xor    rax,rax
  15:   b0 01                   mov    al,0x1
  17:   48 89 c7                mov    rdi,rax
  1a:   48 89 fa                mov    rdx,rdi
  1d:   48 83 c2 0b             add    rdx,0xb
  21:   0f 05                   syscall
  23:   48 31 c0                xor    rax,rax
  26:   b8 3c 00 00 00          mov    eax,0x3c       <---------- Extra zeros
  2b:   48 31 ff                xor    rdi,rdi
  2e:   0f 05                   syscall

So we see the issue with the CALL being encoded with extra zeroes which is why you need a traditional JMP/CALL/POP. There is a second issue with mov eax, 60 since it encodes extra bytes. I think you meant to use mov al, 60

Sample 3

objdump -D ./sample3 -Mintel generates:

0000000000000000 <_start>:
   0:   eb 00                   jmp    2 <label1>     <---------- Extra zeros

0000000000000002 <label1>:
   2:   e8 0c 00 00 00          call   13 <pHworld>   <---------- Extra zeros

0000000000000007 <Hworld>:
   7:   48                      rex.W
   8:   65 6c                   gs ins BYTE PTR es:[rdi],dx
   a:   6c                      ins    BYTE PTR es:[rdi],dx
   b:   6f                      outs   dx,DWORD PTR ds:[rsi]
   c:   20 57 6f                and    BYTE PTR [rdi+0x6f],dl
   f:   72 6c                   jb     7d <pHworld+0x6a>
  11:   64 0a 5e 48             or     bl,BYTE PTR fs:[rsi+0x48]

0000000000000013 <pHworld>:
  13:   5e                      pop    rsi
  14:   48 31 c0                xor    rax,rax
  17:   b0 01                   mov    al,0x1
  19:   48 89 c7                mov    rdi,rax
  1c:   48 89 fa                mov    rdx,rdi
  1f:   48 83 c2 0b             add    rdx,0xb
  23:   0f 05                   syscall
  25:   48 31 c0                xor    rax,rax
  28:   b8 3c 00 00 00          mov    eax,0x3c       <---------- Extra zeros
  2d:   48 31 ff                xor    rdi,rdi
  30:   0f 05                   syscall

Same type of issues as Sample 2.


Sample 1

objdump -D ./sample1 -Mintel generates:

0000000000000000 <_start>:
   0:   eb 1f                   jmp    21 <widen>

0000000000000002 <pHworld>:
   2:   5e                      pop    rsi
   3:   48 31 c0                xor    rax,rax
   6:   b0 01                   mov    al,0x1
   8:   48 89 c7                mov    rdi,rax
   b:   48 89 fa                mov    rdx,rdi
   e:   48 83 c2 0b             add    rdx,0xb
  12:   0f 05                   syscall
  14:   48 31 c0                xor    rax,rax
  17:   b8 3c 00 00 00          mov    eax,0x3c       <---------- Extra zeros
  1c:   48 31 ff                xor    rdi,rdi
  1f:   0f 05                   syscall

0000000000000021 <widen>:
  21:   e8 dc ff ff ff          call   2 <pHworld>

0000000000000026 <Hworld>:
  26:   48                      rex.W
  27:   65 6c                   gs ins BYTE PTR es:[rdi],dx
  29:   6c                      ins    BYTE PTR es:[rdi],dx
  2a:   6f                      outs   dx,DWORD PTR ds:[rsi]
  2b:   20 57 6f                and    BYTE PTR [rdi+0x6f],dl
  2e:   72 6c                   jb     9c <Hworld+0x76>
  30:   64                      fs
  31:   0a                      .byte 0xa

Although you say sample 1 works, it still has a problem that needs to be corrected. mov rax, 60 needs to be mov al, 60.

Matheson answered 12/12, 2017 at 1:3 Comment(4)
Thanks for such a lucid explanation! I will follow up on how encoding works. Also I did not know that the direction of the call could have an impact, thanks a lot for explaining that too!Pomp
Sir, Can you please also look into the edit and tell me why the sample-4 is not producing the output? What conceptual error am I making in restructuring the code that way?Pomp
@YuvrajSingh : Regarding Sample 4 In a executable you have an entry point (_start is the default) that allows the loader to start at any spot in the file. When it comes to running shell code - the shell code ALWAYS starts executing at the first instruction. So in Sample 4 when converted to shell code it starts at pop rsi which puts a random value in RSI as a pointer and tries to print it. That's why you start with a JMP as the first instruction.Matheson
Incidentally if I were constructing shellcode I'd put something more useful than a nop there, like one of my strings.Forge

© 2022 - 2024 — McMap. All rights reserved.