首页
社区
课程
招聘
OllyScript源代码阅读笔记
发表于: 2005-6-19 10:18 8099

OllyScript源代码阅读笔记

2005-6-19 10:18
8099
简介

OllyScript是外国的SHaG写的一个极好用的脚本语言解释器!通过它,咱们可以写出类汇编的脚本来完成一些自动化的功能,这样就不用写插件了。它最大的特点就是基本上封装了常用的OD插件命令,另外还提供了不少好用的功能!小弟斗胆写一写对它的代码阅读笔记,请各位大哥大姐们指导!!

在官方网站上能下到的最后的源代码是它的0.52版。OllyScript的作者也不知是怎么想的,每一次的更新似乎都对架构改动挺大的,而且脚本还不能兼容前面的版本!这有点像Borland的作风呀!每次Delphi/BCB升级都造成以前版本的源代码很难再使用……哈哈,不灌水了!!!

一、词法分析篇

作为一个脚本解释器,第一步当然就是解释出脚本语言中的一个个token啦!!正所谓“巧妇难为无米之炊”嘛!!

首先是在LoadScript()函数中过滤掉所有的注释字符串,并且解析出Label来!代码:

bool LoadScript(LPSTR file)
{
	ResetScript();

	ifstream fin(file, ios::in);
	string scriptline;
	bool is_comment = false;
	while(getline(fin, scriptline))
	{
		scriptline = trim(scriptline);
		if(scriptline == "/*")
			is_comment = true;
		else if(scriptline == "*/")
		{
			is_comment = false;
			continue;
		}

		if(scriptline != "" && !is_comment)
		{
			if(scriptline.find("//") == -1)
				script.push_back(scriptline);
			else if(scriptline.find("//") > 0)
			{
				script.push_back(trim(scriptline.substr(0, scriptline.find("//"))));
			}
		}
	}
	fin.close();

	ParseLabels();

	scriptIsRunning = true;

	return true;
}


过滤注释字符串很容易就不多说了!咱们看看解析label的函数ParseLabels():

bool ParseLabels()
{
	vector<string>::iterator iter;
	iter = script.begin();
	string s;
	int loc = 0;

	while(iter != script.end())
	{
		s = *iter;
		if(s.at(s.length() - 1) == ':')
			labels.insert(pair<string, int>(s.substr(0, s.length() - 1), loc));
		iter++;
		loc++;
	}

	return false;
}


对字符串进行判断,如果形如:“Label1:”,即最后有一个“:”,那么就认为它是一个label,并且记录到labels这个变量中,主要记录的是它的字符串值,以及它在源代码中的偏移!记录偏移是为了在一些跳转指令中,例如jmp、je等等,可以计算到应该跳转到哪里!

词法分析引擎主要是通过 split() 函数来获得一个个的单独的单词。代码如下:

bool split(vector<string> &vec, const string &str, const char delim)  
{
	vector<int> pos;
	bool inQuotes = false;

	for(uint i = 0; i < str.size(); i++)
	{
		if(str[i] == '"')
			inQuotes = !inQuotes;

		if(str[i] == delim && !inQuotes)
			pos.push_back(i);
	}

	vector<int>::iterator iter = pos.begin();

	if(iter == pos.end())
	{
		vec.push_back(str);
		return false;
	}

	uint start = 0, end = 0;

	while(iter != pos.end())
	{
		end = *iter;
		vec.push_back(trim(str.substr(start, end - start)));
		start = end + 1;
		iter++;
	} 

	if(start != str.size())
		vec.push_back(trim(str.substr(start)));

	return true;
}


先是判断单词是否在双引号内,是的话则表示这是一个在引号内的字符串,以后再来处理它!然后后面通过一个vector来储存字符串的结束位置,并且把字符串的前后空格去掉,最后字符串被储存到vec变量中!

大家可以看到词法分析的思路是很清晰的!但缺点在于作者使用了太多的STL(尤其是VC自带的STL!公认的差!),所以在效率上达不到特别高!推荐大家看看LCC的源代码,作者使用了双缓冲技术,在效率上为整个编译器带来了10%~20%的提升!

得到字符串的值之后,还要判断字符串属于哪种类型!也就是返回字符串的Token类型!咱们看看截取的一些代码片断:

bool is_hex(string& s)
{
	for(uint i = 0; i < s.length(); i++)
	{
		if( (s[i] < '0' || s[i] > '9') && (s[i] < 'a' || s[i] > 'f') && (s[i] < 'A' || s[i] > 'F'))
			return false;
	}

	return true;
}


这是用来判断字符串是否为16进制的!

int GetRegNr(string& name)
{
	if(name == "eax")
		return REG_EAX;
	else if(name == "ecx")
		return REG_ECX;
	else if(name == "edx")
		return REG_EDX;
	else if(name == "ebx")
		return REG_EBX;
	else if(name == "esp")
		return REG_ESP;
	else if(name == "ebp")
		return REG_EBP;
	else if(name == "esi")
		return REG_ESI;
	else if(name == "edi")
		return REG_EDI;
	return -1;
}


这是用来判断字符串是不是寄存器,并且是什么寄存器!

这些代码写得中规中矩,四平八稳的,估计SHaG也没有在效率上下太大的功夫!另外也可以看出OllyScript支持的类型比较有限!如果想构造一个有问题的Script应该是很容易的!

二、语法分析篇

OllyScript也是一个基于语法分析驱动的引擎!但它的问题在于写得并不像是正规、传统的语法分析驱动!咱们从编译的龙书上可以知道,手工写的语法分析一般都是LL(1),OllyScript也是LL(1),但它有太多不规范的写法了!细心点一看就会知道!

咱们看看一个例子:

bool CreateTwoOps(string& args)
{
	vector<string> v;
	if(split(v, args, ',') && v.size() == 2)
	{
		op1 = trim(v[0]);
		op2 = trim(v[1]);
		return true;
	}
	return false;
}


这是指令的操作数为2个的语法驱动模块。2个的意思就是指令后面跟2个参数(操作数),例如 add XXXX, 1000 就是这种类型的,XXXX会被放到op1变量里面,1000会被放到op2变量里面!如此类推,操作数只有1、2、3这三种情况,所以作者就写了CreateOneOp()、CreateTwoOps()和CreateThreeOps()三个函数了!

作者采用了这种非正统的写法,正统的写法应该是用一个match()函数去取得下一个token的类型,看能否匹配成功语法表,不成功的话则返还字符串到输入缓冲中!效率低下是作者的通病!

三、框架篇

终于来到框架了!OllyScript的框架是比较容易看明白的,咱们来看:

bool RunScriptSteps()
{
	require_ollyloop = 0;

	while(!require_ollyloop && scriptIsRunning)
	{
		int res = Process(script.at(script_pos));
		if(!res)
		{
			string message = "Error on line ";
			message.append(itoa(script_pos + 1, buffer, 10));
			message.append("\n");
			message.append("Text: ");
			message.append(script.at(script_pos));
			message.append("\n");
			if(errorstr != "")
			{
				message.append(errorstr);
				message.append("\n");
			}
			MessageBox(hwmain, message.c_str(), "OllyScript error!", MB_ICONERROR | MB_OK);
			errorstr = "";

			ResetScript();

			return false;
		}
		script_pos++;
	}
	if(script.size() > script_pos)
		return true;
	else
		return false;
}


这个就是主流程!只要符合两个条件,则一直会在循环中!这两个条件是:
1、不需要返回到OllyDbg中!这是因为发生断点(EOB等等)后,需要返回到OD中,所以这里要设个标记!
2、scriptIsRunning为真!这是表示解释器的当前运行状态为真!

主流程每次通过调用 Process() 函数来处理一行代码!如果发生错误,则打印错误信息并返回!否则就继续下一行!

bool Process(string& codeLine)
{
	codeLine = trim(codeLine);
	
	// Check if its a label
	if(codeLine.find_last_of(":") == codeLine.length() - 1)
		return true;

	command = codeLine;
	args = "";
	size_t pos = codeLine.find_first_of(" \t\r\n");
	if(pos != -1)
	{
		command = trim(codeLine.substr(0, pos));
		int (*pf)(int)=tolower; 
		transform(command.begin(), command.end(), command.begin(), pf);
		args = trim(codeLine.substr(pos));
	}

	bool result = false;

	if(commands.find(command) != commands.end())
	{
		// Command found, execute it
		result = commands[command]();
	}
	else 
	{
		// No such command
		errorstr = "No such command: " + codeLine;
	}
	
	return result;
}


在Process()里面首先判断token是否为一个label!是的话则不处理!否则就查询command表,查到的话则通过命令表指针跳到对应的处理函数中!否则就报错!表示没有这个命令!

四、与OD的交互篇

该解释器使用了一些小技巧来与OD进行交互,例如发生断点时应该回到OD中,它是这样做的:

extc void _export cdecl ODBG_Pluginmainloop(DEBUG_EVENT *debugevent) 
{
	t_status status;
	status = Getstatus();
	
	// Check for breakpoint jumps
	if(script_loaded && debugevent && debugevent->dwDebugEventCode == EXCEPTION_DEBUG_EVENT)
	{
		EXCEPTION_DEBUG_INFO edi = debugevent->u.Exception;
		if(edi.ExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT)
			OnBreakpoint();
		else if(edi.ExceptionRecord.ExceptionCode != EXCEPTION_SINGLE_STEP)
			OnException(edi.ExceptionRecord.ExceptionCode);
	}

	if(status == STAT_STOPPED && script_loaded && GetScriptState() == SS_RUNNING)
	{
		try
		{
			script_loaded = RunScriptSteps();
		}
		catch( ... )
		{
			ResetScript();
			MessageBox(hwmain, "An error occured in the plugin!\nPlease contact SHaG.", "OllyScript", MB_OK | MB_ICONERROR | MB_TOPMOST);
		}
	}
}


在OD提供的回调函数中,判断解释器的状态,如果为SS_RUNNING,则继续执行,否则如果debugevent里面出现了一个EXCEPTION_DEBUG_EVENT,则表示出现了调试状态,需要返回到OD中!

值得注意的是作者还使用了异常处理!这样虽然省事,但同样会导致效率的低下!

五、执行篇

讲到这里其实已经接近尾声了!因为脚本的执行只是一些代码的堆砌而已!例如:

bool DoMSG()
{
	if(!CreateOneOp(args))
		return false;

	string msg;
	if(GetSTROpValue(op1, msg))
	{
		MessageBox(hwmain, msg.c_str(), "OllyScript", MB_ICONINFORMATION | MB_OK);
		return true;
	}
	return false;
}


这里就是一个最简单的执行过程!首先由前面的语法、词法分析引擎知道当前遇到了一个“MSG”,然后进入到DoMsg()函数中,再在DoMsg()里面获得需要显示的内容,最后一个MessageBox()就OK!

六、优缺点分析

1、优点:OllyScript的代码简洁明了,框架比较清晰易懂!适合想写脚本引擎的初学者观看!
2、缺点:作者的编码功底只能算是一般,很多地方都不注重效率,估计是C++甚至是Java程序员出身的!

七、结束语

感谢各位前辈的观看!请指出文章中的不足和建议!由于小弟不喜欢聊天,就不留下联系方式私下讨论了哈!!

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 7
支持
分享
最新回复 (4)
雪    币: 519
活跃值: (1223)
能力值: ( LV12,RANK:650 )
在线值:
发帖
回帖
粉丝
2
编译原理还是很有用的呀.
虽然不太懂OD,还是支持一下.
2005-6-19 10:29
0
雪    币: 207
活跃值: (41)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
3
谢谢斑竹哈!俺们还没开编译原理的课程,好像要到大三才开,小弟以前自己看龙书一知半解的,请斑竹多指教!听师兄师姐们说编译原理中的形式语言与自动机理论比较重要,最近在看Ullman大牛的书!
2005-6-19 10:39
0
雪    币: 339
活跃值: (1510)
能力值: ( LV13,RANK:970 )
在线值:
发帖
回帖
粉丝
4
学习!
2005-6-19 20:24
0
雪    币: 100
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
学习中
有些看不懂!~!~
2005-6-21 08:40
0
游客
登录 | 注册 方可回帖
返回
//