Flareon challenge 4 第3题

| categories crack  translations  | tags RE  flareon 

本文已发表在看雪论坛, 详情可见: https://bbs.pediy.com/thread-223473.htm

题目作者: Matt Williams (@0xmwilliams)

翻译前言: 文章对代码自修改的分析很细致, 使用Unicorn框架来模拟执行代码和Capstone进行反汇编.

文中分析的程序你可以点击此处下载: greek_to_me.zip, 解压密码: www.pediy.com

greek_to_me.exe是一个Windows x86可执行文件, 如下图所示, 程序中的字符串表露了00401101处要达成的情况, 如下所示.

004010F5 push 0 ; flags
004010F7 push 2Bh ; len
004010F9 push offset aCongratulation ; "Congratulations! But wait, where's...”
004010FE push [ebp+s] ; s
00401101 call ds:send

然而, 在地址00401101前面的汇编代码却包含如下所示的奇怪汇编指令

004010A0 icebp
004010A1 push es
004010A2 sbb dword ptr [esi], 1F99C4F0h
004010A8 les edx, [ecx+1D81061Ch]
004010AE out 6, al ; DMA controller, 8237A-5.
004010AE ; channel 3 base address
004010AE ; (also sets current ad

不过也许你在此时能准确地猜测到, 程序为了能达到地址0x401101, 会修改这些奇怪的指令, 因为这些奇怪的指令运行下去, 我们的程序会极有可能崩溃. 另一种迹象能暗合我们认为这是代码自修改的推测, 那就是在查看程序的文件头时, 我们发现程序入口点所在的.text区段是可写的. 到这里, 我们正常的套路就可以往上查看分析, 看是什么能让程序选择0x401063的正确分支.

当然还有另外一种方法就是确定程序的套接字是在哪里生成的, 话不多说, 我们这就来尝试.

greek_to_me.exe包含有0x401151处的一个简单socket函数调用, 如下图所示

fig3.png

sub_401121里我们可以观察到, 程序用了一系列Windows API函数: socket,bind,listen和accept创建了一个监听本地TCP端口2222(0x8AE)的套接字

程序一直等待着监听端口的连接, 直到从建立连接的客户端那接收到最多4个字节. 接收到的字节会存储在缓冲区中并以参数的形式传递给sub_401121. 一旦有接收到字节, 该函数就能在 不停止现有连接的情况下返回一个socket句柄. 要记住, 当执行到0x4010710x401101时, 程序就会使用到它.

如果sub_401121返回了一个合法的socket句柄, 程序会继续执行, 否则程序退出. 如下代码块为寄存器赋初值, 这几个寄存器将在解码循环中发挥用处

00401029 mov ecx, offset loc_40107C
0040102E add ecx, 79h
00401031 mov eax, offset loc_40107C
00401036 mov dl, [ebp+buf]

我们看这段代码, 首先, 一个位于.text区段的可执行的代码地址赋值给ECX寄存器, 并且加上了常量值79h, 这也表明了随后将介绍的解码循环里的终止地址. 地址0x40107C赋给EAX寄存器, 代表解码循环的起始地址. 在0x401036, recv缓冲区的第1个字节被赋给了EDX寄存器的低8位

继续向下看代码块, 其中包含一个进行如下操作的循环

  1. 取出存储在EAX中的地址(0x40107C)所指向内容的1个字节
  2. 将取出的字节跟监听端口收到的第1个字节进行异或
  3. 异或操作得到的结果再加上0x22
  4. 将结果覆写回第1步取出的字节处
00401039 loc_401039:
00401039 mov bl, [eax]
0040103B xor bl, dl
0040103D add bl, 22h
00401040 mov [eax], bl
00401042 inc eax
00401043 cmp eax, ecx
00401045 jl short loc_401039

EAX中存储的地址则自增1并且跟ECX中存储的最大地址进行比较, 只有当EAX的内容跟最大地址0x4010F5相等时循环才会结束.

继续向下看, 程序随后便将刚刚修改了的代码块首地址(0040107C)和块大小0x79作参数传递给sub_4011E6.

00401047 mov eax, offset loc_40107C
0040104C mov [ebp+var_C], eax
0040104F push 79h
00401051 push [ebp+var_C]
00401054 call sub_4011E6
00401059 pop ecx
0040105A pop ecx
0040105B movzx eax, ax
0040105E cmp eax, 0FB5Eh
00401063 jz short loc_40107C

我们可以看到程序返回值的低16位(AX)赋给了EAX寄存器再将EAX跟硬编码值0xFB5E进行比较, 而比较的结果则决定了程序是跳向0x40107C还是执行到显示失败信息的分支

00401065 push 0 ; flags
00401067 push 14h ; len
00401069 push offset buf ; "Nope, that's not it."
0040106E push [ebp+s] ; s
00401071 call ds:send

获得这些信息, 我们可以做出正确假设: sub_4011E6是用来计算验证值或者说是之前解码循环所修改的字节的校验值. 并且可以确定从socket接受到的字节值是用作异或修改0x40107C0x4010F4之间代码块的key值. 而程序自修改的代码则通过一个硬编码的校验值进行验证. 因为使用的key只有单字节, 因此我们可以进行简单的暴力穷举来获得期望的key.

如果修改后的代码正常执行并且通过socket返回了Congratulations字符串, 那么就可以确定暴力穷举成功了. 基于这个假设, 我们可以编写一个如下的脚本代码帮助输出正确值:

import sys
import os
import time
import socket
TCP_IP = '127.0.0.1'
TCP_PORT = 2222
BUFFER_SIZE = 1024
for i in range (0,256):
	os.startfile(sys.argv[1])
	time.sleep(0.1)
	s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	s.connect((TCP_IP, TCP_PORT))
	s.send(chr(i))
	data = s.recv(BUFFER_SIZE)
	s.close()
	if 'Congratulations' in data:
		print "Key found: %x" % i
		break

但如果我们并不想基于解码的字节都正确执行这样一个假设来操作, 而是自己验证解码后的校验值是否匹配, 要怎么办呢? 相比花大量时间逆向校验算法, 这次我们来尝试体验一个有趣的恶意代码分析技术: 代码模拟执行

首先, 我们提取校验函数sub_4011E6的操作码, 我们只关心在0x401265执行完后存储在AX中的返回值, 如下图所示. 并且不需要提取函数的平衡栈的结尾部分. fig9.png

我们同样也需要从0x40107C处提取0x79长度的待解码字节. 我们提取的字节集合都在如下的用于模拟执行的python简易脚本中可见

import binascii
import struct
from unicorn import *
from unicorn.x86_const import *
from capstone import *
CHECKSUM_CODE = binascii.unhexlify(
'55 8B EC 51 8B 55 0C B9 FF 00 00 00 89 4D FC 85 D2 74 51 53 8B 5D 08 56 57 '
'6A 14 58 66 8B 7D FC 3B D0 8B F2 0F 47 F0 2B D6 0F B6 03 66 03 F8 66 89 7D '
'FC 03 4D FC 43 83 EE 01 75 ED 0F B6 45 FC 66 C1 EF 08 66 03 C7 0F B7 C0 89 '
'45 FC 0F B6 C1 66 C1 E9 08 66 03 C1 0F B7 C8 6A 14 58 85 D2 75 BB 5F 5E 5B '
'0F B6 55 FC 8B C1 C1 E1 08 25 00 FF 00 00 03 C1 66 8B 4D FC 66 C1 E9 08 66 '
'03 D1 66 0B C2'.replace(' ', ''))
ENCODED_BYTES = binascii.unhexlify(
'33 E1 C4 99 11 06 81 16 F0 32 9F C4 91 17 06 81 14 F0 06 81 15 F1 C4 91 1A '
'06 81 1B E2 06 81 18 F2 06 81 19 F1 06 81 1E F0 C4 99 1F C4 91 1C 06 81 1D '
'E6 06 81 62 EF 06 81 63 F2 06 81 60 E3 C4 99 61 06 81 66 BC 06 81 67 E6 06 '
'81 64 E8 06 81 65 9D 06 81 6A F2 C4 99 6B 06 81 68 A9 06 81 69 EF 06 81 6E '
'EE 06 81 6F AE 06 81 6C E3 06 81 6D EF 06 81 72 E9 06 81 73 7C'.replace(' ',
''))

如下的代码定义了一个函数, 给定函数一个0x000xFF之间的值, 就能执行原来程序的解码操作.

def decode_bytes(i):
	decoded_bytes = ""
	for byte in ENCODED_BYTES:
		decoded_bytes += chr(((ord(byte) ^ i) + 0x22) & 0xFF)
	return decoded_bytes

接下来, 我们定义一个函数, 函数在给定待解码字节后会利用Unicorn框架来模拟执行校验值函数

def emulate_checksum(decoded_bytes):
	# establish memory addresses for checksum code, stack, and decoded bytes
	address = 0x400000
	stack_addr = 0x410000
	dec_bytes_addr = 0x420000
	# write checksum code and decoded bytes into memory
	mu = Uc(UC_ARCH_X86, UC_MODE_32)
	mu.mem_map(address, 2 * 1024 * 1024)
	mu.mem_write(address, CHECKSUM_CODE)
	mu.mem_write(dec_bytes_addr, decoded_bytes)

如上的代码中初始化了一个32位的x86模拟器, 随后创建了一个用于存储校验函数代码, 函数内部栈以及待解码字节的2MB内存. 校验代码和待解码字节可以写入内存范围的任意地址里.

校验值函数从栈上取两个参数: 待解码字节的起始地址(0x40107C)以及长度(0x79), 下图显示了校验值函数调用后的状态.

fig13.png

为了能让校验值函数能在模拟时正确执行, 我们还需要对栈进行设置来匹配上图的栈空间布局, 并适当填充ESP寄存器. 如下所示, 在模拟执行结束后, 我们可以从emulate_checksum返回计算后的校验值

	# place the address of decoded bytes and size on the stack
	mu.reg_write(UC_X86_REG_ESP, stack_addr)
	mu.mem_write(stack_addr + 4, struct.pack('<I', dec_bytes_addr))
	mu.mem_write(stack_addr + 8, struct.pack('<I', 0x79))
	# emulate and read result in AX
	mu.emu_start(address, address + len(CHECKSUM_CODE))
	checksum = mu.reg_read(UC_X86_REG_AX)
	return checksum

现在到轻松的部分了. 我们暴力穷举异或的key, 解码字节并模拟校验操作, 然后确定哪一个key能获得正确的校验值. 如下所示

for i in range(0, 256):
	decoded_bytes = decode_bytes(i)
	checksum = emulate_checksum(decoded_bytes)
	if checksum == 0xFB5E:
		print 'Checksum matched with byte %X' % i

运行脚本最后打印出正确的单字节值: 0xA2. 然而我们仍然不明白解码后0x40107C处的指令干了什么. 我们来尝试使用Capstone反汇编器来反汇编这些指令, 如下所示

	print 'Decoded bytes disassembly:'
	md = Cs(CS_ARCH_X86, CS_MODE_32)
	for j in md.disasm(decoded_bytes, 0x40107C):
		print "0x%x:\t%s\t%s" % (j.address, j.mnemonic, j.op_str)
	break

运行我们的脚本并提供指令, 结果如下所示

Success with byte A2
Decoded bytes disassembly:
0x40107c: mov bl, 0x65
0x40107e: mov byte ptr [ebp - 0x2b], bl
0x401081: mov byte ptr [ebp - 0x2a], 0x74
0x401085: mov dl, 0x5f
0x401087: mov byte ptr [ebp - 0x29], dl
0x40108a: mov byte ptr [ebp - 0x28], 0x74
0x40108e: mov byte ptr [ebp - 0x27], 0x75
0x401092: mov byte ptr [ebp - 0x26], dl
0x401095: mov byte ptr [ebp - 0x25], 0x62
0x401099: mov byte ptr [ebp - 0x24], 0x72
0x40109d: mov byte ptr [ebp - 0x23], 0x75
0x4010a1: mov byte ptr [ebp - 0x22], 0x74
0x4010a5: mov byte ptr [ebp - 0x21], bl
0x4010a8: mov byte ptr [ebp - 0x20], dl
0x4010ab: mov byte ptr [ebp - 0x1f], 0x66
0x4010af: mov byte ptr [ebp - 0x1e], 0x6f
0x4010b3: mov byte ptr [ebp - 0x1d], 0x72
0x4010b7: mov byte ptr [ebp - 0x1c], 0x63
0x4010bb: mov byte ptr [ebp - 0x1b], bl
0x4010be: mov byte ptr [ebp - 0x1a], 0x40
0x4010c2: mov byte ptr [ebp - 0x19], 0x66
0x4010c6: mov byte ptr [ebp - 0x18], 0x6c
0x4010ca: mov byte ptr [ebp - 0x17], 0x61
0x4010ce: mov byte ptr [ebp - 0x16], 0x72
0x4010d2: mov byte ptr [ebp - 0x15], bl
0x4010d5: mov byte ptr [ebp - 0x14], 0x2d
0x4010d9: mov byte ptr [ebp - 0x13], 0x6f
0x4010dd: mov byte ptr [ebp - 0x12], 0x6e
0x4010e1: mov byte ptr [ebp - 0x11], 0x2e
0x4010e5: mov byte ptr [ebp - 0x10], 0x63
0x4010e9: mov byte ptr [ebp - 0xf], 0x6f
0x4010ed: mov byte ptr [ebp - 0xe], 0x6d
0x4010f1: mov byte ptr [ebp - 0xd], 0

我们可以看出两点, 首先栈上正在填充成一个字符串, 其次, 填充到栈上的常量十六进制值在可显字符范围内(0x20~0x7E). 依据它们在栈上的顺序依次提取出这些可显字符, 或者你可以用调试器观察栈上的内容, 得到题目解答: et_tu_brute_force@flare-on.com.

以下附上python脚本

import binascii
import struct
from unicorn import *
from unicorn.x86_const import *
from capstone import *
CHECKSUM_CODE = binascii.unhexlify(
	'55 8B EC 51 8B 55 0C B9 FF 00 00 00 89 4D FC 85 D2 74 51 53 8B 5D 08 56 57 '
	'6A 14 58 66 8B 7D FC 3B D0 8B F2 0F 47 F0 2B D6 0F B6 03 66 03 F8 66 89 7D '
	'FC 03 4D FC 43 83 EE 01 75 ED 0F B6 45 FC 66 C1 EF 08 66 03 C7 0F B7 C0 89 '
	'45 FC 0F B6 C1 66 C1 E9 08 66 03 C1 0F B7 C8 6A 14 58 85 D2 75 BB 5F 5E 5B '
	'0F B6 55 FC 8B C1 C1 E1 08 25 00 FF 00 00 03 C1 66 8B 4D FC 66 C1 E9 08 66 '
	'03 D1 66 0B C2'.replace(' ', ''))
ENCODED_BYTES = binascii.unhexlify(
	'33 E1 C4 99 11 06 81 16 F0 32 9F C4 91 17 06 81 14 F0 06 81 15 F1 C4 91 1A '
	'06 81 1B E2 06 81 18 F2 06 81 19 F1 06 81 1E F0 C4 99 1F C4 91 1C 06 81 1D '
	'E6 06 81 62 EF 06 81 63 F2 06 81 60 E3 C4 99 61 06 81 66 BC 06 81 67 E6 06 '
	'81 64 E8 06 81 65 9D 06 81 6A F2 C4 99 6B 06 81 68 A9 06 81 69 EF 06 81 6E '
	'EE 06 81 6F AE 06 81 6C E3 06 81 6D EF 06 81 72 E9 06 81 73 7C'.replace(' ',
	''))
def decode_bytes(i):
	decoded_bytes = ""
	for byte in ENCODED_BYTES:
		decoded_bytes += chr(((ord(byte) ^ i) + 0x22) & 0xFF)
	return decoded_bytes
	
def emulate_checksum(decoded_bytes):
	# establish memory addresses for checksum code, stack, and decoded bytes
	address = 0x400000
	stack_addr = 0x410000
	dec_bytes_addr = 0x420000
	# write checksum code and decoded bytes into memory
	mu = Uc(UC_ARCH_X86, UC_MODE_32)
	mu.mem_map(address, 2 * 1024 * 1024)
	mu.mem_write(address, CHECKSUM_CODE)
	mu.mem_write(dec_bytes_addr, decoded_bytes)
	# place the address of decoded bytes and size on the stack
	mu.reg_write(UC_X86_REG_ESP, stack_addr)
	mu.mem_write(stack_addr + 4, struct.pack('<I', dec_bytes_addr))
	mu.mem_write(stack_addr + 8, struct.pack('<I', 0x79))
	# emulate and read result in AX
	mu.emu_start(address, address + len(CHECKSUM_CODE))
	checksum = mu.reg_read(UC_X86_REG_AX)
	return checksum
for i in range(0, 256):
	decoded_bytes = decode_bytes(i)
	checksum = emulate_checksum(decoded_bytes)
	if checksum == 0xFB5E:
		print 'Checksum matched with byte %X' % i
		print 'Decoded bytes disassembly:'
		md = Cs(CS_ARCH_X86, CS_MODE_32)
		for j in md.disasm(decoded_bytes, 0x40107C):
			print "0x%x:\t%s\t%s" % (j.address, j.mnemonic, j.op_str)
		break

Previous     Next