diff --git a/lib/caotral/binary/elf.rb b/lib/caotral/binary/elf.rb index fad0a02..172b522 100644 --- a/lib/caotral/binary/elf.rb +++ b/lib/caotral/binary/elf.rb @@ -4,6 +4,7 @@ require_relative "elf/program_header" require_relative "elf/section" require_relative "elf/section/dynamic" +require_relative "elf/section/hash" require_relative "elf/section/rel" require_relative "elf/section/strtab" require_relative "elf/section/symtab" @@ -14,9 +15,10 @@ module Caotral module Binary class ELF include Enumerable - attr_reader :sections + attr_reader :sections, :program_headers attr_accessor :header def initialize + @program_headers = [] @sections = [] @header = nil end diff --git a/lib/caotral/binary/elf/program_header.rb b/lib/caotral/binary/elf/program_header.rb index 9eaaf68..bf53dff 100644 --- a/lib/caotral/binary/elf/program_header.rb +++ b/lib/caotral/binary/elf/program_header.rb @@ -5,6 +5,27 @@ module Binary class ELF class ProgramHeader include Caotral::Binary::ELF::Utils + PF_X = 1 + PF_W = 2 + PF_R = 4 + PF = { + RWX: PF_R | PF_W | PF_X, + RW: PF_R | PF_W, + RX: PF_R | PF_X, + WX: PF_W | PF_X, + R: PF_R, + W: PF_W, + X: PF_X, + NOP: 0, + }.freeze + PF_BY_V = PF.invert.freeze + PT = { + NULL: 0, + LOAD: 1, + DYNAMIC: 2, + INTERP: 3, + }.freeze + PT_BY_V = PT.invert.freeze def initialize @type = num2bytes(0, 4) @flags = num2bytes(0, 4) @@ -27,6 +48,13 @@ def set!(type: nil, flags: nil, offset: nil, vaddr: nil, paddr: nil, filesz: nil @align = num2bytes(align, 8) if check(align, 8) self end + + def type = PT_BY_V[@type.pack("C*").unpack1("L<")] + def flags = PF_BY_V[@flags.pack("C*").unpack1("L<")] + def offset = @offset.pack("C*").unpack1("Q<") + def filesz = @filesz.pack("C*").unpack1("Q<") + def memsz = @memsz.pack("C*").unpack1("Q<") + private def bytes = [@type, @flags, @offset, @vaddr, @paddr, @filesz, @memsz, @align] end end diff --git a/lib/caotral/binary/elf/reader.rb b/lib/caotral/binary/elf/reader.rb index 89deda9..3300f5f 100644 --- a/lib/caotral/binary/elf/reader.rb +++ b/lib/caotral/binary/elf/reader.rb @@ -24,10 +24,28 @@ def read entry = header[24, 8].unpack1("Q<") phoffset = header[32, 8].unpack1("Q<") shoffset = header[40, 8].unpack1("Q<") + phsize = header[54, 2].unpack1("S<") + phnum = header[56, 2].unpack1("S<") shentsize = header[58, 2].unpack1("S<") shnum = header[60, 2].unpack1("S<") shstrndx = header[62, 2].unpack1("S<") - @context.header.set!(type:, arch:, entry:, phoffset:, shoffset:, shnum:, shstrndx:) + @context.header.set!(type:, arch:, entry:, phoffset:, phsize:, phnum:, shoffset:, shnum:, shstrndx:) + + @bin.pos = phoffset + phnum.times do |i| + ph_entry = @bin.read(phsize) + type = ph_entry[0, 4].unpack1("L<") + flags = ph_entry[4, 4].unpack1("L<") + offset = ph_entry[8, 8].unpack1("Q<") + vaddr = ph_entry[16, 8].unpack1("Q<") + paddr = ph_entry[24, 8].unpack1("Q<") + filesz = ph_entry[32, 8].unpack1("Q<") + memsz = ph_entry[40, 8].unpack1("Q<") + align = ph_entry[48, 8].unpack1("Q<") + ph = Caotral::Binary::ELF::ProgramHeader.new + ph.set!(type:, flags:, offset:, vaddr:, paddr:, filesz:, memsz:, align:) + @context.program_headers.push(ph) + end @bin.pos = shoffset shnum.times do |i| @@ -93,6 +111,16 @@ def read addend = rela ? rel_bin[16, 8].unpack1("q<") : nil Caotral::Binary::ELF::Section::Rel.new(addend: rela).set!(offset:, info:, addend:) end + when :dynamic + dyn_entsize = section.header.entsize + dyn_entsize = 16 if dyn_entsize.zero? + count = body_bin.bytesize / dyn_entsize + count.times.map do |i| + dyn_bin = body_bin.byteslice(i * dyn_entsize, dyn_entsize) + tag = dyn_bin[0, 8].unpack1("Q<") + un = dyn_bin[8, 8].unpack1("Q<") + Caotral::Binary::ELF::Section::Dynamic.new.set!(tag:, un:) + end when :progbits body_bin end diff --git a/lib/caotral/binary/elf/section/dynamic.rb b/lib/caotral/binary/elf/section/dynamic.rb index c531d20..77f7311 100644 --- a/lib/caotral/binary/elf/section/dynamic.rb +++ b/lib/caotral/binary/elf/section/dynamic.rb @@ -5,7 +5,19 @@ class ELF class Section class Dynamic include Caotral::Binary::ELF::Utils - TAG_TYPES = { NULL: 0 }.freeze + TAG_TYPES = { + NULL: 0, + HASH: 4, + STRTAB: 5, + SYMTAB: 6, + RELA: 7, + RELASZ: 8, + RELAENT: 9, + STRSZ: 10, + SYMENT: 11, + TEXTREL: 22, + }.freeze + TAG_TYPES_BY_V = TAG_TYPES.invert.freeze def initialize @tag = num2bytes(0, 8) diff --git a/lib/caotral/binary/elf/section/hash.rb b/lib/caotral/binary/elf/section/hash.rb new file mode 100644 index 0000000..258413d --- /dev/null +++ b/lib/caotral/binary/elf/section/hash.rb @@ -0,0 +1,20 @@ +require "caotral/binary/elf/utils" +module Caotral + module Binary + class ELF + class Section + class Hash + include Caotral::Binary::ELF::Utils + def initialize(nchain:, nbucket: 1) + @nbucket = num2bytes(nbucket, 4) + @nchain = num2bytes(nchain, 4) + @bucket = Array.new(nbucket, num2bytes(0, 4)) + @chain = Array.new(nchain, num2bytes(0, 4)) + end + + private def bytes = [@nbucket, @nchain, *@bucket, *@chain] + end + end + end + end +end diff --git a/lib/caotral/binary/elf/section/rel.rb b/lib/caotral/binary/elf/section/rel.rb index 5e17fe2..7aab386 100644 --- a/lib/caotral/binary/elf/section/rel.rb +++ b/lib/caotral/binary/elf/section/rel.rb @@ -5,6 +5,17 @@ class ELF class Section class Rel include Caotral::Binary::ELF::Utils + TYPES = { + AMD64_NONE: 0, + AMD64_64: 1, + AMD64_PC32: 2, + AMD64_GOT32: 3, + AMD64_PLT32: 4, + AMD64_COPY: 5, + AMD64_GLOB_DAT: 6, + }.freeze + TYPES_BY_V = TYPES.invert.freeze + def initialize(addend: true) @offset = num2bytes(0, 8) @info = num2bytes(0, 8) @@ -27,6 +38,7 @@ def addend end def sym = @info.pack("C*").unpack1("Q<") >> 32 def type = @info.pack("C*").unpack1("Q<") & 0xffffffff + def type_name = TYPES_BY_V[type] def addend? = !!@addend private def bytes = addend? ? [@offset, @info, @addend] : [@offset, @info] diff --git a/lib/caotral/binary/elf/section_header.rb b/lib/caotral/binary/elf/section_header.rb index 77f6aa4..20b3da7 100644 --- a/lib/caotral/binary/elf/section_header.rb +++ b/lib/caotral/binary/elf/section_header.rb @@ -6,6 +6,11 @@ class ELF class SectionHeader include Caotral::Binary::ELF::Utils SHT = { null: 0, progbits: 1, symtab: 2, strtab: 3, rela: 4, hash: 5, dynamic: 6, note: 7, nobits: 8, rel: 9, shlib: 10, dynsym: 11, }.freeze + SHF = { + WRITE: 0x1, + ALLOC: 0x2, + EXECINSTR: 0x4, + }.freeze SHT_BY_VALUE = SHT.invert.freeze def initialize @@ -47,6 +52,7 @@ def type = SHT_BY_VALUE[@type.pack("C*").unpack1("L<")] def info = @info.pack("C*").unpack1("L<") def addr = @addr.pack("C*").unpack1("Q<") def link = @link.pack("C*").unpack1("L<") + def addralign = @addralign.pack("C*").unpack1("Q<") private def bytes = [@name, @type, @flags, @addr, @offset, @size, @link, @info, @addralign, @entsize] end diff --git a/lib/caotral/linker.rb b/lib/caotral/linker.rb index e2d3e7a..2663d63 100644 --- a/lib/caotral/linker.rb +++ b/lib/caotral/linker.rb @@ -5,18 +5,18 @@ module Caotral class Linker - def self.link!(inputs:, output: "a.out", linker: "mold", debug: false, shared: false, executable: true) - new(inputs:, output:, linker:, debug:, shared:, executable:).link + def self.link!(inputs:, output: "a.out", linker: "mold", debug: false, shared: false, executable: true, pie: false) + new(inputs:, output:, linker:, debug:, shared:, executable:, pie:).link end - def initialize(inputs:, output: "a.out", linker: "mold", linker_options: [], executable: true, shared: false, debug: false) + def initialize(inputs:, output: "a.out", linker: "mold", linker_options: [], executable: true, shared: false, pie: false, debug: false) @inputs, @output, @linker = inputs, output, linker @options = linker_options - @executable, @debug, @shared = executable, debug, shared + @executable, @debug, @shared, @pie = executable, debug, shared, pie end - def link(inputs: @inputs, output: @output, debug: @debug, shared: @shared, executable: @executable) - return to_elf(inputs:, output:, debug:, shared:, executable:) if @linker == "self" + def link(inputs: @inputs, output: @output, debug: @debug, shared: @shared, executable: @executable, pie: @pie) + return to_elf(inputs:, output:, debug:, shared:, executable:, pie:) if @linker == "self" IO.popen(link_command).close end @@ -49,12 +49,12 @@ def link_command(inputs: @inputs, output: @output) def libpath = @libpath ||= File.dirname(Dir.glob("/usr/lib*/**/crti.o").last) def gcc_libpath = @gcc_libpath ||= File.dirname(Dir.glob("/usr/lib/gcc/x86_64-*/*/crtbegin.o").last) - def to_elf(inputs: @inputs, output: @output, debug: @debug, shared: @shared, executable: @executable) + def to_elf(inputs: @inputs, output: @output, debug: @debug, shared: @shared, executable: @executable, pie: @pie) elf_objs = inputs.map { |input| Caotral::Binary::ELF::Reader.new(input:, debug:).read } - builder = Caotral::Linker::Builder.new(elf_objs:, debug:, shared:, executable:) + builder = Caotral::Linker::Builder.new(elf_objs:, debug:, shared:, executable:, pie:) builder.resolve_symbols elf_obj = builder.build - Caotral::Linker::Writer.new(elf_obj:, output:, debug:, shared:, executable:).write + Caotral::Linker::Writer.new(elf_obj:, output:, debug:, shared:, executable:, pie:).write File.chmod(0755, output) if executable output end diff --git a/lib/caotral/linker/builder.rb b/lib/caotral/linker/builder.rb index e6017b0..4bbc477 100644 --- a/lib/caotral/linker/builder.rb +++ b/lib/caotral/linker/builder.rb @@ -5,21 +5,25 @@ module Caotral class Linker class Builder include Caotral::Binary::ELF::Utils + R_X86_64_64 = 1 R_X86_64_PC32 = 2 R_X86_64_PLT32 = 4 + R_X86_64_RELATIVE = 8 SYMTAB_BIND = { locals: 0, globals: 1, weaks: 2, }.freeze BIND_BY_VALUE = SYMTAB_BIND.invert.freeze - RELOCATION_SECTION_NAMES = [".rela.text", ".rel.text"].freeze + RELOCATION_SECTION_NAMES = [".rela.text", ".rel.text", ".rela.data", ".rel.data"].freeze ALLOW_RELOCATION_TYPES = [R_X86_64_PC32, R_X86_64_PLT32].freeze - GENERATED_SECTION_NAMES = [".text", ".strtab", ".symtab", ".shstrtab", /\.rela?\./, ".dynstr", ".dynsym", ".dynamic"].freeze + GENERATED_SECTION_NAMES = [".text", ".data", ".strtab", ".symtab", ".shstrtab", /\.rela?\./, ".dynstr", ".dynsym", ".dynamic", ".interp", ".rela.dyn"].freeze + SHT = Caotral::Binary::ELF::SectionHeader::SHT + SHF = Caotral::Binary::ELF::SectionHeader::SHF attr_reader :symbols - def initialize(elf_objs:, executable: true, debug: false, shared: false) + def initialize(elf_objs:, executable: true, debug: false, shared: false, pie: false) @elf_objs = elf_objs - @executable, @debug, @shared = executable, debug, shared + @executable, @debug, @shared, @pie = executable, debug, shared, pie @symbols = { locals: Set.new, globals: Set.new, weaks: Set.new } - sharing_object! + _mode! end def build @@ -36,6 +40,11 @@ def build section_name: ".text", header: Caotral::Binary::ELF::SectionHeader.new ) + data_section = Caotral::Binary::ELF::Section.new( + body: String.new, + section_name: ".data", + header: Caotral::Binary::ELF::SectionHeader.new + ) strtab_section = Caotral::Binary::ELF::Section.new( body: Caotral::Binary::ELF::Section::Strtab.new("\0".b), section_name: ".strtab", @@ -51,10 +60,18 @@ def build section_name: ".shstrtab", header: Caotral::Binary::ELF::SectionHeader.new ) + rela_dyn_section = Caotral::Binary::ELF::Section.new( + body: [], + section_name: ".rela.dyn", + header: Caotral::Binary::ELF::SectionHeader.new + ) + + data_section.header.set!(type: SHT[:progbits], flags: SHF[:ALLOC] | SHF[:WRITE], addralign: 8) start_bytes = [0xe8, *[0] * 4, 0x48, 0x89, 0xc7, 0x48, 0xc7, 0xc0, 0x3c, 0x00, 0x00, 0x00, 0x0f, 0x05] exec_text_offset = 0x1000 base_addr = 0x400000 + base_addr = 0 if @pie unless @executable start_bytes = [] base_addr = 0 @@ -66,7 +83,9 @@ def build elf.header = elf_obj.header.dup strtab_names = [] text_offsets = {} + data_offsets = {} text_offset = 0 + data_offset = 0 sym_by_elf = Hash.new { |h, k| h[k] = [] } @elf_objs.each do |elf_obj| text = elf_obj.find_by_name(".text") @@ -76,17 +95,27 @@ def build size = text.body.bytesize text_offset += size end + data = elf_obj.find_by_name(".data") + unless data.nil? + data_section.body << data.body + data_offsets[elf_obj.object_id] = data_offset + data_offset += data.body.bytesize + end strtab = elf_obj.find_by_name(".strtab") strtab.body.names.split("\0").each { |name| strtab_names << name } unless strtab.nil? symtab = elf_obj.find_by_name(".symtab") base_index = nil unless symtab.nil? base_index = symtab_section.body.size + text_index = elf_obj.sections.index(text) unless text.nil? + data_index = elf_obj.sections.index(data) unless data.nil? + symtab.body.each_with_index do |st, index| sym = Caotral::Binary::ELF::Section::Symtab.new name, info, other, shndx, value, size = st.build.unpack("L + +extern int foo; + +uintptr_t get_addr(void) { + uintptr_t x; + __asm__ volatile ("movabs $foo, %0" : "=r"(x)); + return x; +} + +int foo = 42; + +int main(void) { + return (int)get_addr(); +} diff --git a/sample/C/pie-object.c b/sample/C/pie-object.c new file mode 100644 index 0000000..d3ee408 --- /dev/null +++ b/sample/C/pie-object.c @@ -0,0 +1,3 @@ +int foo() { return 42; } + +int main() { return foo(); } diff --git a/sample/C/shared-object.c b/sample/C/shared-object.c index bf7759e..c1b207f 100644 --- a/sample/C/shared-object.c +++ b/sample/C/shared-object.c @@ -1 +1,2 @@ int foo() { return 42; } + diff --git a/sig/caotral/assembler.rbs b/sig/caotral/assembler.rbs index e5ae9d2..29fc5f0 100644 --- a/sig/caotral/assembler.rbs +++ b/sig/caotral/assembler.rbs @@ -5,6 +5,7 @@ class Caotral::Assembler @asm_reader: Caotral::Assembler::Reader @debug: bool + def self.assemble!: (input: String, ?output: String, ?assembler: String, ?assembler_options: Array[String] | [], ?debug: bool, ?shared: bool) -> String def initialize: (input: String, ?output: String, ?assembler: String, ?type: Symbol, ?debug: bool) -> void def assemble: (?assembler: String, ?assembler_options: Array[String] | [], ?input: String, ?output: String, ?debug: bool, ?shared: bool) -> String def obj_file: () -> String diff --git a/sig/caotral/binary/elf/section/hash.rbs b/sig/caotral/binary/elf/section/hash.rbs new file mode 100644 index 0000000..09c8343 --- /dev/null +++ b/sig/caotral/binary/elf/section/hash.rbs @@ -0,0 +1,4 @@ +class Caotral::Binary::ELF::Section::Hash + def initialize: (nchain: Integer, ?nbucket: Integer) -> void + def build: () -> String +end diff --git a/test/caotral/linker/pie-object_test.rb b/test/caotral/linker/pie-object_test.rb new file mode 100644 index 0000000..846c46f --- /dev/null +++ b/test/caotral/linker/pie-object_test.rb @@ -0,0 +1,115 @@ +require_relative "../../test_suite" + +class Caotral::Linker::PIEObjectLinkingTest < Test::Unit::TestCase + include TestProcessHelper + def setup + @inputs = ["pie.o"] + @output = "pie" + end + + def teardown + File.delete(@inputs[0]) if File.exist?(@inputs[0]) + File.delete(@output) if File.exist?(@output) + end + + def test_non_executable_pie_object + path = Pathname.new("sample/C/pie-object.c").to_s + IO.popen(["gcc", "-fPIE", "-c", "-o", @inputs.first, "%s" % path]).close + + Caotral::Linker.link!(inputs: @inputs, output: @output, linker: "self", executable: false, pie: true) + elf = Caotral::Binary::ELF::Reader.read!(input: @output, debug: false) + section_names = elf.sections.map(&:section_name) + program_header_types = elf.program_headers.map(&:type) + interp = elf.find_by_name(".interp") + dynamic = elf.find_by_name(".dynamic").body.last + assert_include(program_header_types, :LOAD) + assert_include(program_header_types, :DYNAMIC) + assert_include(program_header_types, :INTERP) + assert_equal(interp.body.split("\0").first, "/lib64/ld-linux-x86-64.so.2") + assert_equal(dynamic.null?, true) + assert_include(section_names, ".dynstr") + assert_include(section_names, ".dynsym") + assert_include(section_names, ".dynamic") + assert_include(section_names, ".interp") + assert_equal(:DYN, elf.header.type) + assert_equal(:AMD64, elf.header.arch) + assert_equal(0, elf.header.entry) + end + + def test_executable_pie_object + path = Pathname.new("sample/C/pie-object.c").to_s + IO.popen(["gcc", "-fPIE", "-c", "-o", @inputs.first, "%s" % path]).close + + Caotral::Linker.link!(inputs: @inputs, output: @output, linker: "self", pie: true) + elf = Caotral::Binary::ELF::Reader.read!(input: @output, debug: false) + section_names = elf.sections.map(&:section_name) + program_header_types = elf.program_headers.map(&:type) + interp = elf.find_by_name(".interp") + dynamic = elf.find_by_name(".dynamic").body.last + assert_include(program_header_types, :LOAD) + assert_include(program_header_types, :DYNAMIC) + assert_include(program_header_types, :INTERP) + assert_equal(interp.body.split("\0").first, "/lib64/ld-linux-x86-64.so.2") + assert_equal(dynamic.null?, true) + assert_include(section_names, ".dynstr") + assert_include(section_names, ".dynsym") + assert_include(section_names, ".dynamic") + assert_include(section_names, ".interp") + assert_equal(:DYN, elf.header.type) + assert_equal(:AMD64, elf.header.arch) + assert_not_equal(0, elf.header.entry) + end + + def test_movabs_pie_object + path = Pathname.new("sample/C/movabs.c").to_s + IO.popen(["gcc", "-fno-pic", "-fno-plt", "-c", "-o", @inputs.first, "%s" % path]).close + + Caotral::Linker.link!(inputs: @inputs, output: @output, linker: "self", executable: false, pie: true) + elf = Caotral::Binary::ELF::Reader.read!(input: @output, debug: false) + section_names = elf.sections.map(&:section_name) + program_header_types = elf.program_headers.map(&:type) + interp = elf.find_by_name(".interp") + dynamic = elf.find_by_name(".dynamic").body.last + assert_include(program_header_types, :LOAD) + assert_include(program_header_types, :DYNAMIC) + assert_include(program_header_types, :INTERP) + assert_equal(interp.body.split("\0").first, "/lib64/ld-linux-x86-64.so.2") + assert_equal(dynamic.null?, true) + assert_include(section_names, ".dynstr") + assert_include(section_names, ".dynsym") + assert_include(section_names, ".dynamic") + assert_include(section_names, ".interp") + assert_equal(:DYN, elf.header.type) + assert_equal(:AMD64, elf.header.arch) + assert_equal(0, elf.header.entry) + end + + def test_executable_movabs_pie_object + path = Pathname.new("sample/C/movabs.c").to_s + IO.popen(["gcc", "-fno-pic", "-fno-plt", "-c", "-o", @inputs.first, "%s" % path]).close + + Caotral::Linker.link!(inputs: @inputs, output: @output, linker: "self", pie: true) + elf = Caotral::Binary::ELF::Reader.read!(input: @output, debug: false) + IO.popen("./pie").close + exit_code, handle_code = check_process($?.to_i) + assert_equal(60, exit_code) + assert_equal(0, handle_code) + + section_names = elf.sections.map(&:section_name) + program_header_types = elf.program_headers.map(&:type) + interp = elf.find_by_name(".interp") + dynamic = elf.find_by_name(".dynamic").body.last + assert_include(program_header_types, :LOAD) + assert_include(program_header_types, :DYNAMIC) + assert_include(program_header_types, :INTERP) + assert_equal(interp.body.split("\0").first, "/lib64/ld-linux-x86-64.so.2") + assert_equal(dynamic.null?, true) + assert_include(section_names, ".dynstr") + assert_include(section_names, ".dynsym") + assert_include(section_names, ".dynamic") + assert_include(section_names, ".interp") + assert_equal(:DYN, elf.header.type) + assert_equal(:AMD64, elf.header.arch) + assert_not_equal(0, elf.header.entry) + end +end diff --git a/test/caotral/linker/writer_test.rb b/test/caotral/linker/writer_test.rb index 0053b78..31bb155 100644 --- a/test/caotral/linker/writer_test.rb +++ b/test/caotral/linker/writer_test.rb @@ -17,7 +17,7 @@ def test_write written_output = Caotral::Linker::Writer.write!(elf_obj: @elf_obj, output: "write.o", debug: false) read_written_elf = Caotral::Binary::ELF::Reader.read!(input: written_output, debug: false) assert_equal @elf_obj.header.shoffset, read_written_elf.header.shoffset - assert_equal 5, read_written_elf.sections.size + assert_equal 6, read_written_elf.sections.size assert_equal 0x401000, read_written_elf.header.entry end