This is going to be a writeup for the Reverseme challenges (reverseme and reverseme2 from BSides San Francisco 2021.
Both parts are reasonably simple reverse engineering challenges. I provide the compiled binaries to the player (you can find those in the respective distfiles/ folders), and you have to figure out what to do with them.
Both challenges use the same basic code as the runme challenges, where you send shellcode that is executed. Only in this case, the shellcode must be modified or “encoded” in some way first!
Reverseme
Since this can be solved with basic tools, I’m just going to use objdump disassemble the Reverseme binary. You can much more effectively use IDA or Ghidra, but to use those I might have to take screenshots, deal with file uploads, etc. :)
Here’s the output from objdump, focused on the important part (which I found by searching for main):
$ objdump -D -M intel ./reverseme/distfiles/reverseme [...] ; Read the code from stdin (should be identical to Runme) 1220: e8 3b fe ff ff call 1060 <read@plt> 1225: 48 89 45 e8 mov QWORD PTR [rbp-0x18],rax ; Perform error checking 1229: 48 83 7d e8 00 cmp QWORD PTR [rbp-0x18],0x0 ; Jump if no error 122e: 79 16 jns 1246 <main+0xc1> ; <snip out error handling code> ; A for loop starts here, that loops over the full buffer. This is a small ; optimization - it jumps to the bottom where the for loop's exit condition ; is checked 124d: eb 28 jmp 1277 <main+0xf2> ; This loop is a super unoptimized way of doing: ; xor buffer[i], 0x41 ; inc i 124f: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 1252: 48 63 d0 movsxd rdx,eax 1255: 48 8b 45 f0 mov rax,QWORD PTR [rbp-0x10] 1259: 48 01 d0 add rax,rdx 125c: 0f b6 08 movzx ecx,BYTE PTR [rax] 125f: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 1262: 48 63 d0 movsxd rdx,eax 1265: 48 8b 45 f0 mov rax,QWORD PTR [rbp-0x10] 1269: 48 01 d0 add rax,rdx 126c: 83 f1 41 xor ecx,0x41 126f: 89 ca mov edx,ecx 1271: 88 10 mov BYTE PTR [rax],dl 1273: 83 45 fc 01 add DWORD PTR [rbp-0x4],0x1 ; Eax = the next loop iterator 1277: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 127a: 48 98 cdqe ; Are we at the end of the loop? 127c: 48 39 45 e8 cmp QWORD PTR [rbp-0x18],rax ; Jump to the top until the loop is done 1280: 7f cd jg 124f <main+0xca> ; Get the buffer and jump to it (same as Runme) 1282: 48 8b 45 f0 mov rax,QWORD PTR [rbp-0x10] 1286: ff d0 call rax [...]
When I was learning to reverse engineer, I got a ton of mileage out of compiling C code and looking at the resulting assembly to see what happens to loops and variables and stuff. So it might be illustrative to look at the source (which players wouldn’t have had during the game):
len = read(0, buffer, LENGTH); if(len < 0) { printf("Error reading!\n"); exit(1); } int i; for(i = 0; i < len; i++) { buffer[i] ^= 0x41; } asm("call *%0\n" : :"r"(buffer));
So basically, all that’s doing is XORing each byte by 0x41. Let’s write a quick and inefficient encoder in Ruby:
loop do b = STDIN.read(1) if(b.nil?) exit(0) end print (b.ord ^ 0x41).chr end
And use it to encode the shellcode from Runme:
$ ruby ./reverseme-encoder.rb < ./solution.bin | ./reverseme Send me x64!! CTF{fake_flag}
That’s it for part 1!
Reverseme2
Reverseme2 is very similar. Again, let’s just use objdump:
$ objdump -D -M intel ./reverseme2/distfiles/reverseme2 [...] ; Call srand(0x13371337) 1223: bf 37 13 37 13 mov edi,0x13371337 1228: e8 43 fe ff ff call 1070 <srand@plt> [...] ; Jump to the bottom of the loop (like last time) 1277: eb 38 jmp 12b1 <main+0x10c> ; Top of the loop: ; Call rand() 1279: e8 22 fe ff ff call 10a0 <rand@plt> ; Shift the return value from rand() right by three: 127e: c1 f8 03 sar eax,0x3 ; Take the right-most byte of the new value ... 1281: 0f b6 c8 movzx ecx,al 1284: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 1287: 48 63 d0 movsxd rdx,eax 128a: 48 8b 45 f0 mov rax,QWORD PTR [rbp-0x10] 128e: 48 01 d0 add rax,rdx 1291: 0f b6 00 movzx eax,BYTE PTR [rax] 1294: 89 c2 mov edx,eax 1296: 89 c8 mov eax,ecx 1298: 89 d1 mov ecx,edx ; ... and xor it with the current byte 129a: 31 c1 xor ecx,eax 129c: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 129f: 48 63 d0 movsxd rdx,eax 12a2: 48 8b 45 f0 mov rax,QWORD PTR [rbp-0x10] 12a6: 48 01 d0 add rax,rdx 12a9: 89 ca mov edx,ecx 12ab: 88 10 mov BYTE PTR [rax],dl 12ad: 83 45 fc 01 add DWORD PTR [rbp-0x4],0x1 ; Are we at the end of the loop? 12b1: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 12b4: 48 98 cdqe 12b6: 48 39 45 e8 cmp QWORD PTR [rbp-0x18],rax 12ba: 7f bd jg 1279 <main+0xd4> 12bc: 48 8b 45 f0 mov rax,QWORD PTR [rbp-0x10] ; If so, call into the code 12c0: ff d0 call rax [...]
And once again, compare it to source:
srand(0x13371337); //[...] len = read(0, buffer, LENGTH); //[...] int i; for(i = 0; i < len; i++) { buffer[i] ^= (rand() >> 3) & 0x0FF; } asm("call *%0\n" : :"r"(buffer));
We can make an encoder in C using that exact code:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char *argv[]) { int i; srand(0x13371337); unsigned char buffer[4096]; ssize_t len = read(0, buffer, 4096); if(len < 0) { printf("Error reading!\n"); exit(1); } for(i = 0; i < len; i++) { buffer[i] ^= (rand() >> 3) & 0x0FF; printf("%c", buffer[i]); } return 0; }
And execute it, once again with the same shellcode from runme:
$ ./reverseme2-encoder < ./solution.bin | ./reverseme2 Send me (encoded) x64!! CTF{fake_flag}
Conclusion
I know that was pretty brief, I didn’t want to dig TOO much more into a reversing challenge. I’m happy to answer any questions, though!
Comments
Join the conversation on this Mastodon post (replies will appear below)!
Loading comments...