简介
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直播授课