问题:
我做了一个精灵的可执行文件自我修改它的一个字节。当我正常运行可执行文件时,我可以看到更改是成功的,因为它的运行与预期完全相同(更详细的介绍)。调试时会出现问题:调试器(使用radare2)在查看修改后的字节时返回错误的值。
上下文:
我在最小精灵的启发下,做了一个逆向工程挑战。您可以在那里看到“源代码”(如果您甚至可以这样称呼它):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];...where 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");
}您可以看到,我的求解器处理一个特殊情况:当我的== 10,c= 0。这是因为它是在执行过程中修改的字节的索引。运行求解器,用它调用自己,我得到:
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)的比较结果。这应该能行。然而,这是输出:
标志{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这一次,求解器输出:标志{Wh3n0otiMizaTioNGOesT00F4r}
注意在索引10处:'o‘而不是'p’。在字节被修改的地方,这是一个1的错误。那不可能是巧合对吧?
有人知道为什么这两种脚本都不能工作吗?谢谢。
发布于 2017-06-30 23:19:16
radare2没有问题,但是您对程序的分析是不正确的,因此您编写的代码不正确地处理这个RE。
让我们从
当i为== 10时,c= 0。这是因为它是在执行过程中修改的字节的索引。
这部分是对的。它在开始时被设置为零,但是在每一轮之后都有这样的代码:
xor al, byte [esi]
or byte [ebx + 0xa], al所以让我们了解一下这里发生了什么。al是当前计算出的标志的字符,esi指向作为参数输入的标志,在[ebx + 0xa],我们现在有0(设置在开头),所以索引0xa的字符只有在计算的标志char等于参数中的字符时才会保持为零,并且由于您使用假标志运行r2,所以从第6个字符开始出现问题,但是在索引10的第一个�上会看到这个问题的结果。为了减轻我们需要稍微更新脚本的问题。
eax = r2.cmd('dr? al')
c = int(eax, 16)
r2.cmd("ds 2")
r2.cmd("dr al = 0x0")我们在这里所做的是,在击中brekpoint之后,我们读取计算出的标志char,我们进一步移动两个指令(以到达0x01002054),然后将al设置为0x0,以模拟我们在esi上的字符实际上与计算的字符相同(因此在这种情况下xor将返回0 )。通过这样做,我们将0xa的值保持为零。
现在是第二个角色。这个RE是很棘手的;) -它自己读,如果你忘记了这一点,你可能会以这样的情况结束。让我们试着分析一下为什么这个角色会消失。它是标志的第18个字符(所以索引是17,因为我们从0开始),如果我们检查从二进制读取的字符索引公式,我们注意到索引是:17(dec) = 11(hex)、17 + 32 = 49(dec) = 31(hex)、17 + 64 = 81(dec) = 51(hex)、17 + 96 = 113(dec) = 71(hex)。但是这个51(hex)看起来很熟悉吗?我们以前没在什么地方看到过吗?是的,它是设置断点以读取al值的偏移量。
这是破坏你的第二个字符的代码
r2.cmd('db 0x01002051')
是的-你的断点。您正在设置在该地址中断,一个软断点是在内存地址中放置一个0xcc,因此当读取第18个字符的第3个字节的操作码击中该位置时,它将不会得到0x5b (原始值),它将得到0xcc。因此,为了解决这个问题,我们需要修正这个计算。在这里,它可能可以以更聪明/更优雅的方式完成,但是我找到了一个简单的解决方案,所以我就这样做了:
if i == 17:
c -= (0xcc-0x5b)在代码中添加断点是无意中添加的。
最后代码:
import r2pipe
r2 = r2pipe.open('./tinyelf')
print r2
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)
if i == 17:
c -= (0xcc-0x5b)
r2.cmd("ds 2")
r2.cmd("dr al = 0x0")
flag += chr(c)
print('\n\n' + flag)它打印正确的标志:
标志{Wh3n0ptiMizaTioNGOesT00F4r}
至于Unicorn,您没有设置断点,所以问题2就消失了,第10个索引上的1的差是由于与r2相同的原因。
https://stackoverflow.com/questions/44828937
复制相似问题