huan / sidecar

Easily hook/call binary functions using ES6 class with TypeScript annotation (Powered by Frida)
https://npmjs.com/package/sidecar
Apache License 2.0
47 stars 7 forks source link
assembly frida hook reverse-engineering

Sidecar

NPM NPM Version npm (tag) Powered by Frida

Sidecar is a runtime hooking tool for intercepting function calls by TypeScript annotation with ease, powered by Frida.RE.

Frida Sidecar

Image source: 1920s Raleigh Box Sidecar Outfit & ShellterProject

What is a "Sidecar" Pattern?

Segregating the functionalities of an application into a separate process can be viewed as a Sidecar pattern. The Sidecar design pattern allows you to add a number of capabilities to your application without the need of additional configuration code for 3rd party components.

As a sidecar is attached to a motorcycle, similarly in software architecture a sidecar is attached to a parent application and extends/enhances its functionalities. A Sidecar is loosely coupled with the main application.

— SOURCE: Sidecar Design Pattern in your Microservices Ecosystem, Samir Behara, July 23, 2018

What is a "Hooking" Patern?

Hook: by intercepting function calls or messages or events passed between software components. — SOURCE: Hooking, Wikipedia

Features

  1. Easy to use by TypeScript decorators/annotations
    1. @Call(memoryAddress) for make a API for calling memory address from the binary
    2. @Hook(memoryAddress) for emit arguments when a memory address is being called
  2. Portable on Windows, macOS, GNU/Linux, iOS, Android, and QNX, as well as X86, Arm, Thumb, Arm64, AArch64, and Mips.
  3. Powered by Frida.RE and can be easily extended by any agent script.

Requirements

  1. Mac: disable System Integrity Protection

Introduction

When you are running an application on the Linux, Mac, Windows, iPhone, or Android, you might want to make it programmatic, so that your can control it automatically.

The SDK and API are designed for achieving this, if there are any. However, most of the application have very limited functionalities for providing a SDK or API to the developers, or they have neither SDK nor API at all, what we have is only the binary executable application.

How can we make a binary executable application to be able to called from our program? If we can call the function in the application process directly, then we will be able to make the application as our SDK/API, then we can make API call to control the application, or hook function inside the application to get to know what happened.

I have the above question and I want to find an universal way to solve it: Frida is for rescue. Frida is a dynamic instrumentation toolkit for developers and reverse-engineers, which can help us easily call the internal function from a process, or hook any internal function inside the process. And it has a nice Node.js binding and TypeScript support, which is nice to me because I love TypeScript much.

That's why I start build this project: Sidecar. Sidecar is a runtime hooking tool for intercepting function calls by decorate a TypeScript class with annotation.

Here's an example code example for demostration that how easy it can help you to hook a exiting application.

Talk is cheap, show me the code

@Sidecar('chatbox')
class ChatboxSidecar extends SidecarBody {

  @Call(0x11c9)
  @RetType('void')
  mo (
    @ParamType('pointer', 'Utf8String') content: string,
  ): Promise<string> { return Ret(content) }

  @Hook(0x11f4)
  mt (
    @ParamType('pointer', 'Utf8String') content: string,
  ) { return Ret(content) }

}

async function main () {
  const sidecar = new ChatboxSidecar()
  await attach(sidecar)

  sidecar.on('hook', ({ method, args }) => {
    console.log('method:', method)
    console.log('args:', args)
    sidecar.mo('Hello from Sidecar'),
  })

  process.on('SIGINT',  () => detach(sidecar))
  process.on('SIGTERM', () => detach(sidecar))
}

main().catch(console.error)

Learn more from the sidecar example: https://github.com/huan/sidecar/blob/main/examples

To-do list

Explanation

1. Sidecar Steps

When we are running a Sidecar Class, the following steps will happend:

  1. From the sidecar class file, decorators save all configs to the class metadata.
    1. @Sidecar(): <src/decorators/sidecar/sidecar.ts>, <src/decorators/sidecar/build-sidecar-metadata.ts>
    2. @Call(): <src/decorators/call/call.ts>
    3. @ParamType(): <src/decorators/param-type/param-type.ts>
    4. @RetType(): <src/decorators/ret-type/ret-type.ts>
    5. @Hook(), @Name, etc.
  2. SidecarBody(<src/sidecar-body/sidecar-body.ts>) base class will generate agentSource:
    1. getMetadataSidecar()(<src/decorators/sidecar/metadata-sidecar.ts>) for get the sidecar metadata from the class
    2. buildAgentSource()(<src/agent/build-agent-source.ts>) for generate the agent source code for the whole sidecar system.
  3. Call the attach() method to attach the sidecar to the target
  4. Call the detach() method to detach the sidecar to the target

References

1. @Sidecar(sidecarTarget, initAgentScript)

  1. sidecarTarget : SidecarTarget,
  2. initAgentScript? : string,

The class decorator.

sidecarTarget is the executable binary name, and the initAgentScript is a Frida agent script that help you to do whatever you want to do with Frida system.

Example:

import { Sidecar } from 'sidecar'
@Sidecar('chatbox')
class ChatboxSidecar {}

It is possible to load a init agent script, for example:

const initAgentScript = 'console.log("this will be runned when the sidecar class initiating")'
@Sidecar(
  'chatbox', 
  initAgentScript,
)

sidecarTarget supports Spawn mode, by specifing the sidecarTarget as a array:

@Sidecar([
  '/bin/sleep', // command
  [10],         // args
])

To learn more about the power of initAgentScript, see also this great repo with lots of examples: Hand-crafted Frida examples

2. class SidecarBody

Base class for the Sidecar class. All Sidecar class need to extends from the SidecarBody, or the system will throw an error.

Example:

import { SidecarBody } from 'sidecar'
class ChatboxSidecar extends SidecarBody {}

3. @Call(functionTarget)

  1. functionTarget: FunctionTarget

The native call method decorator.

functionTarget is the address (in number type) of the function which we need to call in the executable binary.

Example:

import { Call } from 'sidecar'
class ChatboxSidecar {
  @Call(0x11c9) mo () {}
}

If the functionTarget is not the type of number, then it can be string or an FunctionTarget object. See FunctionTarget section to learn more about the advanced usage of FunctionTarget.

4. @Hook(functionTarget)

  1. functionTarget: FunctionTarget

The hook method decorator.

functionTarget is the address (in number type) of the function which we need to hook in the executable binary.

Example:

import { Hook } from 'sidecar'
class ChatboxSidecar {
  @Hook(0x11f4) mt () {}
}

If the functionTarget is not the type of number, then it can be string or an FunctionTarget object. See FunctionTarget section to learn more about the advanced usage of FunctionTarget.

5. @RetType(nativeType, ...pointerTypeList)

  1. nativeType : NativeType
  2. pointerTypeList : PointerType[]
import { RetType } from 'sidecar'
class ChatboxSidecar {
  @RetType('void') mo () {}

6. @ParamType(nativeType, ...pointerTypeList)

  1. nativeType : NativeType
  2. pointerTypeList : PointerType[]
import { ParamType } from 'sidecar'
class ChatboxSidecar {
  mo (
    @ParamType('pointer', 'Utf8String') content: string,
  ) {}

7. Name(parameterName)

TODO: to be implemented.

  1. parameterName: string

The parameter name.

This is especially useful for Hook methods. The hook event will be emit with the method name and the arguments array. If the Name(parameterName) has been set, then the event will have additional information for the parameter names.

import { Name } from 'sidecar'
class ChatboxSidecar {
  mo (
    @Name('content') content: string,
  ) {}

8. Ret(...args)

  1. args: any[]

Example:

import { Ret } from 'sidecar'
class ChatboxSidecar {
  mo () { return Ret() }

9. FunctionTarget

The FunctionTarget is where @Call or @Hook to be located. It can be created by the following factory helper functions:

  1. addressTarget(address: number, module?: string): memory address. i.e. 0x369adf. Can specify a second module to call address in a specified module
  2. agentTarget(funcName: string): the JavaScript function name in initAgentScript to be used
  3. exportTarget(exportName: string, exportModule?: string): export name of a function. Can specify a second moduleName to load exportName from it.
  4. objcTarget: to be added
  5. javaTarget: to be added

For convenice, the number and string can be used as FunctionTarget as an alias of addressTarget() and agentTarget(). When we are defining the @Call(target) and @Hook(target):

  1. if the target type is number, then it will be converted to addressTarget(target)
  2. if the target type is string, then it will be converted to agentTarget(target)

Example:

import { 
  Call,
  addressTarget,
} from 'sidecar'
class ChatboxSidecar {
  @Call(
    addressTarget(0x11c9)
  )
  mo () {}
}

9.1 agentTarget(funcName: string)

agentTarget let you specify a funcName in the initAgentScript source code, and will use it directly for advanced users.

There's two type of the AgentTarget usage: @Call and @Hook.

  1. AgentTarget with @Call: the funcName should be a JavaScript function instance in the initAgentScript. The decorated method call that function.
  2. AgentTarget with @Hook: the funcName should be a NativeCallback instance in the initAgentScript. The decorated method hook that callback.

Notes:

  1. The NativeFunction passed to @Call must pay attention to the Garbage Collection of the JavaScript inside Frida. You have to hold a reference to all the memory you alloced by yourself, for example, store them in a Object like const refHolder = { buf }, then make sure the refHolder will be hold unless you can free the memory that you have alloced before. (See also: Frida Best Practices)
  2. the NativeCallback passed to @Hook is recommended to be a empty function, like () => {} because it will be replaced by Sidecar/Frida. So you should not put any code inside it,

Debug utility: sidecar-dump

Sidecar provide a utility named sidecar-dump for viewing the metadata of the sidecar class, or debuging the frida agent init source code.

You can run this util by the following command:

$ npx sidecar-dump --help
sidecar-dump <subcommand>
> Sidecar utility for dumping metadata/source for a sidecar class

where <subcommand> can be one of:

- metadata - Dump sidecar metadata
- source - Dump sidecar agent source

For more help, try running `sidecar-dump <subcommand> --help`

sidecar-dump support two sub commands:

  1. metadata: dump the metadata for a sidecar class
  2. source: dump the generated frida agent source code for a sidecar class

1. sidecar-dump metadata

Sidecar is using decorators heavily, for example, we are using @Call() for specifying the process target, @ParamType() for specifying the parameter data type, etc.

Internally, sidecar organize all the decorated information as metadata and save them into the class.

the sidecar-dump metadata command is to viewing this metadata information, so that we can review and debug them.

For example, the following is the metadata showed by sidecar-dump for our ChatboxSidecar class from examples/chatbox-sidecar.ts.

$ sidecar-dump metadata examples/chatbox-sidecar.ts
{
  "initAgentScript": "console.log('inited...')",
  "interceptorList": [
    {
      "agent": {
        "name": "mt",
        "paramTypeList": [
          [
            "pointer",
            "Utf8String"
          ]
        ],
        "target": "agentMt_PatchCode",
        "type": "agent"
      }
    }
  ],
  "nativeFunctionList": [
    {
      "agent": {
        "name": "mo",
        "paramTypeList": [
          [
            "pointer",
            "Utf8String"
          ],
          [
            "pointer",
            "Utf8String"
          ]
        ],
        "retType": [
          "void"
        ],
        "target": "agentMo",
        "type": "agent"
      }
    }
  ],
  "sidecarTarget": "/tmp/t/examples/chatbox/chatbox-linux"
}

2. sidecar-dump source

Sidecar is using Frida to connect to the program process and make communication with it.

In order to make the connection, sidecar will generate a frida agent source code, and using this agent as the bridge between the sidecar, frida, and the target program process.

the sidecar-dump source command is to viewing this frida agent source, so that we can review and debug them.

For example, the following is the source code showed by sidecar-dump for our ChatboxSidecar class from examples/chatbox-sidecar.ts.

$ sidecar-dump source  examples/chatbox-sidecar.ts
...
const __sidecar__mo_Function_wrapper = (() => {
  const nativeFunctionAddress =
    __sidecar__moduleBaseAddress
    .add(0x11e9)

  const nativeFunction = new NativeFunction(
    nativeFunctionAddress,
    'int',
    ['pointer'],
  )

  return function (...args) {
    log.verbose(
      'SidecarAgent',
      'mo(%s)',
      args.join(', '),
    )

    // pointer type for arg[0] -> Utf8String
    const mo_NativeArg_0 = Memory.alloc(1024 /*Process.pointerSize*/)
    mo_NativeArg_0.writeUtf8String(args[0])

    const ret = nativeFunction(...[mo_NativeArg_0])
    return Number(ret)
  }
})()

;(() => {
  const interceptorTarget =
    __sidecar__moduleBaseAddress
    .add(0x121f)

  Interceptor.attach(
    interceptorTarget,
    {
      onEnter: args => {
        log.verbose(
          'SidecarAgent',
          'Interceptor.attach(0x%s) onEnter()',
          Number(0x121f).toString(16),
        )

        send(__sidecar__payloadHook(
          'mt',
          [ args[0].readUtf8String() ]
        ), null)

      },
    }
  )
})()

rpc.exports = {
  mo: __sidecar__mo_Function_wrapper,
}

You can dump the sidecar agent source code to a javascript file, then using it with frida directly for debugging & testing.

$ sidecar-dump source chatbox-sidecar.ts > agent.js
$ frida chatbox -l agent.js
     ____
    / _  |   Frida 14.2.18 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/

[Local::chatbox]-> rpc.exports.mo('hello from frida cli')

Resources

RPA Examples

  1. Quake REST API demo

Papers

  1. Assembly to Open Source Code Matching for Reverse Engineering and Malware Analysis

Dll

  1. Wikipedia: DLL injection
  2. Code Tutorial: InjectDLL

Frida

  1. TypeScript - Frida环境搭建 - windows (给IDE提供智能感知/提示)
  2. TypeScript - Example - Frida agent written in TypeScript
  3. Talk Video - Prototyping And Reverse Engineering With Frida by Jay Harris
  4. Talk Video - r2con 2017 - Intro to Frida and Dynamic Machine Code Transformations by Ole Andre
  5. Hand-crafted Frida examples
  6. Slide - 基于 FRIDA 的全平台逆向分析 - caisi.zz@alipay.com (GitHub repo)
  7. Awesome Frida
  8. How to call methods in Frida Gadget (JavaScript API iOS)
  9. Frida调用栈符号恢复
  10. Cross-platform reversing with Frida, Oleavr, NoConName December 2015
  11. Frida: JavaScript API
  12. Calling native functions with Frida, @poxyran
  13. Shellcoding an Arm64 In-Memory Reverse TCP Shell with Frida, Versprite
  14. Anatomy of a code tracer, Ole André Vadla Ravnås, Oct 24, 2014
  15. frida-boot 👢 a binary instrumentation workshop, using Frida, for beginners, @leonjza
  16. frida javascript api手册
  17. Frida 12.7 Released - CModule
  18. Getting Started with Frida: Hooking a Function and Replacing its Arguments

Unicode

  1. 字符编码笔记:ASCII,Unicode 和 UTF-8, 阮一峰,2007年10月28日

Assembler

ObjC

Java

Related project: FFI Adapter

I have another NPM module named ffi-adapter, which is a Foreign Function Interface Adapter Powered by Decorator & TypeScript.

Learn more about FFI from its examples at https://github.com/huan/ffi-adapter/tree/master/tests/fixtures/library

Badge

Powered by Sidecar

[![Powered by Sidecar](https://img.shields.io/badge/Powered%20By-Sidecar-brightgreen.svg)](https://github.com/huan/sidecar)

Demos for Community

We have created different demos that work-out-of-box for some use cases.

You can visit them at Sidecar Demos if you are interested.

History

master v1.0 (Nov 24, 2021)

  1. ES Modules support (#17)
  2. TypeScript version 4.5
  3. Breaking change: Add hook event for all hooked methods

v0.14 (Aug 13, 2021)

Publish to NPM as sidecar package name!

  1. Enforce AgentTarget not to be decorated by neither @ParamType nor @RetType for prevent confusing.

v0.12 (Aug 5, 2021)

  1. Refactor wrappers for include '[' and ']' in array return string
  2. agentTarget now point to JavaScript function in initAgentScript instead of NativeFunction
  3. Add scripts/post-install.ts to double check frida_binding.node existance and run prebuild-install with cdn if needed (#14)

v0.9 (Jul 29, 2021)

  1. agentTarget will use NativeFunction instead of a plain javascript function
  2. Clean sidecar frida agent templates
    1. Use closure to encapsulate variables
    2. Add __sidecar__ namespace for all variable names
  3. Enhance @Sidecar() to support spawn target. e.g. @Sidecar(['/bin/sleep', [10]])
  4. Add .so & .DLL library example for Linux & Windows (Dynamic Library Example)
  5. Add support for raw pointer type

v0.6 (Jul 7, 2021)

  1. Upgrade to TypeScript 4.4-dev for supporting index signatures for symbols. (Microsoft/TypeScript#44512)
  2. Add sidecar-dump utility: it dump the sidecar metadata and source from a class defination file now.
  3. Add pack testing for sidecar-dump to make sure it works under Liniux, Mac, and Windows.

v0.2 (Jul 5, 2021)

  1. Add agent type support to FunctionTarget so that both @Call and @Hookcan use a pre-defined native function ptr defined from the initAgentScript. (more types like java, objc, name, and module to be added)

v0.1 (Jul 4, 2021)

First worked version, published to NPM as sidecar.

v0.0.1 (Jun 13, 2021)

Repo created.

Troubleshooting

1. Debug initAgentScript

If sidecar tells you that there's some script error internally, you should use sidecar-dump utility to dump the source of the frida agent script to a agent.js file, and use frida -l agent.js to debug it to get a clearly insight.

$ sidecar-dump source your-sidecar-class.ts > agent.js
$ frida -l agent.js
# by this way, you can locate the error inside the agent.js
# for easy debug and fix.

Special thanks

Thanks to Quinton Ashley @quinton-ashley who is the previous owner of NPM name sidecar and he transfer this beautify name to me for publishing this project after I requested via email. Appreciate it! (Jun 29, 2021)

Author

Huan LI (李卓桓), Microsoft Regional Director, zixia@zixia.net

Profile of Huan LI (李卓桓) on StackOverflow

Copyright & License