除法优化算法逆向分析


魔数除法算法逆向

寻找指定代码

本文任务为对样本dll库指定文件偏移处代码进行逆向分析。

首先从任务要求中可知,指定算法在文件偏移0x5FACE处

由于在windows环境下,我们使用Stud_PE程序辅助分析PE头所附带的各种信息:image_base为0x10700000 ;text段的文件偏移与RVA的差值为 0x52009-0x51009=0x1000

PE头信息

又从区段图中可知,文件偏移0x5FACE处仍在text段中,因此其偏移量不变,则逆向代码对应 VA=image_base+file_offset-offset=0x10700000+0x5FACE-0x1000=0x1075EACE

text区段大小

得出代码VA以后,我们可以继续使用Stud_PE查看一下对应文件偏移处的机器码,方便后续验证VA有效性

接下来使用IDA打开样本dll文件,并找到对应的VA处代码,将其与上图进行对照,发现机器码无误,VA计算正确

IDA中寻找对应VA代码

逆向分析算法

找到对应函数位置以后,我们正式开始进行代码逆向分析

windows下_fastcall调用约定中,前两个参数分别对应寄存器ecx、edx,因此我们设此处参数为arg(第一处cmp后将原始参数存入了栈中),此处比较后返回的代码串为

if(arg>=3&&arg<0xd){
	return &table1[arg*2]
}

虽然汇编代码中的查表处为*8,但是推测此处表类型为int(4字节),有(ecx*8)/bit=(arg*2)/int
1779884933101

解决完函数开头的条件分支以后,我们继续对右侧代码进行分析:整理出第一个汇编序列如下

序列1

mov eax,ecx ;eax=arg
cdq	    ;edx为arg符号位
mov edi,eax ;edi=arg
xor edi,edx ;根据arg正负判断是否取反
sub edi,edx ;根据arg正负判断是否+1

概括为 int a=abs(arg),此处我们为了简便,将每次的长串计算结果存储到一个变量中

继续向下查看整理出第二个汇编序列

序列2

mov esi,ecx ;esi=arg
shr esi,1Fh ;esi=(arg)>>31
sub esi,80000000h ;esi=(arg)>>31-0x80000000
mov eax,esi 
xor edx,edx
div edi ;esi / a且edx为余数
sub esi,edx ;等价于esi-esi%a
dec esi  ; 再-1

mov [esp+18h+var_8],1Fh;直接给局部变量赋值 

概括为 int b=(arg>>31)-0x80000000-((arg>>31)-0x80000000))-1;int c=31;

继续向下查看整理出第三个汇编序列

序列3

mov eax,80000000h
div esi
mov ebp,eax ;ebp =0x80000000/b = d

mov ebx,80000000h
imul eax,esi ;d*b
sub ebx,eax ; 0x80000000-(0x80000000/b)*b=0x80000000%b

mov eax,80000000h
div edi
mov ecx.eax ; ecx=0x80000000/a=f

mov edx,80000000h
imul eax,edi
sub edx,eax;同理,此处也是模运算

概括为 int d=0x80000000/b;int e=0x80000000%b;int f=0x80000000/a;int g=0x80000000%a;

变量初值部分逆向完毕,继续向下分析循环体部分,观察跳转部分可以得知,先进行内部操作,再判断循环条件,这应该是do_while循环

循环体分析

我们提炼出循环终止的条件,可以看到几个寄存器代表的值没有改变,因此有循环体如下

do{

}while((d<a-g) || (d==a-g)&&!e)//注意这里是先jnb跳转后,再用jz+test来判断跳转的,不可以直接写成>=

解决大循环体后,我们针对循环体内部操作进行逆向,此处的汇编代码较为简单,我们可以直接概括为C语言

e*=2;d*=2;++c;
if(e>=b){++d;e-=b;}
g*=2;f*=2;
if(g>=a){++f;g-=a;}

内部逆向

解决完循环体以后,我们来逆向该算法的最后一部分

首先可以看到向全局变量dword_1079F090处存入了f+1;然后将原始参数放入ecx中,测试其是否为负数;

若为负,执行取反操作;否则顺序执行最后一段汇编,全局变量dword_1079F090地址作为返回值,并将c-32的值存入另一个全局变量中

概括如下,最终返回值符合定义offset

g1 = arg+1;
if(arg<0)g1=-g1;
int *result=&g1;
g2=c-32;
return result;

返回值部分

最终我们完整分析出来的C代码为

int *magic_number(int arg)
{
  if ( arg >= 3 && arg < 0xD )
    return &table1[2 * a1];
  int a = abs32(arg);
  int c = 31;
  int b = (arg >> 31) - 0x80000000 - ((arg >> 31) - 0x80000000) % a - 1;
  int d = 0x80000000 / b;
  int e = 0x80000000 % b;
  int f = 0x80000000 / a;
  int g = 0x80000000 % a;
  do
  {
    e *= 2;
    d *= 2;
    ++c;
    if ( e >= b )
    {
      ++d;
      e -= b;
    }
    g *= 2;
    f *= 2;
    if ( g >= a )
    {
      ++f;
      g -= a;
    }
  }
  while ( d < a - g || d == a - g && !e );
  g1 = f + 1;
  if (arg<0)
    g1 = -g1;
  result = &g1;
  g2 = c - 32;
  return result;
}

数学模型构建

本节我们进行刚才逆向出来的算法的数学模型构建;重定义变量含义如下:

arg (ecx) 真实除数 divisor (d)
a (edi) 除数的绝对值 abs_d
b(esi) 辅助除数 aux_divisor
c(esp+8+c) 移位计数器,初始 31 shift_counter
d(ebp) 2^31 / aux_divisor 的商 aux_quot
e(ebx) 2^31 / aux_divisor 的余数 aux_rem
f (ecx) 2^31 / abs_d 的商(魔数种子) magic_quot
g(edx) 2^31 / abs_d 的余数 magic_rem
g1 最终魔数 magic_number (M)
g2 最终移位量 shift_amount (s)

函数初始的直接返回应该对应着快速查表,获取小除数的magic_number并直接返回。除此以外才是magic_number的真正生成过程,我们据此进行构建

首先明确目标,我们希望找到一个整数 M 和一个整数 s,使得对于任意整数 x,以下等式成立:(其中d为除数,M为魔数,s为移位量)

如果我们把 1/d 表示成一个定长的小数,比如 1/d ≈ Q / 2^N,其中 Q 是整数,那么 x/d ≈ (x * Q) >> N

那么寻找魔数与移位量的问题就被转换为求Q和N的问题

该算法的思路是:从 N = 31 开始,计算 Q = floor(2^31 / d)。但由于这是下取整,2^31 / d 总是略大于 Q。Q乘以 x 后,误差会被放大,导致结果偶尔比真实商小 1。为了消除这个误差,我们需要提高 N(增加精度),让 Q 更精确地逼近 2^N / d

因此我们在循环中维护计数器 cc(初始 31),并引入辅助除数 b

所以初始商和余数分别为:

其中 d,e 对应辅助除数 bf,g 对应绝对值 a

汇编代码里的 do ... while 循环,就是在逐位地执行 二进制长除法

在循环体执行 之前 ,始终满足:

循环体每次执行的操作将其视为长除法的左移过程:

  • 由 (1),2^c=d⋅b+e

  • 下一步 c′=c+1,则需表示 2^(c+1)=2d⋅b+2e

  • 若 2e≥b**,说明 2e 中可再分出一个 b,因此商多 1,余数减去 b。这正好给出:

    img

  • 同理,f′,g′ 更新后也满足

循环每迭代一次,c 递增 1,商和余数始终对应 2^c/b 和 2^c/a

循环条件公式

循环结束以后,由于我们最终的除法估算通过下界得到,所以不可避免的出现商小1的情况,因此我们在魔数上+1,向上进行舍入的修正


文章作者: Yssx
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Yssx !
评论
  目录