Immunity Debugger设置JIT

Windows 7以后,Immunity Debugger毕竟不再更新了,很多人开始转用x64dbg了。但用习惯了,除非是x64代码,不然还是不想换呢。Immunity Debugger一直有个不大不小的问题,就是当其他应用crash时,它的即时调试器模式总是启动不起来。

如果直接查看注册表,会发现是程序当前路径获取失败;只要人为添加如下信息到注册表即可让Immunity Debugger成为JIT:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug]
"UserDebuggerHotKey"=dword:00000000
"Auto"="0"
"Debugger"="\"D:\\Program Files (x86)\\Immunity Inc\\Immunity Debugger\\ImmunityDebugger.exe\" -AEDEBUG %ld %ld"

FlashPlayer针对dll劫持的缓解措施

不知道Adobe究竟是受什么启发,在FlashPlayer的23版本开始引入了针对dll劫持的缓解措施。

FlashPlayer 22的启动参数处理流程示意如下:

 v63 = hInstance;
  v4 = GetModuleHandleW(L"kernel32.dll");
  v5 = GetProcAddress(v4, "SetDllDirectoryA");
  v6 = 0;
  if ( v5 )
    ((void (__stdcall *)(char *))v5)(byte_D44471);
  v7 = GetModuleHandleW(L"kernel32.dll");
  v8 = GetProcAddress(v7, "SetDefaultDllDirectories");
  if ( v8 )
    ((void (__stdcall *)(signed int))v8)(2048);
  sub_41CC51();
  // <- new codes are inserted here
  v9 = GetCommandLineA();
  v10 = v9;
  v11 = *v9;
  if ( v11 != '"' )
  {
    if ( (unsigned __int8)v11 > 0x20u )
    {
      do
        ++v10;
      while ( *v10 > 0x20u );
    }
    goto LABEL_15;
  }
  do
    ++v10;
  while ( *v10 != 34 && *v10 );
  if ( *v10 != '"' )
    goto LABEL_15;

而23版本在GetCommandLineA前插入了新的缓解代码,如下所示:

  sub_41D360();
  GetModuleFileNameW(0, &Filename, 0x104u);
  wcscpy_s(&Dst, 0x104u, &Filename);
  v9 = wcsrchr(&Dst, 0x5Cu);
  if ( v9 )
    *v9 = 0;
  v10 = wcsrchr(&Filename, 0x5Cu);
  wcscpy_s(&v82, 0x104u, v10 + 1);
  v11 = GetCommandLineW();
  v66 = CommandLineToArgvW(v11, &pNumArgs);
  memset(&v86, 0, 0x208u);
  for ( i = 1; i < pNumArgs; ++i )
  {
    if ( wcsstr(v66[i], L"-relaunched") )
      v68 = 1;
  }
  if ( sub_41D705(&Dst) )                       // check dll
  {
    uExitCode = 0;
    if ( v68 )
    {
      MessageBoxW(0, L"Dll's are not allowed next to the Standalone Player", L"Error", 0);
    }
    else if ( sub_41CD97(&NewFileName, (int)&v82) )// create temp directory
    {
      v13 = 1;
      if ( CopyFileW(&Filename, &NewFileName, 1) )
      {
        if ( pNumArgs > 1 )
        {
          do
          {
            wcscat_s(&v86, 0x104u, v66[v13]);
            if ( v13 < pNumArgs )
              wcscat_s(&v86, 0x104u, L" ");
            ++v13;
          }
          while ( v13 < pNumArgs );
        }
        wcscat_s(&v86, 0x104u, L"-relaunched");
        if ( !sub_41D779(&NewFileName, (int)&v86, 0) )
          uExitCode = 1;
        wcscpy_s(&PathName, 0x104u, &NewFileName);
        v14 = wcsrchr(&PathName, 0x5Cu);
        if ( v14 )
          *v14 = 0;
        DeleteFileW(&NewFileName);
        RemoveDirectoryW(&PathName);
      }
      else
      {
        uExitCode = 1;
      }
    }
    else
    {
      uExitCode = 1;
    }
    ExitProcess(uExitCode);
  }
  v15 = GetCommandLineA();

插入的代码功能:FlashPlayer在运行的时候,会检测当前目录是否包含*.dll文件,如果包含,就拷贝自身到temp目录,然后以-relaunched参数启动。

如果以-relaunched启动后的FlashPlayer检测到目录仍然包含*.dll就会弹出错误对话框,然后终止运行。

所以包含dll时,查看进程管理器,看到的FlashPlayer都是这样的形式:

"C:\Users\admin\AppData\Local\Temp\{F0CF3F41-B0CC-44A3-B59F-EA1D57B9DF7C}\FlashPlayer.exe" -relaunched

osx下编译android内核

如果是在linux系统下编译android的内核,基本不会有什么大的问题,但osx就稍微顽皮一些。

以nexus 5的内核编译为例,首先下载编译内核用的arm-eabi-gcc工具:

git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7/

然后下载内核源代码

git clone https://android.googlesource.com/kernel/msm.git

之后切换代码到需要的branch

$ cd msm/
$ git branch -a

  * master
    remotes/origin/HEAD -> origin/master
    remotes/origin/android-msm-2.6.35
    remotes/origin/android-msm-3.9-usb-and-mmc-hacks
    remotes/origin/android-msm-flo-3.4-jb-mr2
    remotes/origin/android-msm-flo-3.4-kitkat-mr0
    remotes/origin/android-msm-flo-3.4-kitkat-mr1
    remotes/origin/android-msm-hammerhead-3.4-kitkat-mr1
...
$ git checkout origin/android-msm-hammerhead-3.4-kitkat-mr1

以上都是常规步骤,针对osx还有一些必须的改动:


增加两个头文件elf.h和features.h到内核源码的scripts/mod下面,头文件下载

修改scripts/mod/mk_elfconfig.c和scripts/mod/modpost.h两个文件,将<elf.h>改成 “elf.h”

将scripts/recordmcount.c中的<elf.h>修改为 “mod/elf.h”

修改kernel/timeconst.pl,将defined(@array)的修改为@array


最后再编译即可:

export ARCH=arm
export CC=arm-eabi-
export CROSS_COMPILE=arm-eabi-
make hammerhead_defconfig
make menuconfig
make -k

逆向ARM64内核zImage

主流的旗舰Android手机已经尽数升级到64位,相应的,内核镜像zImage也发生了改变。如果想要用IDA Pro逆向分析arm64的手机内核,特别是完成内核符号的加载,着实需要折腾一番功夫。

从/dev/block或ROM包中提取boot.img,然后用abootimg -x解开得到zImage

如果zImage是gzip压缩的,就gzip -d解压得到kernel

以上两部都是常规项目,下面重点是要从kernel中提取本应显示在/proc/kallsyms下的内核符号,这样IDA Pro加载分析时才更得心应手。参考Bits, Please!的文章中32位的kernel符号提取方法,可以很快想到64位的解决方案:

首先要知道内核加载时的虚拟地址,一种投机的方法是,手机开机后执行:

shell@surabaya:/ $ dmesg
...
[    0.000000] Virtual kernel memory layout:
[    0.000000]     vmalloc : 0xffffff8000000000 - 0xffffffbdbfff0000   (   246 GB)
[    0.000000]     vmemmap : 0xffffffbdc0000000 - 0xffffffbfc0000000   (     8 GB maximum)
[    0.000000]     PCI I/O : 0xffffffbffa000000 - 0xffffffbffb000000   (    16 MB)
[    0.000000]     fixed   : 0xffffffbffbdfe000 - 0xffffffbffbdff000   (     4 KB)
[    0.000000]     modules : 0xffffffbffc000000 - 0xffffffc000000000   (    64 MB)
[    0.000000]     memory  : 0xffffffc000000000 - 0xffffffc0fe550000   (  4069 MB)
[    0.000000]       .init : 0xffffffc001600000 - 0xffffffc001813000   (  2124 KB)
[    0.000000]       .text : 0xffffffc000080000 - 0xffffffc001600000   ( 22016 KB)
[    0.000000]       .data : 0xffffffc00181d000 - 0xffffffc001995f80   (  1508 KB)
...

由于现在手机还没有开启KASLR,所以基地址基本上总是0xffffffc000080000,有了这个地址就可以从kernel中找到symbol table了。内核导出的前两个符号stext,_text等总是指向0xffffffc000080000,所以搜索连续的两个0xffffffc000080000就能找到symbol table。之后按照Bits, Please!的方法就可以导出所有符号了,唯一要注意的是32位到64位,地址长度变成了8字节,内存对齐也从0x10变成了0x100。修改原来的Python脚本,开发了一个arm64解析符号的脚本:

import sys
import struct

#The default address at which the kernel text segment is loaded
DEFAULT_KERNEL_TEXT_START = 0xffffffc000080000

#The size of the QWORD in a 64-bit architecture
QWORD_SIZE = struct.calcsize("Q")

#The size of the DWORD in a 32-bit architecture
DWORD_SIZE = struct.calcsize("I")

#The size of the WORD in a 32-bit architecture
WORD_SIZE = struct.calcsize("H")

#The alignment of labels in the resulting kernel file
LABEL_ALIGN = 0x100

#The minimal number of repeating addresses pointing to the kernel's text start address
#which are used as a heuristic in order to find the beginning of the kernel's symbol
#table. Since usually there are at least two symbols pointing to the beginning of the
#text segment ("stext", "_text"), the minimal number for the heuristic is 2.
KALLSYMS_ADDRESSES_MIN_HEURISTIC = 2

def read_qword(kernel_data, offset):
	'''
	Reads a DWORD from the given offset within the kernel data
	'''
	return struct.unpack("<Q", kernel_data[offset : offset + QWORD_SIZE])[0]

def read_dword(kernel_data, offset):
	'''
	Reads a DWORD from the given offset within the kernel data
	'''
	return struct.unpack("<I", kernel_data[offset : offset + DWORD_SIZE])[0]

def read_word(kernel_data, offset):
	'''
	Reads a WORD from the given offset within the kernel data
	'''
	return struct.unpack("<H", kernel_data[offset : offset + WORD_SIZE])[0]

def read_byte(kernel_data, offset):
	'''
	Reads an unsigned byte from the given offset within the kernel data
	'''
	return struct.unpack("<B", kernel_data[offset : offset + 1])[0]

def read_c_string(kernel_data, offset):
	'''
	Reads a NUL-delimited C-string from the given offset
	'''
	current_offset = offset
	result_str = ""
	while kernel_data[current_offset] != '\x00':
		result_str += kernel_data[current_offset]
		current_offset += 1
	return result_str

def label_align(address):
	'''
	Aligns the given value to the closest label output boundry
	'''
	return address & ~(LABEL_ALIGN-1)

def find_kallsyms_addresses(kernel_data, kernel_text_start):
	'''
	Searching for the beginning of the kernel's symbol table
	Returns the offset of the kernel's symbol table, or -1 if the symbol table could not be found
	'''
	search_str = struct.pack("<Q", DEFAULT_KERNEL_TEXT_START) * KALLSYMS_ADDRESSES_MIN_HEURISTIC
	return kernel_data.find(search_str)

def get_kernel_symbol_table(kernel_data, kernel_text_start):	
	'''
	Retrieves the kernel's symbol table from the given kernel file
	'''

	#Getting the beginning and end of the kallsyms_addresses table
	kallsyms_addresses_off = find_kallsyms_addresses(kernel_data, kernel_text_start)
	kallsyms_addresses_end_off = kernel_data.find(struct.pack("<Q", 0), kallsyms_addresses_off)
	num_symbols = (kallsyms_addresses_end_off - kallsyms_addresses_off) / QWORD_SIZE

	#Making sure that kallsyms_num_syms matches the table size
	kallsyms_num_syms_off = label_align(kallsyms_addresses_end_off + LABEL_ALIGN)
	kallsyms_num_syms = read_qword(kernel_data, kallsyms_num_syms_off)
	if kallsyms_num_syms != num_symbols:
		print "[-] Actual symbol table size: %d, read symbol table size: %d" % (num_symbols, kallsyms_num_syms)
		return None	

	#Calculating the location of the markers table
	kallsyms_names_off = label_align(kallsyms_num_syms_off + LABEL_ALIGN)
	current_offset = kallsyms_names_off
	for i in range(0, num_symbols):
		current_offset += read_byte(kernel_data, current_offset) + 1
	kallsyms_markers_off = label_align(current_offset + LABEL_ALIGN)

	#Reading the token table
	'''
        Not sure if this can be a universal solution
        '''
	kallsyms_token_table_off = label_align(kernel_data.find(struct.pack("<Q", 0)*2, kallsyms_markers_off)+LABEL_ALIGN)
##	kallsyms_token_table_off = label_align(kallsyms_markers_off + (((num_symbols + 255) >> 8) * QWORD_SIZE))
	current_offset = kallsyms_token_table_off
	for i in range(0, 256):
		token_str = read_c_string(kernel_data, current_offset)
		current_offset += len(token_str) + 1
	kallsyms_token_index_off = label_align(current_offset + LABEL_ALIGN)

	#Creating the token table
	token_table = []
	for i in range(0, 256):
		index = read_word(kernel_data, kallsyms_token_index_off + i * WORD_SIZE)
		token_table.append(read_c_string(kernel_data, kallsyms_token_table_off + index))

	#Decompressing the symbol table using the token table
	offset = kallsyms_names_off
	symbol_table = []
	for i in range(0, num_symbols):
		num_tokens = read_byte(kernel_data, offset)
		offset += 1
		symbol_name = ""
		for j in range(num_tokens, 0, -1):
			token_table_idx = read_byte(kernel_data, offset)
			symbol_name += token_table[token_table_idx]
			offset += 1

		symbol_address = read_qword(kernel_data, kallsyms_addresses_off + i * QWORD_SIZE)
		symbol_table.append((symbol_address, symbol_name[0], symbol_name[1:]))
		
	return symbol_table

def main():

	#Verifying the arguments
	if len(sys.argv) < 2:
		print "USAGE: %s: <KERNEL_FILE> [optional: <0xKERNEL_TEXT_START>]" % sys.argv[0]
		return
	kernel_data = open(sys.argv[1], "rb").read()
	kernel_text_start = int(sys.argv[2], 16) if len(sys.argv) == 3 else DEFAULT_KERNEL_TEXT_START

	
	#Getting the kernel symbol table
	symbol_table = get_kernel_symbol_table(kernel_data, kernel_text_start)
	fp = open("syms","wb")
	for symbol in symbol_table:
		print "%016X %s %s" % symbol
		fp.write("%016X %s %s\n" % symbol)
	fp.close()
		
if __name__ == "__main__":
	main()

输出的符号会按照/proc/kallsyms打印出来,同时会写入当前目录syms文件。接下来就是让IDA Pro识别syms文件了,我的做法是针对每个符号尝试给特定地址重命名,如果失败就undefine以后再试一次,对于代码段的函数都重新makecode一次:

lines = open("syms","rb").read().split("\n")
for line in lines:
    [addr, type, name] = line.split(" ")
    if not MakeNameEx(int(addr,16), name, SN_NOWARN):
        MakeUnkn(int(addr,16),1)
        MakeNameEx(int(addr,16), name, SN_NOWARN)
    if type == "t" or type=="T":
        MakeUnkn(int(addr,16),1)
        MakeCode(int(addr,16))

暴力破解Android锁屏口令

JellyBean开始,Android的锁屏口令以hash形式存放,口令通常是4位数字(对于多位复杂口令方法也是一样的),暴力破解完全可行

锁屏口令的hash存放在/data/system/password.key,形如

11 36 65 6D 5C 67 18 C1 DE FC 71 B4 31 B2 CB 56 52 A8 AD 55 0E 20 BD CF 52 B0 00 02 C8 DF 35 C9 63 B7 12 98

共72个字符,包含Sha1和MD5两个hash,参考Android源码

byte[] saltedPassword = (password + getSalt()).getBytes();

byte[] sha1 = MessageDigest.getInstance(algo = “SHA-1”).digest(saltedPassword);

byte[] md5 = MessageDigest.getInstance(algo = “MD5”).digest(saltedPassword);

hashed = (toHex(sha1) + toHex(md5)).getBytes();

前40位是Sha1,后32位是MD5,计算(口令+salt)得到hash

salt的存放位置为/data/system/locksettings.db,使用sqlite3打开数据库,输入

select value from locksettings where name='lockscreen.password_salt'

就得到形如3582477098377895419的salt值了,最后将其转化为小写的16进制64位整数31 b7 83 f0 b0 c9 5d fb

有了这些信息用就可以用hashcat跑了,用MD5部分(0E 20 BD CF 52 B0 00 02 C8 DF 35 C9 63 B7 12 98)爆破的指令为

cudaHashcat64.exe -m 10 0E20BDCF52B00002C8DF35C963B71298:31b783f0b0c95dfb -a 3 ?d?d?d?d

OSX下调试Flash插件

其实无论调试什么,都会发现lldb的功能朴实的让人心急如焚。比如Windows调试器基本都会自动记录上一次的断点信息,每次调试时根据模块位置重新下好端点。

lldb可能天生就是为源码调试准备的,一旦没有源码,根据模块名+偏移的下端点方式它是无论如何都不能识别。好在它提供了python接口,方便开发调试插件弥补自身的缺陷。

所以与其说lldb是个调试器,不说它是个SDK,只有基于它开发出来的图形调试器才具备实用性。

那就来看看一些在Windows调试时不值得一提的简单操作,在lldb下该如何达阵

附加Flash进程


用Safari打开指定页面后,执行以下脚本lldb就会附加到包含Flash的进程上了

pid=$(ps aux | grep WebKit.Plugin | grep 64 | awk '{print $2}'|sort| tail -1) 
lldb -p $pid

搜索内存中的指定常数


由于没有查看memory layout的命令,只能借助vmmap,先找到比如malloc的内存区域,然后再逐一生成查找命令。以下命令用于从WebKit找到HeapSpray的特定字符:

pid=$(ps aux | grep Build | grep web | awk '{print $2}' | sort | tail -1)
vmmap $pid | grep "WebKit Malloc " | grep "-" | awk '{print $3}' | awk -F '-' '{print "memory find -e '$1'","0x" $1, "0x" $2}

在模块固定偏移下断点


这个听起来是最稀松平常的任务了,比如打算在Flash的0x78A4C0偏移处下一个断点,而且要在每次lldb附加后自动完成。首先要编写一个lldb的插件,完成Flash模块基地址的查找和断点地址的计算,最后下断点:

import lldb
import shlex
import optparse
import time

def obreak(debugger, command, result, dict):
  command_args = shlex.split(command)
  parser = create_obreak_options()
  try:
    (options, args) = parser.parse_args(command_args)
  except:
   return
  if len(args) > 0:
    offset = int(args[0], 16)
  else:
    offset = 0x78A4C0
  target = debugger.GetSelectedTarget()
  base = 0
  for mod in target.modules:
    if mod.file.GetFilename() == "FlashPlayer-10.6":
      for sec in mod.sections:
        if sec.name == "__TEXT":
          base = sec.get_addr().load_addr
  address = base+offset
  target.BreakpointCreateByAddress(address) 

def create_obreak_options():
  usage = "usage: %prog -f offset"
  description='''break on offset_belongs_to_Flash'''
  parser = optparse.OptionParser(description=description, prog='obreak',usage=usage)
  parser.add_option("-f", "--offset", dest="offset", help="break on certain offset of Flash", metavar="OFFSET")
  return parser

def __lldb_init_module (debugger, dict):
  parser = create_obreak_options()
  obreak.__doc__ = parser.format_help()
  debugger.HandleCommand('command script add -f %s.obreak obreak' % __name__)

但这还不够,还需要在.lldbinit.rc文件中增加一些辅助代码,保证上述插件的自动执行

command script import ~/path_to_obreak.py
target stop-hook add -o "script '--loading script--'"
target stop-hook add
obreak
target stop-hook disable 2
target stop-hook disable 3

OSX下调试WebKit

Safari的解析和渲染引擎WebKit是开源的项目,并提供了很多脚本方便调试。在OSX下分析一个漏洞还是头一回,用到的技巧大都取自WebKit官方的一篇JS引擎漏洞分析

准备环境


首先当然是要下载Webkit的源码,以前写过如何在Ubuntu环境里下载编译WebKit,当时只考虑了最新版本,所以直接从官网下载代码压缩包。

但如果是分析漏洞,一般要根据testcase的描述找到对应版本的WebKit,然后用svn和git下载指定版本的代码。

官方的建议的命令是

svn co -r 200796 http://svn.webkit.org/repository/webkit/trunk webkitDir

由于我查看的testcase信息来自于WebKit的Github镜像,所以用git下载的源码。

不过国内访问git的速度实在不怎么样,最后就用VPS从美国先git clone好整个源码树,然后压缩传回本地

根据testcases找到对应的branch编号以后,再切换过去

git reset --hard 6711d17

原理分析


为了尽快分析清楚漏洞的原理,当然要直接调用WebKit去解析testcase,先编译debug的程序:

./Tools/Scripts/set-webkit-configuration --asan
./Tools/Scripts/build-webkit --debug

然后就可以使用DumpRenderTree直接调用编译好的WebKit来解析引发问题的testcase了

VM=WebKitBuild/Debug/ && DYLD_FRAMEWORK_PATH=$VM lldb $VM/DumpRenderTree LayoutTests/js/regress-155776.html

真实环境下的调试


一旦问题分析清楚,必须回到Safari下才算是真正的利用过程起点。编译release版的程序:

./Tools/Scripts/build-webkit --release

然后使用提供的辅助脚本,就能自动将WebKit库注入到浏览器中,使Safari运行在刚刚编译好的WebKit之上:

./Tools/Scripts/run-safari

编译avm

2013年时Adobe停止了对Tarmain项目的维护,不想2016年三月居然重新更新了代码,近几年AVM的变化终于又能借此一窥究竟了。

linux下编译avmplus是最容易的:

cd avmplus
mkdir obj
cd obj
python ../configure.py
# 从生成的Makefile中删除-Werror
make

Windows下无论是用VS2010还是VS2015都无法顺利编译通过,基本上会遇到两类问题,一个是字符不识别,一个是函数未解析
1. 字符问题在Windows 10时已经不存在了,但Windows 7与到时候也非常容易解决,删掉ErrorConstant文件中非英语部分就OK了
2. 至于 unresolved symbol 问题,可以先查找到缺失类所属的文件,然后把它们加入到工程的对应目录也就OK了,这个错误按说真是很奇怪,怎么会漏掉文件没有加入到项目中呢

获取斗鱼直播视频的下载地址

斗鱼直播应该火了好一阵了,和每一个互联网新兴业务一样,人气激增到国家都专门为其制定管理政策了。对流行节拍有意而为之的后知后觉,让我近一两个月才关注了几个主播。女主播看脸蛋,也插科打诨来几个污段子,才美兼备的自是不多。男主播的话,就看他套路其他女主播的真人秀,或视频或夜店。

只是直播时间都太晚,看几次就觉得严重影响睡眠,能自动录播就好了。

网页里的播放器自然是用Flash开发的,P2P式的NetStream。如果找到主播房间号对应的NetStream传输地址就可以使用第三方软件去下载视频流了。虽然播放器的主程序是加密传输到本地后载入内存的,但毕竟Loader真正开始加载的时候,该解密的都解了。播放器外部代码还做了混淆,但主程序因为加密放下戒备,真Dump下来的话,可读性极高。

开源的FFDEC出现以后,SWF Decompiler和As3 Sorcerer都再没用过。直播开始后,FFDEC去Dump浏览器进程中的Flash文件,大小1.5M左右的就是解密后的播放器主程序。反编译可以看见代码本身包含调试信息,只要当前页面的URL中包含dydebug的字样,播放器就会调用浏览器的console.log输出不同阶段的中间结果,这当中就包含了NetStream流的地址,形如:

NetConnection连接状态:
NetConnection.Connect.Success 
Param.RtmpUrl =http://hdl3.douyucdn.cn/live   
Param.LiveID=602624rWYVytrxHL_550.flv?wsAuth=00f153fe25aa13a9e735f72774ae495a&token=web-0-602624-bb8f0c410da443bc88baa2c53e8d76c0&logo=0&expire=0

组合RtmpUrl和LiveID就得到了视频地址,直接粘贴到浏览器地址栏就下载视频到本地了。

正好新装了Visual Studio 2015,就写了个下载地址解析的工具,距离上一次使用C#写点什么已经四五年了。Anyway,webBrowser控件很方便,可以自动调用IE引擎打开附加dydebug的直播地址。由于console是浏览器自定义的调试模块,webBrowser作为一个控件需要自行实现一个console供Flash调用。其实也就是在页面加载完毕后,添加一段javascript,声明一个window.console.log,内部调用external.log就能传递消息调试信息给C#代码了。

解析程序是Windows 10的VS2015编译的,其他系统可能要安装.net运行时才能正常运行。输入房间号后,视网络状况,一分钟内可以解析出直播地址并放入剪贴板。

斗鱼直播地址解析程序下载:douyu

douyu

更多实现细节可参考主窗口的代码,为了运行清爽,webBrowser控件会隐藏,视频页面加载时会静音。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace douyu
{
    [System.Runtime.InteropServices.ComVisible(true)]
    public partial class Form1 : Form
    {
        [System.Runtime.InteropServices.DllImport("winmm.dll")]
        public static extern int waveOutGetVolume(IntPtr h, out uint dwVolume);
        
        [System.Runtime.InteropServices.DllImport("winmm.dll")]
        public static extern int waveOutSetVolume(IntPtr h, uint dwVolume);
        
        public Form1()
        {
            InitializeComponent();
            browser.ObjectForScripting = this;
        }

        public void log(String msg)
        {
            if(msg.Contains("NetConnection.Connect.Success"))
            {
                Match res = Regex.Match(msg, "RtmpUrl =(.+?) ");
                String rtmpurl = res.Groups[1].Value;
                res = Regex.Match(msg, "LiveID=(.+)");
                String liveid = res.Groups[1].Value;                
                String url_address = rtmpurl + "/" + liveid;
                /* considering the relocation (Thanks to lipinghao) */
                HttpWebRequest myReq = (HttpWebRequest)WebRequest.Create(url_address);
                myReq.AllowAutoRedirect = false;
                WebResponse response = myReq.GetResponse();
                if (response.Headers["Location"]!=null)
                {
                    url_address = response.Headers["Location"];
                }
                Clipboard.SetDataObject(url_address);
                resultbox.Text = url_address;
                urlpath.Text = "下载地址已经复制到剪贴板";
            }            
        }

        private uint _savedVolume;
        private void button_parse_Click(object sender, EventArgs e)
        {
            waveOutGetVolume(IntPtr.Zero, out _savedVolume);
            browser.Navigate("http://www.douyu.com/" + urlpath.Text + "?dydebug");
            waveOutSetVolume(IntPtr.Zero, 0);
        }

        private void browser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
        {
            HtmlElement head = browser.Document.GetElementsByTagName("head")[0];
            HtmlElement scriptEl = browser.Document.CreateElement("script");
            scriptEl.SetAttribute("text", "function hook_console() { window.console = new Object(); window.console.log = function(param){external.log(param)}}");
            head.AppendChild(scriptEl);
            browser.Document.InvokeScript("hook_console");
        }
        
    }
}

变革时别分神

这个假期回家有点早,也真的是后面的票都卖没了。也许是以天为单位的时差的缘故,除了旅途中短暂的喧嚣,世界甚至有些寂静。回想大学每次回家前,我总爱制定个时间表,紧锣密鼓、分秒必争,预习明年的课程或者写个程序练练手。研究生以后,说不上是看破红尘了还是怎么的,习惯就凭空消失了。怀着这段倍感亲切的记忆,我煞有介事地又写下了这个假期的时间表

望着新表,我觉得张口就能过度解释,拽很多理论进来。但还是很满意,下了车就逐条解释给最友好的听众,我妈。

虽然总看英文材料,但拼写能力真是递弱代偿,抽空抄抄单词是个简单的任务,练练字熟悉拼写,赚取第一笔成就感。然后看看书,随便哪本,毕竟书比网络快餐,收获的不仅仅是事实还有思想。

精彩的部分来了,11:00开始的一个小时是灵活时间段,姑且这么称呼好了。灵活时间段是本表区别以往的最大亮点,在这段时间里,可以和姥爷打牌,可以帮忙干活,并没有固定方向。一天中总会发生计划外的情况,尽量把他们安排在灵活时间段,既不会因为计划没完成产生负罪感,搞得一天心情都很糟糕,也能确保后面的计划照常执行。As you see,下午2:30是另一个灵活时间段。

午饭和晚饭的时间也相对靠后,家人和上班族当然不一样,即,计划时也要考虑周边人的情况,越少地影响他们越容易顺利执行。晚饭过后除了活动活动筋骨,foremost的任务就是完成家里电脑集体升迁Windows 10。回家前我就全副武装了U盘,常用工具和操作系统都备齐,毕竟家装的方正宽带(电信)表面提速暗地里断路。

现实是我妈实在懒得折腾去备份她电脑的资料,我只能循循善诱在D盘目录建了个bak文件夹,让她每天整理一点资料进去,打算最后再把bak拷到活动硬盘。这期间我大刀阔斧地给买了小米60寸、换了联通线路的宽带、给朋友爹的iMac重装了系统、买了新鞋,并通过数次谈话节目给姥爷从软文讲起,告诉他不能相信报刊上的医院信息,开药要适量,还有我的工作。到后来由于释放能量过多,甚至觉得空虚寂寞冷了。

不管怎样终于要开始升级Windows 10了,说说笑笑,我就重启进入硬盘分区步骤,把所有盘合并,准备分成三个区,再安装系统,恍然间,妈问,bak还没拷到活动硬盘吧。这下傻了,除了临行前她叮嘱的消消乐的作弊脚本拷走了,bak的内容全成了分区狂魔的刀下鬼。哎,准备了这么久,全白费啊,妈赶紧说没了就没了,更加剧了我的懊悔。

Windows 10安装完毕,期间无语。我只分了C盘用于操作系统,其余空间保持未分配的状态。一进入系统,赶紧试了试最近听说的一个磁盘恢复软件TestDisk,迷迷糊糊的用它恢复了分区信息。然后重启,果然Windows 10没了。重装Windows 10,D盘找回来了,bak也回来了。也许说了这么多都是变革,但初衷的升级Windows 10一溜号差点成了绝唱。

总结一下技术细节:

最初,C盘60G,D盘180G

重新分区时候

C盘50G,后面的空间没有分配所以没有改动

第一次装好Windows,用TestDisk恢复了原分区,由于C盘被破坏,Windows没了

分区再次变成C盘60G,D盘180G

第二次在C盘上安装系统,顺利进入,D盘也就赫然出现了,bak也在