SSE指令由浅入深

June 12, 2019 逆向 访问: 24 次

最近在CTF比赛中遇到过几次SSE指令,之前没怎么了解这个,于是抽时间总结了一些这方面的知识,希望能帮助更多人理解SSE指令。

介绍

SSE是一组Intel CPU指令,是继MMX的扩充指令集,也是流化SIMD扩展,提供有70条新指令和一组暂存器,用于处理128位打包数据。在Linux下可以使用cat /proc/cpuinfo来查看CUP支持的指令集。

SSE新增的八个新的128位暂存器,这些128位元的暂存器可以用存放32位源的单精度浮点数,SSE的浮点数运算指令就是使用这些暂存器,下面是SSE新增的暂存器的示意图

SSE的浮点数运算指令可以分成两类:Packed和Scalar;Packed指令是一次对XMM暂存器中4个浮点数(DATA0!DATA3)都进行计算,而Scalar则只对XMM暂存器中的DATA0进行运算。如下图所示:

常用SSE指令和解释

浮点指令

  • 赋值
movaps  把4个对齐的单精度值传送到xmm寄存器或者内存
movups  把4个不对齐的单精度值传送到xmm寄存器或者内存
movlps  把2个单精度值传送到内存或者寄存器的低四字
movhps  把2个单精度值传送到内存或者寄存器的高四字
movlhps 把2个单精度值从低四字传送到高四字
movhlps 把2个单精度值从高四字传送到低四字
  • 数学运算
addps   将两个打包值相加
subps   将两个打包值相减
mulps   将两个打包值相乘
divps   将两个打包值相除
rcpps   计算打包值的倒数
sqrtps  计算打包值的平方根
rsqrtps 计算打包值的平方根倒数
maxps   计算两个打包值中的最大值
minps   计算两个打包值中的最小值
  • 比较
cmpps   比较打包值
cmpss   比较标量值
comiss  比较标量值并且设置eflags寄存器
ucomiss 比较标量值(包括非法值)并设置eflags寄存器
  • unpack和shuffle
SHUFPS  交错的结果对存储在xmm1中
UNPCKHPS 从xmm1和xmm2 / m128的高四字中解包和交错单精度浮点值
UNPCKLPS 从xmm1和xmm2 / m128的低四字中解包和交错单精度浮点值。
  • 逐位逻辑运算
andps   计算两个打包值的按位逻辑与
andnps  计算两个打包值的按位逻辑非
orps    计算两个打包值的按位逻辑或
xorps   计算两个打包值的按位逻辑异或

整数指令

pavgb   计算打包无符号字节整数的平均值
pavgw   计算打包无符号字整数的平均值
pextrw  把一个字从mmx寄存器复制到通用寄存器
pinsrw  把一个字从通用寄存器复制到mmx寄存器
pmaxub  计算打包无符号字节整数的最大值
pmaxsw  计算打包有符号字整数的最大值
pminub  计算打包无符号字节整数的最小值
pminsw  计算打包有符号字整数的最小值
pmulhuw 将打包无符号字整数相乘并且存储高位结果
psadbw  计算无符号字节整数的绝对差的总和

SSE浮点数运算操作

addps/addss _mm_add_ps/_mm_add_ss   加法
subps/subss _mm_sub_ps/_mm_sub_ss   減法
mulps/mulss _mm_mul_ps/_mm_mul_ss   乘法
divps/divss _mm_div_ps/_mm_div_ss        除法
sqrtps/sqrtss   _mm_sqrt_ps/_mm_sqrt_ss 平方根
maxps/maxss _mm_max_ps/_mm_max_ss   逐项取最大值
minps/minss _mm_min_ps/_mm_min_ss   逐项取最小值

SSE指令的运用

可以分别用C++、SSE内嵌原语、SSE汇编来实现运算sqrt(f1*f1+f2*f2)+0.5
纯C++代码

void my_sqrt(  float* pA_1,float* pA_2, float* pR_1, int nSize)
{
    int i;
    float* pS_1 = pA_1;
    float* pS_2 = pA_2;
    float* pD_1 = pR_1;
    for ( i = 0; i < nSize; i++ )
    {
        *pD_1 = (float)sqrt((*pS_1) * (*pS_1) + (*pS_2)*(*pS_2)) + 0.5f;
        pS_1++;
        pS_2++;
        pD_1++;
    }
}

SSE内嵌原语

void my_sqrt_sse_1(  float* pA_1,float* pA_2, float* pR_1, int nSize)
{
    int nLoop = nSize/ 4;
    __m128 m1, m2, m3, m4;
    __m128* pSrc1 = (__m128*) pA_1;
    __m128* pSrc2 = (__m128*) pA_2;
    __m128* pD_1 = (__m128*) pR_1;
    __m128 m0_5 = _mm_set_ps1(0.5f);
    for ( int i = 0; i < nLoop; i++ )
    {
        m1 = _mm_mul_ps(*pSrc1, *pSrc1);
        m2 = _mm_mul_ps(*pSrc2, *pSrc2);
        m3 = _mm_add_ps(m1, m2);
        m4 = _mm_sqrt_ps(m3);
        *pD_1 = _mm_add_ps(m4, m0_5);
        pSrc1++;
        pSrc2++;
        pD_1++;
    }
}

SSE汇编

void my_sqrt_sse_1(  float* pA_1,float* pA_2, float* pR_1, int nSize)
{
    int nLoop = nSize/4;
    float f = 0.5f;
    _asm
    {
        movss   xmm2, f                         // xmm2[0] = 0.5
        shufps  xmm2, xmm2, 0                   // xmm2[1, 2, 3] = xmm2[0]
        mov         esi, pA_1                   // 输入的源数组1的地址送往esi
        mov         edx, pA_2                   // 输入的源数组2的地址送往edx
        mov         edi, pR_1                   // 输出结果数组的地址保存在edi
        mov         ecx, nLoop                  //循环次数送往ecx
start_loop:
        movaps      xmm0, [esi]                 // xmm0 = [esi]
        mulps       xmm0, xmm0                  // xmm0 = xmm0 * xmm0
        movaps      xmm1, [edx]                 // xmm1 = [edx]
        mulps       xmm1, xmm1                  // xmm1 = xmm1 * xmm1
        addps       xmm0, xmm1                  // xmm0 = xmm0 + xmm1
        sqrtps      xmm0, xmm0                  // xmm0 = sqrt(xmm0)
        addps       xmm0, xmm2                  // xmm0 = xmm1 + xmm2
        movaps      [edi], xmm0                 // [edi] = xmm0
        add         esi, 16                     // esi += 16
        add         edx, 16                     // edx += 16
        add         edi, 16                     // edi += 16
        dec         ecx                         // ecx--
        jnz         start_loop                  //如果不为0则转向start_loop
    }
}

MMX指令和解释

MMX指令分为一下几种:

  • 数据传送:movd, movq
  • 数据转换:packsswb, packssdw, packuswb, punpckhbw, punpckhwd, punpckhdq, punpcklbw, punpcklwd, punpckldq
  • 并行算术:paddb, paddw, paddd, paddsb, paddsw, paddusb, paddusw, psubb, psubw, psubd, psubsb, psubsw, psubusb, psubusb, psubusw, pmulhw, pmullw, pmaddwd
  • 并行比较:pcmpeqb, pcmpeqw, pcmpeqd, pcmpgtb, pcmpgtw, pcmpgtd
  • 并行逻辑:pand, pandn, por, pxor
  • 移位与旋转:psllw, pslld, psllq, psrlw, psrld, psrlq, psraw, psrad
  • 状态管理:emms

下面只列出一些常用指令的解释,如果有需要的话可以到传送门 去查询

movd  变量a的32位拷贝到MMX寄存器的低32位,高32位置零
movd  MMX变量的低32位拷贝到int类型中
punpcklbw MM,MM/m64   把目的寄存器与源寄存器的低32位按字节交错排列放入目的寄存器
punpcklwd MM,MM/m64   把目的寄存器与源寄存器的低32位按字交错排列放入目的寄存器
pshufd XMM,XMM/m128,imm8(0~255)  将源存储器的4个双字由imm8指定选入目的寄存器,内存变量必须对齐内存16字节
movaps  把4个对准的单精度值传送到xmm寄存器或者内存
pmulld  对128位寄存器的每32位做整形乘法运算,形成一个64位的立即数,然后取立即数的低32位到目的寄存器的对应bit位中
movups  将压缩单精度浮点值从 xmm2/m128 移到 xmm1
paddd   对128位寄存器的每32位做整形加法运算
pxor    对 xmm2 同 xmm1 执行逐位“异或”运算

例子0x01

以2019强网杯Just re作为例子,具体获取flag的方式不再说了,只谈一下涉及到SSE、MMX指令的部分
在check1中存在一段这样的指令:

.text:00401728                 movsx   ecx, dh
.text:0040172B                 movd    xmm0, ecx
.text:0040172F                 punpcklbw xmm0, xmm0
.text:00401733                 punpcklwd xmm0, xmm0
.text:00401737                 pshufd  xmm0, xmm0, 0
.text:0040173C                 movaps  xmmword ptr [ebp-20h], xmm0
.text:00401740                 test    esi, esi
.text:00401742                 jz      loc_401891
.text:00401748                 xor     esi, esi
.text:0040174A                 cmp     dword_4053C4, 2
.text:00401751                 jl      loc_40180B
.text:00401757                 movd    xmm0, dword ptr [ebp-20h]
.text:0040175C                 mov     esi, 10h
.text:00401761                 movaps  xmm3, ds:xmmword_404340
.text:00401768                 pmovzxbd xmm4, xmm0
.text:0040176D                 pmulld  xmm4, ds:xmmword_404380
.text:00401776                 movups  xmm0, xmmword_405018
.text:0040177D                 movaps  xmm1, xmm4
.text:00401780                 movups  xmm2, xmm3
.text:00401783                 paddd   xmm1, xmm0
.text:00401787                 paddd   xmm2, xmm5
.text:0040178B                 movups  xmm0, xmmword_405028
.text:00401792                 pxor    xmm2, xmm1
.text:00401796                 movaps  xmm1, xmm4
.text:00401799                 movups  xmmword_405018, xmm2
.text:004017A0                 movaps  xmm2, ds:xmmword_404350
.text:004017A7                 paddd   xmm1, xmm0
.text:004017AB                 movups  xmm0, xmmword_405038
.text:004017B2                 paddd   xmm2, xmm3
.text:004017B6                 paddd   xmm2, xmm5
.text:004017BA                 pxor    xmm2, xmm1
.text:004017BE                 movaps  xmm1, xmm4
.text:004017C1                 movups  xmmword_405028, xmm2
.text:004017C8                 movaps  xmm2, ds:xmmword_404360
.text:004017CF                 paddd   xmm1, xmm0
.text:004017D3                 movups  xmm0, xmmword_405048
.text:004017DA                 paddd   xmm2, xmm3
.text:004017DE                 paddd   xmm2, xmm5
.text:004017E2                 paddd   xmm4, xmm0
.text:004017E6                 pxor    xmm2, xmm1
.text:004017EA                 movaps  xmm1, ds:xmmword_404370
.text:004017F1                 paddd   xmm1, xmm3
.text:004017F5                 paddd   xmm1, xmm5
.text:004017F9                 pxor    xmm1, xmm4
.text:004017FD                 movups  xmmword_405038, xmm2
.text:00401804                 movups  xmmword_405048, xmm1

对于这类题,用X32dbg/X64dbg动态调试最容易理解程序的流程,经过动态调试分析,不难发现这段汇编代码的意思和下面这段一样

do
{
  *(&xmmword_405018 + v20) = (v20 + v3) ^ (0x1010101 * v11 + *(&xmmword_405018 + v20));
  ++v20;
}
while ( v20 < 24 );

例子0x02

SSE不仅运用被在CTF逆向题目中,在PWN题目中也可能出现
首先程序只开启了PIE保护,载入IDA中查看程序流程

程序先是使用__xstat函数对flag.txt的大小进行了查询,然后分配了文件大小+33的空白内存给s
sub_B1C函数是通过/dev/urandom来生成32位字符串

这个函数中存在SSE指令的是sub_930函数

.text:0000000000000930 sub_930         proc near               ; CODE XREF: main+A0↓p
.text:0000000000000930                                         ; sub_B1C+33↓p
.text:0000000000000930                 mov     rax, 0FFFFFFFFFFFFFFF0h
.text:0000000000000937                 mov     rdx, rdi
.text:000000000000093A                 pxor    xmm0, xmm0
.text:000000000000093E
.text:000000000000093E loc_93E:                                ; CODE XREF: sub_930+19↓j
.text:000000000000093E                 add     rax, 10h
.text:0000000000000942                 pcmpistri xmm0, xmmword ptr [rdx+rax], 8
.text:0000000000000949                 jnz     short loc_93E
.text:000000000000094B                 add     rax, rcx
.text:000000000000094E                 retn
.text:000000000000094E sub_930         endp
.text:000000000000094E

代码比较简单,用到了pcmpistri指令,意思是执行字符串数据与隐式长度的打包比较,生成索引,并将结果存储在ECX中;这个函数的作用是确保从/dev/urandom读取的字符串长度小于0x5f
继续往下看,会发现在sub_94F函数中也存在SSE指令

经过动态调试分析出来该函数的意思:如果我们输入的字符串的ascii码小于内存中字符串的ascii码的话,函数返回1,如果大于则返回-1,相等的话返回(index+1)%16
那么我们可以通过不断的猜测得到内存中的字符串,也就是flag
Exp:

from pwn import *
context.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
index_1 = "qwertyuiop[]\\asdfghjkl;'zxcvbnm,./`1234567890--=~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:\"ZXCVBNM<>?"
index_1 = ''.join(sorted(index_1))
flag = ""
r = process("./pwn")
k = 2
def guess(string, l, x):
    r.recvuntil("> ")
    mid = (l+x)/2
    #gdb.attach(r)
    r.sendline(flag + string[mid])
    res = r.recvline()[:-1]
    res = int(res.split("[")[1].split("]")[0])
    if res == k:
        return string[mid]
    elif res < 0:
        return guess(string, l, mid - 1)
    return guess(string, mid + 1, x)
for i in xrange(32):
    a = guess(index_1, 0, len(index_1))
    flag += a
    k+=1
    if k > 16:
        k = 1
index_2 = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
for i in xrange(20):
    a = guess(index_2, 0, len(index_2))
    flag += a
    k+=1
    #print flag
    if k > 16:
        k = 1
log.info("flag  ---> " + flag)

总结

通过做一些存在SSE指令的题目可以发现,动态调试更方便我们理解程序到底是干什么的。
本文如有不妥之处,敬请斧正。

参考文献:

x86 and amd64 instruction reference
Ping-Che Chen

添加新评论