diff --git a/profiler/lib/augure-profiler/profiler.rb b/profiler/lib/augure-profiler/profiler.rb index c08cf70..df2d140 100644 --- a/profiler/lib/augure-profiler/profiler.rb +++ b/profiler/lib/augure-profiler/profiler.rb @@ -139,7 +139,14 @@ def emit_symbols(facts) # CTF heuristic: a local win/flag function = a callable target # (the ret2func family's target_function fact). The winning # symbol keeps its name AND its address - the payload builder - # needs the address, not just the existence. + # needs the address, not just the existence. The address is only + # usable when the module is ET_EXEC (non-PIE: the address is + # absolute): an ET_DYN module's symbol address is module- + # relative, and a companion library's win is not in the binary's + # address space - the merged fact base must not carry an address + # the runtime never maps (the emporium libret2csu defines its + # own ret2win at 0x2515, module-relative). + exec = @elf.header.type == "EXEC" @elf.symbols.each do |sym| next unless sym.name.to_s.match?(/\Awin\z|win\z|flag\z/) # an IMPORT is not a win HERE: the ret2csu shape imports its @@ -147,6 +154,8 @@ def emit_symbols(facts) # and a built [qword win] payload would jump to NULL next if sym.shndx.to_s == "UNDEF" + next unless exec + facts.add("target_function", "true") facts.add("win_symbol", sym.name, sym.value.to_i) end diff --git a/profiler/spec/auto_vuln_spec.rb b/profiler/spec/auto_vuln_spec.rb index 5262972..ed37102 100644 --- a/profiler/spec/auto_vuln_spec.rb +++ b/profiler/spec/auto_vuln_spec.rb @@ -43,4 +43,18 @@ facts = AugureProfiler::Profiler.new(bin).facts.to_s expect(facts).not_to match(/vuln\("sof"\)/) end + + it "does not mistake a PLT stub for a win symbol in a companion library" do + lib = ProfilerBinaryBuilder.win_importer_lib + facts = AugureProfiler::Profiler.new(lib).facts.to_s + expect(facts).to match(/plt_addr\("ret2win"/) + expect(facts).not_to match(/win_symbol\("ret2win"/) + end + + it "does not emit a module-relative win address from a shared object" do + lib = ProfilerBinaryBuilder.win_definer_lib + facts = AugureProfiler::Profiler.new(lib).facts.to_s + expect(facts).not_to match(/win_symbol\("ret2win"/) + expect(facts).not_to match(/target_function\("true"\)/) + end end diff --git a/profiler/spec/support/binary_builder.rb b/profiler/spec/support/binary_builder.rb index 5d6b7c6..e841aaa 100644 --- a/profiler/spec/support/binary_builder.rb +++ b/profiler/spec/support/binary_builder.rb @@ -62,6 +62,7 @@ def importing_win(dir = Dir.mktmpdir("augure_win")) _o, e, s = Open3.capture3("gcc", lib_src, "-shared", "-fPIC", "-o", lib) raise "lib build failed: #{e}" unless s.success? + @last_built_lib = lib src = File.join(dir, "importer.c") # the volatile global keeps the relocation alive (the compiler # cannot fold it away); the function is never called @@ -78,6 +79,53 @@ def importing_win(dir = Dir.mktmpdir("augure_win")) bin end + # The companion-library shape: a .so that IMPORTS the win function + # (its PLT stub carries the win name at a stub address). The win + # symbol heuristic must not mistake the stub for a callable target: + # a [qword win] payload pointing at a PLT stub jumps into the + # resolver, not at the win. + def win_importer_lib(dir = Dir.mktmpdir("augure_winlib")) + src = File.join(dir, "libpwn.c") + File.write(src, <<~C) + #include + extern void ret2win(void); + __attribute__((noinline)) void pwnme(void) { + char small[8]; + read(0, small, 64); + ret2win(); + } + C + lib = File.join(dir, "libpwn.so") + # -fcf-protection: the .plt.sec layout (CFI) is where metasm + # materializes the stub as a defined FUNC symbol in .dynsym, the + # emporium companion libraries' shape + _o, e, s = Open3.capture3("gcc", src, "-shared", "-fPIC", "-o", lib, + "-fno-stack-protector", "-O0", "-fcf-protection=full") + raise "lib build failed: #{e}" unless s.success? + + lib + end + + # A shared object that DEFINES the win function: its symbol address + # is module-relative (ET_DYN), the merged fact base must not carry + # an address the runtime never maps at that value. + def win_definer_lib(dir = Dir.mktmpdir("augure_wdeflib")) + src = File.join(dir, "libwin.c") + File.write(src, <<~C) + #include + __attribute__((noinline)) void ret2win(void) { + char small[8]; + read(0, small, 16); + } + C + lib = File.join(dir, "libwin2.so") + _o, e, s = Open3.capture3("gcc", src, "-shared", "-fPIC", "-o", lib, + "-fno-stack-protector", "-O0") + raise "lib build failed: #{e}" unless s.success? + + lib + end + # A sized copy whose length is an immediate too small to reach the # return slot: bounded, not the overflow. -fno-builtin keeps memcpy a # real call (the compiler would fold it away otherwise).