【发布时间】:2017-12-03 09:06:27
【问题描述】:
问题:
我制作了一个 elf 可执行文件,它可以自行修改它的一个字节。它只是将 0 更改为 1。当我正常运行可执行文件时,我可以看到更改是成功的,因为它完全按预期运行(更多信息会在下面进一步介绍)。调试时出现问题:调试器(使用radare2)查看修改后的字节返回错误值。
上下文:
在Smallest elf 的启发下,我提出了逆向工程挑战。你可以在那里看到“源代码”(如果你甚至可以这样称呼它):https://pastebin.com/Yr1nFX8W。
组装和执行:
nasm -f bin -o tinyelf tinyelf.asm
chmod +x tinyelf
./tinyelf [flag]
如果标志是正确的,它返回 0。任何其他值都意味着你的答案是错误的。
./tinyelf FLAG{wrong-flag}; echo $?
...输出“255”。
!解决方案剧透!
可以静态反转它。完成后,您会发现标志中的每个字符都是通过以下计算找到的:
flag[i] = b[i] + b[i+32] + b[i+64] + b[i+96];
...其中 i 是字符的索引,b 是可执行文件本身的字节。这是一个无需调试器即可解决挑战的 c 脚本:
#include <stdio.h>
int main()
{
char buffer[128];
FILE* fp;
fp = fopen("tinyelf", "r");
fread(buffer, 128, 1, fp);
int i;
char c = 0;
for (i = 0; i < 32; i++) {
c = buffer[i];
// handle self-modifying code
if (i == 10) {
c = 0;
}
c += buffer[i+32] + buffer[i+64] + buffer[i+96];
printf("%c", c);
}
printf("\n");
}
您可以看到我的求解器处理了一种特殊情况:当 i == 10 时,c = 0。这是因为它是在执行期间修改的字节的索引。运行求解器并用它调用 tinyelf 我得到:
FLAG{Wh3n0ptiMizaTioNGOesT00F4r}
./tinyelf FLAG{Wh3n0ptiMizaTioNGOesT00F4r} ; echo $?
输出:0。成功!
太好了,现在让我们尝试动态解决它,使用 python 和radare2:
import r2pipe
r2 = r2pipe.open('./tinyelf')
r2.cmd('doo FLAG{AAAAAAAAAAAAAAAAAAAAAAAAAA}')
r2.cmd('db 0x01002051')
flag = ''
for i in range(0, 32):
r2.cmd('dc')
eax = r2.cmd('dr? al')
c = int(eax, 16)
flag += chr(c)
print('\n\n' + flag)
它在命令上设置一个断点,将输入字符与预期字符进行比较,然后得到与 (al) 比较的内容。这应该有效。然而,这里是输出:
FLAG{Wh3n0�tiMiza�ioNGOesT00F4r}
2 个不正确的值,其中一个位于索引 10(修改后的字节)处。奇怪,也许是radare2的错误?接下来我们试试unicorn(一个cpu模拟器):
from unicorn import *
from unicorn.x86_const import *
from pwn import *
ADDRESS = 0x01002000
mu = Uc(UC_ARCH_X86, UC_MODE_32)
code = bytearray(open('./tinyelf').read())
mu.mem_map(ADDRESS, 20 * 1024 * 1024)
mu.mem_write(ADDRESS, str(code))
mu.reg_write(UC_X86_REG_ESP, ADDRESS + 0x2000)
mu.reg_write(UC_X86_REG_EBP, ADDRESS + 0x2000)
mu.mem_write(ADDRESS + 0x2000, p32(2)) # argc
mu.mem_write(ADDRESS + 0x2000 + 4, p32(ADDRESS + 0x5000)) # argv[0]
mu.mem_write(ADDRESS + 0x2000 + 8, p32(ADDRESS + 0x5000)) # argv[1]
mu.mem_write(ADDRESS + 0x5000, "x" * 32)
flag = ''
def hook_code(uc, address, size, user_data):
global flag
eip = uc.reg_read(UC_X86_REG_EIP)
if eip == 0x01002051:
c = uc.reg_read(UC_X86_REG_EAX) & 0x7f
#print(str(c) + " " + chr(c))
flag += chr(c)
mu.hook_add(UC_HOOK_CODE, hook_code)
try:
mu.emu_start(0x01002004, ADDRESS + len(code))
except Exception:
print flag
这一次求解器输出:FLAG{Wh3n0otiMizaTioNGOesT00F4r}
注意索引 10:'o' 而不是 'p'。正是在字节被修改的地方,这是一个错误的错误。这不会是巧合吧?
任何人都知道为什么这两个脚本都不起作用?谢谢。
【问题讨论】:
-
这里可能涉及太多复杂性。由于我还没有完全阅读您的问题,所以我只给出两分钱:如果您有理由相信您的工具不能很好地处理 SMC,为什么不使用一个非常简单的示例来明确地测试呢?例如,您可以通过翻转一个位将
jz转换为jnz。这将以可测量的方式转移流量。 -
这两个工具过去都与其他自修改代码一起工作,因此它们通常能够处理 SMC。我不知道为什么他们不能在这种情况下。
-
确保在进行自修改的代码和修改后的代码之间有控制转移指令。英特尔要求这样做以保证能够识别修改后的代码。
-
任何 CTI 工作?在执行修改后的代码之前,我会执行很多 jmp,所以我认为这不是问题......不过感谢您的建议!
-
较旧的 x86 CPU 有一个预取缓冲区,指令在执行之前被提取到该缓冲区中。任何更改 (E)IP 的控制传输指令都会清除预取缓冲区并开始用新的指令集填充它。所以自修改代码通常会使用 JMP 指令(通常是
jmp $+2)来清除缓冲区。现代 CPU 有更复杂的管道,需要特殊的硬件来检测自修改代码,但英特尔只保证在遵循相同的旧规则时它可以工作。
标签: python debugging assembly reverse-engineering self-modifying