diff --git a/lib/caotral/binary/elf/reader.rb b/lib/caotral/binary/elf/reader.rb index 6f716ac..ffde1b3 100644 --- a/lib/caotral/binary/elf/reader.rb +++ b/lib/caotral/binary/elf/reader.rb @@ -97,7 +97,9 @@ def read shndx = sym_bin[6, 2].unpack1("S<") value = sym_bin[8, 8].unpack1("Q<") size = sym_bin[16, 8].unpack1("Q<") - Caotral::Binary::ELF::Section::Symtab.new.set!(name:, info:, other:, shndx:, value:, size:) + name_string = @context.sections[section.header.link]&.body&.lookup(name).to_s + + Caotral::Binary::ELF::Section::Symtab.new.set!(name:, info:, other:, shndx:, value:, size:, name_string:) end when :rel, :rela rela = type == :rela diff --git a/lib/caotral/binary/elf/section/dynamic.rb b/lib/caotral/binary/elf/section/dynamic.rb index 0641e8a..8f2da23 100644 --- a/lib/caotral/binary/elf/section/dynamic.rb +++ b/lib/caotral/binary/elf/section/dynamic.rb @@ -1,4 +1,5 @@ require "caotral/binary/elf/utils" + module Caotral module Binary class ELF @@ -7,6 +8,7 @@ class Dynamic include Caotral::Binary::ELF::Utils TAG_TYPES = { NULL: 0, + NEEDED: 1, PLTRELSZ: 2, PLTGOT: 3, HASH: 4, @@ -46,6 +48,7 @@ def jmp_rel? = tag == TAG_TYPES[:JMPREL] def plt_rel? = tag == TAG_TYPES[:PLTREL] def plt_rel_size? = tag == TAG_TYPES[:PLTRELSZ] def plt_got? = tag == TAG_TYPES[:PLTGOT] + def needed? = tag == TAG_TYPES[:NEEDED] private def bytes = [@tag, @un] end diff --git a/lib/caotral/binary/elf/section/symtab.rb b/lib/caotral/binary/elf/section/symtab.rb index c6f5676..e923f30 100644 --- a/lib/caotral/binary/elf/section/symtab.rb +++ b/lib/caotral/binary/elf/section/symtab.rb @@ -19,13 +19,14 @@ def initialize(**opts) end def build = bytes.flatten.pack("C*") - def set!(name: nil, info: nil, other: nil, shndx: nil, value: nil, size: nil) + def set!(name: nil, info: nil, other: nil, shndx: nil, value: nil, size: nil, name_string: nil) @name = num2bytes(name, 4) if check(name, 4) @info = num2bytes(info, 1) if check(info, 1) @other = num2bytes(other, 1) if check(other, 1) @shndx = num2bytes(shndx, 2) if check(shndx, 2) @value = num2bytes(value, 8) if check(value, 8) @size = num2bytes(size, 8) if check(size, 8) + @name_string = name_string if name_string self end diff --git a/lib/caotral/linker.rb b/lib/caotral/linker.rb index 2663d63..c927302 100644 --- a/lib/caotral/linker.rb +++ b/lib/caotral/linker.rb @@ -5,13 +5,14 @@ module Caotral class Linker - 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 + def self.link!(inputs:, output: "a.out", linker: "mold", debug: false, shared: false, executable: true, pie: false, needed: []) + new(inputs:, output:, linker:, debug:, shared:, executable:, pie:, needed:).link end - def initialize(inputs:, output: "a.out", linker: "mold", linker_options: [], executable: true, shared: false, pie: false, debug: false) + def initialize(inputs:, output: "a.out", linker: "mold", linker_options: [], executable: true, shared: false, pie: false, debug: false, needed: []) @inputs, @output, @linker = inputs, output, linker @options = linker_options + @needed = needed @executable, @debug, @shared, @pie = executable, debug, shared, pie end @@ -49,12 +50,13 @@ 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, pie: @pie) + def to_elf(inputs: @inputs, output: @output, debug: @debug, shared: @shared, executable: @executable, pie: @pie, needed: @needed) elf_objs = inputs.map { |input| Caotral::Binary::ELF::Reader.new(input:, debug:).read } - builder = Caotral::Linker::Builder.new(elf_objs:, debug:, shared:, executable:, pie:) + builder = Caotral::Linker::Builder.new(elf_objs:, debug:, shared:, executable:, pie:, needed:) builder.resolve_symbols elf_obj = builder.build - Caotral::Linker::Writer.new(elf_obj:, output:, debug:, shared:, executable:, pie:).write + metadata = builder.linker_metadata + Caotral::Linker::Writer.new(elf_obj:, output:, metadata:, 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 f4f0a18..cb1c916 100644 --- a/lib/caotral/linker/builder.rb +++ b/lib/caotral/linker/builder.rb @@ -14,6 +14,7 @@ class Builder GENERATED_SECTION_NAMES = [".text", ".data", ".strtab", ".symtab", ".shstrtab", /\.rela?\./, ".dynstr", ".dynsym", ".dynamic", ".interp", ".rela.dyn", ".plt", ".got.plt"].freeze SHT = Caotral::Binary::ELF::SectionHeader::SHT SHF = Caotral::Binary::ELF::SectionHeader::SHF + SectionDynamic = Caotral::Binary::ELF::Section::Dynamic UNSUPPORTED_REL_TYPES = [ REL_TYPES[:AMD64_GOTPCREL], REL_TYPES[:AMD64_GOTPCRELX], @@ -25,13 +26,14 @@ class Builder DYNAMIC_TAGS[:JMPREL], ].freeze - attr_reader :symbols + attr_reader :symbols, :linker_metadata - def initialize(elf_objs:, executable: true, debug: false, shared: false, pie: false) + def initialize(elf_objs:, executable: true, debug: false, shared: false, pie: false, needed: []) @elf_objs = elf_objs - @executable, @debug, @shared, @pie = executable, debug, shared, pie - @symbols = { locals: Set.new, globals: Set.new, weaks: Set.new } + @executable, @debug, @shared, @pie, @needed = executable, debug, shared, pie, needed + @symbols = { locals: Set.new, globals: Set.new, weaks: Set.new, }.freeze _mode! + @linker_metadata = {} end def build @@ -48,6 +50,12 @@ def build section_name: ".text", header: Caotral::Binary::ELF::SectionHeader.new ) + rodata_section = Caotral::Binary::ELF::Section.new( + body: String.new, + section_name: ".rodata", + header: Caotral::Binary::ELF::SectionHeader.new + .set!(type: SHT[:progbits], flags: SHF[:ALLOC], addralign: 16) + ) data_section = Caotral::Binary::ELF::Section.new( body: String.new, section_name: ".data", @@ -55,13 +63,13 @@ def build .set!(type: SHT[:progbits], flags: SHF[:ALLOC] | SHF[:WRITE], addralign: 8) ) plt_section = Caotral::Binary::ELF::Section.new( - body: [0, 0].pack("Q= 0xff00 ? sym.value : sections[sym.shndx].then { |st| st.header.addr + sym.value } - sym_offset = entry.offset + start_len - sym_addend = entry.addend? ? entry.addend : bytes[sym_offset, 4].unpack1("l<") - value = sym_addr + sym_addend - (target_addr + sym_offset) - bytes[sym_offset, 4] = [value].pack("l<") - end - target.body = bytes + rel_texts << rel_section if rel_section.section_name.to_s.start_with?(".rela.text", ".rel.text") end sections = sections.reject { |section| RELOCATION_SECTION_NAMES.any? { |name| name === section.section_name.to_s } } if @executable sections.each { |section| elf.sections << section } + @linker_metadata[:got_plt_offsets] = got_plt_offsets + @linker_metadata[:pending_text_relocations] = rel_texts elf end @@ -444,24 +461,25 @@ def build_hash_section hash_section end - def build_dynamic_section + def build_dynamic_section(dynstr:) tag_types = Caotral::Binary::ELF::Section::Dynamic::TAG_TYPES dynamic_section = Caotral::Binary::ELF::Section.new( body: [ - Caotral::Binary::ELF::Section::Dynamic.new.set!(tag: tag_types[:PLTRELSZ]), - Caotral::Binary::ELF::Section::Dynamic.new.set!(tag: tag_types[:PLTGOT]), - Caotral::Binary::ELF::Section::Dynamic.new.set!(tag: tag_types[:HASH]), - Caotral::Binary::ELF::Section::Dynamic.new.set!(tag: tag_types[:RELA]), - Caotral::Binary::ELF::Section::Dynamic.new.set!(tag: tag_types[:RELASZ]), - Caotral::Binary::ELF::Section::Dynamic.new.set!(tag: tag_types[:RELAENT], un: 24), - Caotral::Binary::ELF::Section::Dynamic.new.set!(tag: tag_types[:STRTAB]), - Caotral::Binary::ELF::Section::Dynamic.new.set!(tag: tag_types[:STRSZ]), - Caotral::Binary::ELF::Section::Dynamic.new.set!(tag: tag_types[:SYMTAB]), - Caotral::Binary::ELF::Section::Dynamic.new.set!(tag: tag_types[:SYMENT], un: 24), - Caotral::Binary::ELF::Section::Dynamic.new.set!(tag: tag_types[:PLTREL]), - Caotral::Binary::ELF::Section::Dynamic.new.set!(tag: tag_types[:TEXTREL]), - Caotral::Binary::ELF::Section::Dynamic.new.set!(tag: tag_types[:JMPREL]), - Caotral::Binary::ELF::Section::Dynamic.new + *@needed.map { |lib| SectionDynamic.new.set!(tag: tag_types[:NEEDED], un: dynstr.body.offset_of(lib)) }, + SectionDynamic.new.set!(tag: tag_types[:PLTRELSZ]), + SectionDynamic.new.set!(tag: tag_types[:PLTGOT]), + SectionDynamic.new.set!(tag: tag_types[:HASH]), + SectionDynamic.new.set!(tag: tag_types[:RELA]), + SectionDynamic.new.set!(tag: tag_types[:RELASZ]), + SectionDynamic.new.set!(tag: tag_types[:RELAENT], un: 24), + SectionDynamic.new.set!(tag: tag_types[:STRTAB]), + SectionDynamic.new.set!(tag: tag_types[:STRSZ]), + SectionDynamic.new.set!(tag: tag_types[:SYMTAB]), + SectionDynamic.new.set!(tag: tag_types[:SYMENT], un: 24), + SectionDynamic.new.set!(tag: tag_types[:PLTREL]), + SectionDynamic.new.set!(tag: tag_types[:TEXTREL]), + SectionDynamic.new.set!(tag: tag_types[:JMPREL]), + SectionDynamic.new ].compact, section_name: ".dynamic", header: Caotral::Binary::ELF::SectionHeader.new diff --git a/lib/caotral/linker/writer.rb b/lib/caotral/linker/writer.rb index cb3e886..c37dc07 100644 --- a/lib/caotral/linker/writer.rb +++ b/lib/caotral/linker/writer.rb @@ -7,14 +7,19 @@ class Writer REL_TYPES = Caotral::Binary::ELF::Section::Rel::TYPES.freeze ALLOW_RELOCATION_TYPES = [REL_TYPES[:AMD64_PC32], REL_TYPES[:AMD64_PLT32]].freeze RELOCATION_SECTION_NAMES = [".rela.text", ".rela.dyn", ".rela.data", ".rela.plt"].freeze - attr_reader :elf_obj, :output, :entry, :debug - def self.write!(elf_obj:, output:, entry: nil, debug: false, executable: true, shared: false) - new(elf_obj:, output:, entry:, debug:, shared:, executable:).write + attr_reader :elf_obj, :output, :entry, :debug, :got_plt_offsets, :pending_text_relocations + def self.write!(elf_obj:, output:, metadata: nil, entry: nil, debug: false, executable: true, shared: false) + new(elf_obj:, output:, metadata:, entry:, debug:, shared:, executable:).write end - def initialize(elf_obj:, output:, entry: nil, debug: false, executable: true, shared: false, pie: false) + + def initialize(elf_obj:, output:, metadata: nil, entry: nil, debug: false, executable: true, shared: false, pie: false) @elf_obj, @output, @entry, @debug, @executable, @shared, @pie = elf_obj, output, entry, debug, executable, shared, pie @program_headers = [] @write_sections = elf_obj.sections + @got_plt_offsets = {} + @got_plt_offsets = metadata.fetch(:got_plt_offsets, {}) if metadata + @pending_text_relocations = [] + @pending_text_relocations = metadata.fetch(:pending_text_relocations, []) if metadata end def write @@ -44,18 +49,16 @@ def write lph.set!(type:, offset: text_offset, vaddr:, paddr:, filesz:, memsz:, flags:, align:) f.write(@elf_obj.header.build) phs.each { |ph| f.write(ph.build) } - if pph - pph.set!( - type: 6, - offset: phoffset, - vaddr: phoffset, - paddr: phoffset, - filesz: phsize * phnum, - memsz: phsize * phnum, - flags: program_header_flags(:R), - align: 8 - ) - end + pph.set!( + type: 6, + offset: phoffset, + vaddr: phoffset, + paddr: phoffset, + filesz: phsize * phnum, + memsz: phsize * phnum, + flags: program_header_flags(:R), + align: 8 + ) if pph gap = [text_offset - f.pos, 0].max f.write("\0" * gap) @@ -82,6 +85,8 @@ def write rel.header.set!(offset: rel_offset, size: rel_size, entsize:) end + rewrite_text_section(file: f) unless rel_text_sections.empty? + patch_dynamic_sections(file: f) if dynamic? patch_program_headers(file: f) write_program_headers(file: f) @@ -102,6 +107,39 @@ def write end private + def rewrite_text_section(file:) + cur = file.pos + grouped_rels = rel_text_sections.group_by { |rel| rel.header.info } + grouped_rels.each do |target_index, rels| + target = @elf_obj.sections[target_index] + bytes = target.body.dup + symtab_body = symtab_section.body + vaddr = target.header.addr + file.seek(target.header.offset) + rels.each do |rel| + rel.body.each do |entry| + next unless ALLOW_RELOCATION_TYPES.include?(entry.type) + sym = symtab_body[entry.sym] + next if sym.nil? + target_addr = target == text_section ? vaddr : target.header.addr + sym_offset = entry.offset + sym_addend = entry.addend? ? entry.addend : bytes[sym_offset, 4].unpack1("l<") + sym_addr = if sym.shndx == 0 + plt_section.header.addr + 16 * (((got_plt_offsets[entry.sym] - 24) / 8) + 1) + elsif sym.shndx >= 0xff00 + sym.value + else + @elf_obj.sections[sym.shndx].header.addr + sym.value + end + value = sym_addr + sym_addend - (target_addr + sym_offset) + bytes[sym_offset, 4] = [value].pack("l<") + end + end + file.write(bytes) + end + file.seek(cur) + end + def patch_dynamic_sections(file:) dynamic_sections.each do |dyn| addr = text_section.header.addr + (dyn.header.offset - text_section.header.offset) @@ -126,6 +164,7 @@ def patch_dynamic_sections(file:) if dynamic? && dynamic_section && rela_dyn_section rdsh = rela_dyn_section&.header bodies = dynamic_section.body + bodies.delete_if { |dyn| dyn.tag == dynamic_tables[:TEXTREL] } unless rela_dyn_section.body.any? { |rel| rel.type == REL_TYPES[:AMD64_RELATIVE] } bodies.find { |dyn| dyn.tag == dynamic_tables[:RELA] }.set!(un: rdsh&.addr.to_i) bodies.find { |dyn| dyn.tag == dynamic_tables[:RELASZ] }.set!(un: rdsh&.size.to_i) bodies.find { |dyn| dyn.tag == dynamic_tables[:STRSZ] }&.set!(un: dynstr_section.header.size.to_i) @@ -173,7 +212,7 @@ def patch_program_headers(file:) segment_start = text_section.header.offset segment_start = 0 if @pie - segment_end = [text_section, data_section,].concat(dynamic_sections).compact.map { |s| s.header.offset + s.header.size }.max + segment_end = [text_section, rodata_section, data_section,].concat(dynamic_sections).compact.map { |s| s.header.offset + s.header.size }.max dynamic_filesz = segment_end - segment_start load_program_header.set!(filesz: dynamic_filesz, memsz: dynamic_filesz) @@ -200,6 +239,16 @@ def write_elf_sections(file:) addr: text_section.header.addr ) + if rodata_section + ordata_offset = file.pos + file.write(rodata_section.build) + rodata_section.header.set!( + offset: ordata_offset, + size: rodata_section.body.bytesize, + addr: text_section.header.addr + (ordata_offset - text_section.header.offset) + ) + end + if data_section data_offset = file.pos file.write(data_section.build) @@ -212,19 +261,22 @@ def write_elf_sections(file:) if plt_section plt_offset = file.pos - file.write(plt_section.build) + file.write(plt_section.body.flatten.pack("C*")) + size = file.pos - plt_offset plt_section.header.set!( offset: plt_offset, - size: plt_section.body.bytesize, + size:, addr: text_section.header.addr + (plt_offset - text_section.header.offset) ) - end - if got_plt_section + + raise Caotral::Binary::ELF::Error, "missing .got.plt for .plt" if got_plt_section.nil? + got_plt_offset = file.pos - file.write(got_plt_section.build) + file.write(got_plt_section.body.flatten.pack("C*")) + size = file.pos - got_plt_offset got_plt_section.header.set!( offset: got_plt_offset, - size: got_plt_section.body.bytesize, + size:, addr: text_section.header.addr + (got_plt_offset - text_section.header.offset) ) end @@ -278,6 +330,39 @@ def write_shared_dynamic_sections(file:) dynamic_section.body.each { |dynamic| file.write(dynamic.build) } size = file.pos - dynamic_offset dynamic_section.header.set!(offset: dynamic_offset, size:, addr: text_addr + (dynamic_offset - tsh.offset)) + + if plt_section + current_offset = file.pos + primary, *rest = plt_section.body + plt_offset = plt_section.header.offset + got_plt_offset = got_plt_section.header.offset + file.seek(plt_offset) + plt_addr = plt_section.header.addr + got_plt_addr = got_plt_section.header.addr + # only support x86-64 binaries with PLT + primary[2..5] = [(got_plt_addr + 8) - (plt_addr + 6)].pack("l<").bytes + primary[8..11] = [(got_plt_addr + 16) - (plt_addr + 12)].pack("l<").bytes + slot_offset = 24 + rest.each_with_index do |entry, i| + entry_addr = plt_addr + 16 + 16 * i + slot_addr = got_plt_addr + slot_offset + 8 * i + entry[2..5] = [slot_addr - (entry_addr + 6)].pack("l<").bytes + entry[7..10] = [i].pack("l<").bytes + entry[12..15] = [plt_addr - (entry_addr + 16)].pack("l<").bytes + end + file.write(primary.flatten.pack("C*")) + file.write(rest.flatten.pack("C*")) + + file.seek(got_plt_offset) + primary, secondary, third, *rest = got_plt_section.body + primary = [dynamic_section.header.addr].pack("Q<").bytes + rest.each_with_index { |_entry, i| rest[i] = [plt_addr + 22 + 16 * i].pack("Q<").bytes } + file.write(primary.flatten.pack("C*")) + file.write(secondary.flatten.pack("C*")) + file.write(third.flatten.pack("C*")) + file.write(rest.flatten.pack("C*")) + file.seek(current_offset) + end end def ref_index(section_name) @@ -382,6 +467,7 @@ def dynamic_program_header = @dynamic_program_header ||= program_headers.find { def text_section = @text_section ||= @write_sections.find { |s| ".text" === s.section_name.to_s } def rel_sections = @rel_sections ||= @write_sections.select { |s| RELOCATION_SECTION_NAMES.include?(s.section_name.to_s) } + def rel_text_sections = @rel_text_sections ||= @pending_text_relocations def symtab_section = @symtab_section ||= @write_sections.find { |s| ".symtab" === s.section_name.to_s } def strtab_section = @strtab_section ||= @write_sections.find { |s| ".strtab" === s.section_name.to_s } def shstrtab_section = @shstrtab_section ||= @write_sections.find { |s| ".shstrtab" === s.section_name.to_s } @@ -391,6 +477,7 @@ def dynamic_section = @dynamic_section ||= @write_sections.find { |s| ".dynamic" def interp_section = @interp_section ||= @write_sections.find { |s| ".interp" === s.section_name.to_s } def rela_dyn_section = @rela_dyn_section ||= @write_sections.find { |s| ".rela.dyn" === s.section_name.to_s } def data_section = @data_section ||= @write_sections.find { |s| ".data" === s.section_name.to_s } + def rodata_section = @rodata_section ||= @write_sections.find { |s| ".rodata" === s.section_name.to_s } def hash_section = @hash_section ||= @write_sections.find { |s| ".hash" === s.section_name.to_s } def plt_section = @plt_section ||= @write_sections.find { |s| ".plt" === s.section_name.to_s } def got_plt_section = @got_plt_section ||= @write_sections.find { |s| ".got.plt" === s.section_name.to_s } diff --git a/sample/C/needed.c b/sample/C/needed.c new file mode 100644 index 0000000..4509b17 --- /dev/null +++ b/sample/C/needed.c @@ -0,0 +1,6 @@ +extern int puts(const char *); + +int main(void) { + puts("hello, glibc"); + return 0; +} diff --git a/sample/C/start.c b/sample/C/start.c new file mode 100644 index 0000000..2b2fdba --- /dev/null +++ b/sample/C/start.c @@ -0,0 +1,10 @@ +typedef unsigned long size_t; + +extern long write(int fd, const void *buf, size_t count); +extern void _exit(int status); + +void _start(void) { + static const char msg [] = "hello, world!!!\n"; + write(1, msg, sizeof(msg) - 1); + _exit(0); +} diff --git a/test/caotral/linker/needed_test.rb b/test/caotral/linker/needed_test.rb new file mode 100644 index 0000000..fe0ac94 --- /dev/null +++ b/test/caotral/linker/needed_test.rb @@ -0,0 +1,41 @@ +require_relative "../../test_suite" + +class LinkerNeededTest < Test::Unit::TestCase + include TestProcessHelper + + def setup + @input = "needed.o" + @output = "needed" + end + + def teardown + File.delete(@input) if File.exist?(@input) + File.delete(@output) if File.exist?(@output) + end + + def test_needed_in_dynamic_section + compile_path = Pathname.new("sample/C/needed.c").to_s + IO.popen(["gcc", "-fPIE", "-c", "-o", @input, "%s" % compile_path]).close + + Caotral::Linker.link!(inputs: [@input], output: @output, linker: "self", pie: true, needed: ["libc.so.6"]) + + elf = Caotral::Binary::ELF::Reader.read!(input: @output, debug: false) + dynstr = elf.find_by_name(".dynstr") + dynsym = elf.find_by_name(".dynsym") + needed = elf.find_by_name(".dynamic").body.select(&:needed?) + assert_equal(1, needed.size) + assert_equal(dynstr.body.lookup(needed.first.un), "libc.so.6") + assert_include(dynsym.body.map(&:name_string), "puts") + end + + def test_needed_in_executable + compile_path = Pathname.new("sample/C/needed.c").to_s + IO.popen(["gcc", "-fPIE", "-c", "-o", @input, "%s" % compile_path]).close + + Caotral::Linker.link!(inputs: [@input], output: @output, linker: "self", pie: true, needed: ["libc.so.6"], executable: true) + + IO.popen(["./#{@output}"]).close + pid, exit_code = check_process($?.to_i) + assert_equal(0, exit_code) + end +end diff --git a/test/caotral/linker/writer_test.rb b/test/caotral/linker/writer_test.rb index 31bb155..ac05b91 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 6, read_written_elf.sections.size + assert_equal 7, read_written_elf.sections.size assert_equal 0x401000, read_written_elf.header.entry end