RCTF2018-magic


涉及知识点

入口函数位置混淆

调用原始代码进行数据爆破

setjmp/longjmp机制

rc4密码算法

onexit机制

先运行发现输出错误信息

IDA打开,分析main函数可知:在main函数前面还有其他函数(不是启动函数),

  • swap_xor利用异或交换数据,并且传入的是地址,所以swap_xor(&v7, &v7)返回0,而在main函数其他地方也没有发现有输出的地方,所以main函数不是入口函数
  • 对main函数交叉引用跟踪到sub_4011B0函数

在sub_4011B0函数、main函数前分别下断点

  • 可以发现,main函数还没有执行,但报错信息已经输出

查看trace信息

  • 可以发现,在main函数前面sub_402218函数调用了puts函数,且sub_402357函数调用了sub_402218函数

跟到sub_402218函数

  • 可以看到该函数先对错误信息字符串解密,然后输出,再加密

对sub_402218交叉引用跟到sub_402357函数

  • 可以看到,当dword_4099D0[0]=0时,就会输出错误信息,所以在这里我们要让dword_4099D0[0]不等于0
  • 跟进sub_402268函数,发现其对dword_4099D[0]进行了相关赋值

跟入sub_402268函数(这里我已经打好了补丁)

  • 该函数的原始逻辑是:srand(v2[1]),所以我们要爆破时间戳

爆破时间戳

  • 这里直接调用程序里面的sub_402268函数进行爆破;Note:要给sub_402268中的其他函数赋一个地址

  • time:5B00E398

  • dword_4099D0[0]: 322CE7A4

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include "crypt.h"
#include<Windows.h>

typedef unsigned int(*check_time)();
static UINT time = 0x5AFFE78F + 1; //时间戳种子在(0x5AFFE78F,0x5B28A8F]之间
UINT mytime(int) { //遍历时间戳
	return time++;
}

char byte_405020_bak[256] = { 0 };
int main() {
	
	UINT64 * pTime64 = (UINT64 *)0x40A38C;
	UINT64 * pSrand  = (UINT64 *)0x40A414;
	UINT64 * pRand   = (UINT64 *)0x40A3FC;
	UINT64 * pMemset = (UINT64 *)0x40A3DC;

	HMODULE hmd = LoadLibrary(TEXT("your path\magic.exe"));
	memcpy(byte_405020_bak, (void*)0x405020, 256);
	check_time check = (check_time)0x402268;
	*pTime64 = (UINT64)mytime;
	*pSrand  = (UINT64)srand;
	*pRand   = (UINT64)rand;
	*pMemset = (UINT64)memset;

	while (TRUE) {
		memcpy((void*)0x405020, byte_405020_bak, 256);
		UINT res = check();
		if (res) {
			printf("time:%20X\ndword_4099D0[0]:%9X", time-1, res);
			break;
		}
	}
	return 0;
}

给程序打个补丁

打好补丁后,在运行程序

  • 此时,第一关已经攻破

保持sub_4011B0函数、main函数前的断点,再在sub_402268函数前下个断点,我们继续动态调试

  • sub_402357执行完后,回到sub_4032A0函数

  • 继续往后跟,跟到sub_40318函数
    • 发现onexit()函数,这个函数的作用是注册一个函数,使得程序在exit()的时候会调这个被注册的函数,这个被注册的函数就是sub_403260

  • 继续跟,会执行main函数,这里就不看了,因为main函数没啥用;main函数执行完后会调用exit函数(还记得上面的onexit🐎

  • 执行exit时,回调到sub_403260函数,这里跟进result就到都头了

  • 跟进sub_4023B1,这个函数就是上面的result,
    • 首先对我们的输入进行rc4加密(rc4的算法特征很明显
    • 然后再进入虚拟机(sub_4029C7(data)函数)

对虚拟机进行分析

  • 题型:给定了可执行程序和opcode,逆向emulator,结合opcode文件,推出flag

  • 这个其实挺简单的,指令作用都特别明显

  • 只是这里用了setjmp/longjmp机制(百度看一下,很简单)

"""
0ABh, 3, 0     reg[3] = 0
0ABh, 4, 1Ah   reg[4] = 0x1A
0ABh, 0, 'f'   reg[0] = 'f'
s1:
0AAh, 5, 2     reg[5] = reg[2]
0A9h, 'S'      reg[5] += reg[3]
0A0h, 5        reg[5] = reg[5][0]
0ABh, 6, 0CCh  reg[6] = 0xCC
0A9h, 'V'      reg[5] += reg[6]
0ABh, 6, 0FFh  reg[6] = 0xFF
0ACh, 'V'      reg[5] &= reg[6]
0AEh, 'P'      reg[5] ^= reg[0]
0ADh, 0        reg[0] = ~LOBYTE(reg[0])
0AAh, 6, 5     reg[6] = reg[5]
0AAh, 5, 1     reg[5] = reg[1]
0A9h, 'S'      reg[5] += reg[3]
0A0h, 5        reg[5] = reg[5][0]
0AFh, 'V', 0   
          dword_409060 = 5;
          dword_409064 = 6;
          if ( !setjmp(::Buf) )
            byte_405340[v7] = 5 / 0;  --> 异常  --> reg[5] =  reg[5] == reg[6]
          v7 += 2;


0A7h, 1
        if reg[5]:
          v7 += 1;
        ++v7;
        cmp reg[5] == 1
        jz s2

0CCh 

s2:
0A9h, '5'     reg[3] += reg[5]
0AAh, 5, 3    reg[5] = reg[3]
0AFh, 'T', 0   
        if ( !setjmp(::Buf) )
            byte_405340[v7] = 5 / 0;  --> 异常 --> reg[5] =  reg[5] == reg[4]
          v7 += 2;
        


0A6h, 0D1h
        if reg[5] == 0:
          v7 += 1;
        ++v7;
        cmp reg[5] == 1
        jz s1
0CCh
0, 0, 0, 0, 0, 0, 0
"""

指令序列对应汇编

"""
r1 = [
  0x89, 0xC1, 0xEC, 0x50, 0x97, 0x3A, 0x57, 0x59, 0xE4, 0xE6, 
  0xE4, 0x42, 0xCB, 0xD9, 0x08, 0x22, 0xAE, 0x9D, 0x7C, 0x07, 
  0x80, 0x8F, 0x1B, 0x45, 0x04, 0xE8, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00
]
r2 = rc4_input
mov r3, 0
mov r4, 0x1A
mov r0, 0x66

s1:
    mov r5, r2
    add r5, r3
    mov r5, byte ptr r5
    mov r6, 0xCC
    add r5, r6
    mov r6, 0xFF
    and r5, r6=0xFF
    xor r5, r0
    mov r0, ~LOBYTE(r0)
    mov r6, r5
    mov r5, r1
    add r5, r3
    mov r5, byte ptr r5
    cmp r5, r6
    jmp s2:
    exit

s2:
    add r3, r5
    mov r5, r3
    cmp r5, r4
    jmp s1
    exit
"""

指令序列对应python代码

rc4_input = "abcdefg"
r0 = 0x66
for i in range(len(rc4_input)):
    r6 = ((ord(rc4_input[i]) + 0xCC) & 0xFF) ^ r0
    r0 = (~r0)&0xFF
    if r1[i] != r6: break

直接爆破rc4_input:

  • 得到238cbefd25d765f4b6b3b6fe174a2effc384ed21a4ab11096a5
r0 = 0x66
r1 = [
  0x89, 0xC1, 0xEC, 0x50, 0x97, 0x3A, 0x57, 0x59, 0xE4, 0xE6, 
  0xE4, 0x42, 0xCB, 0xD9, 0x08, 0x22, 0xAE, 0x9D, 0x7C, 0x07, 
  0x80, 0x8F, 0x1B, 0x45, 0x04, 0xE8, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00
]
rc4_input = []
input = ""
for i in range(26):
    for j in range(0x100):
        r =  ((j + 0xCC) & 0xFF) ^ r0
        if r1[i] == r:
            rc4_input.append(hex(j))
            break
    r0 = (~r0)&0xFF
for i in rc4_input:
    input += i[2:]
print(input)

然后直接rc4解密就行:密钥就是前面的dword_4099D0[0],这里采用大佬的方法直接修改内存数据取解密,因为不知道dword_4099D0[0]有没有在其他地方改改,所以这样其实更加保险

脚本如下:

from idaapi import *
rc4 = [0x23,0x8c,0xbe,0xfd,0x25,0xd7,0x65,0xf4,0xb6,0xb3,0xb6,0xf,0xe1,0x74,0xa2,0xef,0xfc,0x38,0x4e,0xd2,0x1a,0x4a,0xb1,0x10,0x96,0xa5]
i = 0
for addr in range(data_addr, data_addr + 26): #我这里data_addr = 0x60FD54
    idc.patch_byte(addr, rc4[i])
    i = i + 1

结果如下:

参考文章:

https://cloud.tencent.com/developer/article/1170775

https://www.52pojie.cn/thread-742361-1-1.html


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