Skip to content

Instantly share code, notes, and snippets.

@tenderlove
Last active January 12, 2022 00:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tenderlove/8ed325ce9b5cd56fb9625d047b131df8 to your computer and use it in GitHub Desktop.
Save tenderlove/8ed325ce9b5cd56fb9625d047b131df8 to your computer and use it in GitHub Desktop.
Use fisk and fiddle to patch a Ruby method at runtime
require "fisk"
require "fisk/helpers"
require "fiddle/import"
module Ruby
extend Fiddle::Importer
dlload
typealias "VALUE", "uintptr_t"
typealias "struct rb_iseq_constant_body", "void"
Basic = [ "VALUE flags", "VALUE klass" ]
RBasic = struct Basic
RTypedData = struct Basic + [ "void * type", "VALUE typed_flag", "void * data" ]
RBISeqT = struct [ "VALUE flags",
"VALUE wrapper",
"struct rb_iseq_constant_body * body",
"void * exec" ]
class RBISeqT
def constant_body
RBISeqConstantBody.new body
end
end
RBISeqConstantBody = struct [
"int type",
"unsigned int iseq_size",
"VALUE * iseq_encoded"
]
class RBISeqConstantBody
def instructions
Fiddle::Pointer.new iseq_encoded.to_i, iseq_size
end
end
def self.rb_iseq_t obj
RBISeqT.new data_ptr(obj)
end
def self.data_ptr obj
addr = Fiddle.dlwrap(obj)
RTypedData.new(addr).data
end
# Generate some code and return the address for the generated code.
# In this example, we'll generate some assembly that prints a string
# and then exits the process. The address for the string is actually
# the underlying char buffer for the Ruby string object
def self.generate_exit next_insn
fisk = Fisk.new
jitbuf = Fisk::Helpers.jitbuffer 4096
str = "fooooooooo\n"
wrapper = Fiddle::Pointer[str]
fisk.asm(jitbuf) do
push rdi
mov rdi, imm32(1) # File number for stdout
mov rsi, imm64(wrapper.to_i) # Address of the char * backing str
mov rdx, imm32(str.bytesize) # Number of bytes in the string
mov rax, imm32(0x02000004) # write syscall on macOS x86_64
syscall
pop rdi
mov r8, imm64(next_insn)
jmp r8
end
jitbuf.memory
end
end
def thing
puts "hello"
end
# First call "thing" to prove it's a real Ruby method
thing
# Get the iseq object for the `thing` method
iseq = RubyVM::InstructionSequence.of(method(:thing))
# Get the underlying rb_iseq_t structure
rb_iseq = Ruby.rb_iseq_t(iseq)
# Get the constant body from the iseq. It holds the actual instructions
constant_body = rb_iseq.constant_body
# Get a pointer to the instructions for this iseq
pointer = constant_body.instructions
# Get the address of the instruction we're going to patch
next_insn = pointer[0, Fiddle::SIZEOF_UINTPTR_T].unpack1("Q")
# Patch the first instruction to point at our generated code
pointer[0, Fiddle::SIZEOF_UINTPTR_T] = [Ruby.generate_exit(next_insn).to_i].pack("Q")
# Run the newly patched method
thing
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment