Back

Learn Basic Signal from DNUICTF easyre

暗泉杯第一次见到信号量实现的VM,稍微记录一下

一些信号量函数的学习

信号量的注册在init函数中

  v2.sa_handler = (__sighandler_t)sub_400E1D;
  v2.sa_flags = 4;
  sigaction(34, &v2, &v1);

sigaction() 函数定义为

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

sigaction 结构体定义如下

struct sigaction {
    void (*sa_handler) (int);
    sigset_t sa_mask;
    int sa_flags;       // 用来设置信号处理的相关操作
    void (*sa_restorer) (void);
}

关于 sa_flags 的定义如下 (from linux/include/uapi/asm-generic/signal-defs.h)

#ifndef SA_NOCLDSTOP
#define SA_NOCLDSTOP	0x00000001
#endif
#ifndef SA_NOCLDWAIT
#define SA_NOCLDWAIT	0x00000002
#endif
#ifndef SA_SIGINFO
#define SA_SIGINFO	0x00000004
#endif
/* 0x00000008 used on alpha, mips, parisc */
/* 0x00000010 used on alpha, parisc */
/* 0x00000020 used on alpha, parisc, sparc */
/* 0x00000040 used on alpha, parisc */
/* 0x00000080 used on parisc */
/* 0x00000100 used on sparc */
/* 0x00000200 used on sparc */
#define SA_UNSUPPORTED	0x00000400
#define SA_EXPOSE_TAGBITS	0x00000800
/* 0x00010000 used on mips */
/* 0x00800000 used for internal SA_IMMUTABLE */
/* 0x01000000 used on x86 */
/* 0x02000000 used on x86 */
/*
 * New architectures should not define the obsolete
 *	SA_RESTORER	0x04000000
 */
#ifndef SA_ONSTACK
#define SA_ONSTACK	0x08000000
#endif
#ifndef SA_RESTART
#define SA_RESTART	0x10000000
#endif
#ifndef SA_NODEFER
#define SA_NODEFER	0x40000000
#endif
#ifndef SA_RESETHAND
#define SA_RESETHAND	0x80000000
#endif

题目中使用的是 SA_SIGINFO,对信号处理程序提供了附加信息:一个指向 siginfo 结构的指针以及一个指向上下文标识符的指针

比如上述部分就是将 sub_400E1D 函数注册为34信号的处理函数

主程序中,为 2 信号注册了一个处理函数,这个函数是用于对最终加密结果进行判断的,随后进行了一个初始化,接下来进入一个死循环,等待子程序发送 2 信号

接下来需要了解是如何读取指令的

子程序中调用了函数 sub_400A0D(dword_4019C0, s1);,其中,第一个参数是vm的指令码,s1是输入的 flag

这个函数是由 while 循环和 switch 语句实现的

    v41 = a1[(unsigned __int8)qword_6030C8[20]];
    if ( v41 == 23 )
      break;
    ++qword_6030C8[20];

这里可以看出来 a1 存储了vm程序的机器码,qword_6030C8[20] 存放的是 eip

    switch ( v41 )
    {
      case 0:
      case 8:
      case 9:
      case 10:
      case 12:
      case 13:
      case 14:
      case 17:
      case 19:
      case 20:
        v2 = qword_6030C8[20];
        qword_6030C8[20] = v2 + 1;
        *v42 = a1[v2];
        break;
      default:
        break;
    }

这个 switch 中判断指令码是否为 0, 8, 9 ...,如果是,将会进行 eip+1 的操作,并再从机器码中取出一位,说明这些指令含有参数

接下来选择几个指令,分析是如何传参的

      case 0:
        v3 = getppid();
        sigqueue(v3, 34, (const union sigval)v42);
        break;

0 指令发送了 34 信号量,将参数设置为 sigval 进行传递,这个指令仅传递了一个指令参数

      case 1:
        val = qword_6030C8 + 16;
        v4 = getppid();
        sigqueue(v4, 34, (const union sigval)val);
        break;
      case 2:
        vala = qword_6030C8 + 17;
        v5 = getppid();
        sigqueue(v5, 34, (const union sigval)vala);
        break;
      case 3:
        valb = qword_6030C8 + 18;
        v6 = getppid();
        sigqueue(v6, 34, (const union sigval)valb);
        break;

1, 2, 3 三条指令区别仅在于使用的全局变量地址不同(可以猜测出来是三个不同的寄存器)

      case 8:
        valf = qword_6030C8 + 16;
        v11 = getppid();
        sigqueue(v11, 37, (const union sigval)valf);
        break;

根据之前的分析,8 指令理应含有参数,但只传递了寄存器一个参数,因此观察处理函数

int __fastcall sub_400F16(__int64 a1, siginfo_t *a2)
{
  *(_BYTE *)a2->_sifields._timer.si_sigval.sival_ptr += s1[79];
  return semop(semid, &stru_6030BE, 1uLL);
}

发现使用了 s1[79] 这个变量,而 vm 函数中含有语句 v42 = s1 + 79; 因此,参数通过 s1[79] 这个全局变量传递到了处理函数中

还有一类指令只有kill

      case 21:
        v24 = getppid();
        kill(v24, 46);
        break;

这类指令仅传递了全局变量

最后经过对每条指令的分析,以及动调查看内存,确定 qword_6030C8[19]esp*(_QWORD *)qword_6030C8 模拟了栈,*((_QWORD *)qword_6030C8 + 1) 是输入的开始地址,qword_6030C8[16, 17, 18] 是三个寄存器,qword_6030C8[21] 是跳转用的 ZF 标志位

Writeup

先手动反汇编

code = [
    17, 52, 0, 42, 5, 16, 20, 9, 23, 0, 36, 5, 3, 17, 29, 6, 0, 
    0, 5, 3, 17, 64, 6, 0, 72, 5, 17, 29, 23, 14, 1, 21, 4, 15, 
    1, 22, 2, 0, 0, 4, 3, 5, 16, 20, 50, 5, 9, 2, 19, 29, 5, 18, 
    21, 4, 16, 20, 61, 10, 1, 19, 52, 3, 4, 18, 14, 1, 21, 4, 7, 
    1, 22, 2, 0, 0, 4, 3, 5, 16, 20, 85, 5, 9, 1, 19, 64, 5, 18
]

sub_400E1D = "push({});"
sub_400E78 = "pop({});"
sub_400F16 = "{} += {};"
sub_400FA8 = "{} -= {};"

eip = 0 # 20
eax = 0 # 16
ebx = 0 # 17
ecx = 0 # 18
edx = 0 # 19
memory = [2] * 0x1000
memory2 = [1] * 50
while eip < len(code):
    cur_op = code[eip]
    if cur_op == 0:
        cur_arg = code[eip + 1]
        print ("_%02X:" % (eip), sub_400E1D.format(cur_arg))
        edx += 1
        memory[edx] = cur_arg
        eip += 1
    elif cur_op == 1:
        cur_arg = "eax"
        print ("_%02X:" % (eip), sub_400E1D.format(cur_arg))
        edx += 1
        memory[edx] = eax
    elif cur_op == 2:
        cur_arg = "ebx"
        print ("_%02X:" % (eip), sub_400E1D.format(cur_arg))
        edx += 1
        memory[edx] = ebx
    elif cur_op == 3:
        cur_arg = "ecx"
        print ("_%02X:" % (eip), sub_400E1D.format(cur_arg))
        edx += 1
        memory[edx] = ecx
    elif cur_op == 4:
        cur_arg = "eax"
        edx -= 1
        print ("_%02X:" % eip, sub_400E78.format(cur_arg))
        eax = memory[edx]
    elif cur_op == 5:
        cur_arg = "ebx"
        edx -= 1
        print ("_%02X:" % eip, sub_400E78.format(cur_arg))
        ebx = memory[edx]
    elif cur_op == 6:
        cur_arg = "ecx"
        edx -= 1
        print ("_%02X:" % eip, sub_400E78.format(cur_arg))
        ecx = memory[edx]
    elif cur_op == 7:
        eax += ebx
        print ("_%02X:" % eip, "eax += ebx;")
    elif cur_op == 8:
        cur_arg = code[eip + 1]
        print ("_%02X:" % eip, sub_400F16.format("eax", cur_arg))
        eax += cur_arg
        eip += 1
    elif cur_op == 9:
        cur_arg = code[eip + 1]
        print ("_%02X:" % eip, sub_400F16.format("ebx", cur_arg))
        ebx += cur_arg
        eip += 1
    elif cur_op == 10:
        cur_arg = code[eip + 1]
        print ("_%02X:" % eip, sub_400F16.format("ecx", cur_arg))
        ecx += cur_arg
        eip += 1
    elif cur_op == 11:
        eax -= ebx
        print ("_%02X:" % eip, "eax -= ebx;")
    elif cur_op == 12:
        cur_arg = code[eip + 1]
        print ("_%02X:" % eip, sub_400FA8.format("eax", cur_arg))
        eax -= cur_arg
        eip += 1
    elif cur_op == 13:
        cur_arg = code[eip + 1]
        print ("_%02X:" % eip, sub_400FA8.format("ebx", cur_arg))
        ebx -= cur_arg
        eip += 1
    elif cur_op == 14:
        cur_arg = code[eip + 1]
        print ("_%02X:" % eip, sub_400FA8.format("ecx", cur_arg))
        eip += 1
    elif cur_op == 15:
        eax ^= ebx
        print ("_%02X:" % eip, f"eax ^= ebx;")
    elif cur_op == 16:
        zf = (eax == ebx)
        print ("_%02X:" % eip, f"zf = (eax == ebx);")
    elif cur_op == 17:
        cur_arg = code[eip + 1]
        print ("_%02X:" % eip, f"push(eip+2); eip = {hex(cur_arg)};")
        memory[edx] = eip
        edx += 1
        eip += 1
    elif cur_op == 18:
        edx -= 1
        print ("_%02X:" % eip, f"pop(eip);")
    elif cur_op == 19:
        cur_arg = code[eip + 1]
        print ("_%02X:" % eip, f"eip = {hex(cur_arg)};")
        eip += 1
    elif cur_op == 20:
        cur_arg = code[eip + 1]
        print ("_%02X:" % eip, f"if zf:    eip = {hex(cur_arg)};")
        eip += 1
    elif cur_op == 21:
        memory[edx] = memory2[ecx]
        print ("_%02X:" % eip, f"push(memory2[ecx]);")
        edx += 1
    elif cur_op == 22:
        edx -= 1
        memory2[ecx] = memory[edx]
        print ("_%02X:" % eip, f"pop(memory2[ecx]);")
    elif cur_op == 23:
        print ("_%02X:" % eip, "break;")
    eip += 1

拿到反汇编结果

_00: push(eip+2); eip = 0x34;
_02: push(42);
_04: pop(ebx);
_05: zf = (eax == ebx);
_06: if zf:    eip = 0x9;
_08: break;
_09: push(36);
_0B: pop(ebx);
_0C: push(ecx);
_0D: push(eip+2); eip = 0x1d;
_0F: pop(ecx);
_10: push(0);
_12: pop(ebx);
_13: push(ecx);
_14: push(eip+2); eip = 0x40;
_16: pop(ecx);
_17: push(72);
_19: pop(ebx);
_1A: push(eip+2); eip = 0x1d;
_1C: break;
_1D: ecx -= 1;
_1F: push(memory2[ecx]);
_20: pop(eax);
_21: eax ^= ebx;
_22: push(eax);
_23: pop(memory2[ecx]);
_24: push(ebx);
_25: push(0);
_27: pop(eax);
_28: push(ecx);
_29: pop(ebx);
_2A: zf = (eax == ebx);
_2B: if zf:    eip = 0x32;
_2D: pop(ebx);
_2E: ebx += 2;
_30: eip = 0x1d;
_32: pop(ebx);
_33: pop(eip);
_34: push(memory2[ecx]);
_35: pop(eax);
_36: zf = (eax == ebx);
_37: if zf:    eip = 0x3d;
_39: ecx += 1;
_3B: eip = 0x34;
_3D: push(ecx);
_3E: pop(eax);
_3F: pop(eip);
_40: ecx -= 1;
_42: push(memory2[ecx]);
_43: pop(eax);
_44: eax += ebx;
_45: push(eax);
_46: pop(memory2[ecx]);
_47: push(ebx);
_48: push(0);
_4A: pop(eax);
_4B: push(ecx);
_4C: pop(ebx);
_4D: zf = (eax == ebx);
_4E: if zf:    eip = 0x55;
_50: pop(ebx);
_51: ebx += 1;
_53: eip = 0x40;
_55: pop(ebx);
_56: pop(eip);

发现其实是调用了几个函数,分别在0x34、0x40、0x1D

0x34处的函数判断了长度,0x1D处的函数从后往前异或数字,每次加2,0x40处的函数从后往前进行加法,数字每次加1

所以最后的加密算法为:首先从后往前 ^36, ^38, ^40,随后从后往前+0, +1, +2…,最后从后往前 ^72, ^74, ^76…

反向解密即可

s2 = [
    0xA3, 0xD8, 0xAC, 0xA9, 0xA8, 0xD6, 0xA6, 0xCD, 0xD0, 0xD5,
    0xF7, 0xB7, 0x9C, 0xB3, 0x31, 0x2D, 0x40, 0x5B, 0x4B, 0x3A,
    0xFD, 0x57, 0x42, 0x5F, 0x58, 0x52, 0x54, 0x1B, 0x0C, 0x78,
    0x39, 0x2D, 0xD9, 0x3D, 0x35, 0x1F, 0x09, 0x41, 0x40, 0x47,
    0x42, 0x11
]

flag = ''
x = 36
y = 0
z = 72

for i in s2[::-1]:
    flag += chr(((i ^ z) - y) ^ x)
        x += 2
        y += 1
        z += 2

print (flag[::-1])
# 'flag{Now_Y0u_Know_th4_Signa1_0f_Linux!!!!}'
Built with Hugo
Theme Stack designed by Jimmy