Hi Everybody!
This is going to be a challenge-author writeup for the Secure Asset Manager challenge from BSides San Francisco 2021.
It’s designed to be a sort of “server management software”. I sort of chose that theme to play off the Solarwinds thing, the theme wasn’t super linked to the challenge.
The challenge was to analyze and reverse engineer a piece of client-side software that “checks in” with a server. For the check-in, the client is required to “validate” itself. The server sends a random “challenge” - which is actually a block of randomized x86 code - and that code used to checksum active memory to prevent tampering. If anybody reading this worked on bots for the original Starcraft (and other Battle.net games), this might seem familiar! It’s based on Battle.net’s CheckRevision code.
Server
The players don’t normally get to see it, but this is my server code. I’d like to draw your attention to assembly-generator.rb in particular, which is what creates the challenge. It just does a whole bunch of random, and really bad checksumming with a few instructions and also randomized NOPs:
0.upto(rand(1..5)) do 0.upto(rand(2..5)) do # Do something to the value a few times s.push([ "xor eax, #{ random_int }", "add eax, #{ random_int }", "sub eax, #{ random_int }", "ror eax, #{ rand(1..30) }", "rol eax, #{ rand(1..30) }", ].sample) end # Mix in the previous value (or seed) s.push("xor ecx, eax") s.push('') s.push(nop()) end
The server dumps all those random instructions into a file, assembles it with nasm, and sends over the resulting code.
To generate a checksum on the server side, I actually used what I’d consider a solution: dumping client memory.
First solution: dump memory
To validate the client, the server wraps gdb (the GNU Debugger) and sends commands to dump process memory. Here’s the code:
def dump_binary(binary, target) L.info("Dumping memory from #{ binary } using gdb...") begin Timeout.timeout(3) do Open3.popen2("gdb -q #{ binary }") do |i, o, t| # Don't confirm things i.puts("set no-confirm") # Breakpoint @ malloc - we just need to stop anywhere i.puts("break malloc") # Run the executable i.puts("run") # Remove the breakpoint - this is VERY important, the breakpoint will mess # up the memory dump! i.puts("delete") # Get the pid i.puts("print (int) getpid()") loop do out = o.gets().strip() puts(out) if out =~ /\$1 = ([0-9]+)/ L.info("Found PID: #{ $1 }") L.info("Reading /proc/#{ $1 }/maps to find memory block") mappings = File.read("/proc/#{ $1 }/maps").split(/\n/) mappings.each do |m| if m =~ /([0-9a-f]+)-([0-9a-f]+) (r-xp).*\/secure-asset-manager$/ L.debug("Found memory block: #{ m }") i.puts("dump memory #{ target } 0x#{ $1 } 0x#{ $2 }") i.puts("quit") loop do out = o.gets() if !out break end puts(out.strip()) end L.info("Memory from original binary dumped to #{ target }") return end end end end end end rescue Timeout::Error L.fatal("Something went wrong dumping the binary! Check the gdb output above") exit(1) end end
Then I used a secondary script, which I called not-solution.c, to actually execute the code. But instead of performing the checksum on memory, it performs it on the dumped binary. It even uses the same checksum function from the real client:
uint32_t checksum(data_block_t *code, data_block_t *binary) { // Allocate +rwx memory uint32_t (*rwx)(uint8_t*, uint8_t*) = mmap(0, code->length, PROT_EXEC | PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0); // Populate it with the code memcpy(rwx, code->data, code->length); // Run the code uint32_t result = rwx(binary->data, binary->data + binary->length); // Wipe and unmap the memory memset(rwx, 0, code->length); munmap(rwx, code->length); return result; }
That server-side code is literally what my solution does, and I even use the same not-solution.c script to do it.
In retrospect, I need to stop using gdb in containers. It only causes headaches for our infrastructure guy, David. :)
Alternative solution: proxy
The first time I ever made a Starcraft bot, I used the real game client to connect, redirected it through a proxy, and once the connection was established, I’d disconnect the game and keep the bot going. It was crazy inefficient and a big pain to reconnect, but it worked and I was super proud of it!
I’m happy that at least one team solved it this way.
I actually don’t have anything written that implements this, but I’d love to see a writeup where somebody did! I have no idea if there’s any tooling out there that can make this easy.
Accidental solution: edit the binary
So it turns out, I only checksummed the code portion. You could freely change the data section of the binary (say, change the check-in command to the flag command) and everything Just Works. D’oh!
Conclusion
For years I’ve wanted to do a challenge like this. I’d actually like to repackage this and use a similar concept again, only a bit harder and with less opportunity to bypass it. Stay tuned for 2022!
Comments
Join the conversation on this Mastodon post (replies will appear below)!
Loading comments...