Last active
January 12, 2022 00:17
-
-
Save tenderlove/8ed325ce9b5cd56fb9625d047b131df8 to your computer and use it in GitHub Desktop.
Use fisk and fiddle to patch a Ruby method at runtime
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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