Problem:
我制作了一个自我修改其字节之一的elf可执行文件 . 它只是将1更改为1.当我正常运行可执行文件时,我可以看到更改是成功的,因为它完全按预期运行(更多的是进一步向下) . 调试时出现问题:调试器(使用radare2)在查看修改后的字节时返回错误的值 .
Context:
我做了一个逆向工程挑战,受到Smallest elf的启发 . 你可以看到"source code"(如果你甚至可以称之为):https://pastebin.com/Yr1nFX8W .
汇编和执行:
nasm -f bin -o tinyelf tinyelf.asm
chmod +x tinyelf
./tinyelf [flag]
如果标志是正确的,则返回0.任何其他值表示您的答案是错误的 .
./tinyelf FLAG{wrong-flag}; echo $?
...输出“255” .
!Solution SPOILERS!
可以静态反转它 . 完成后,您会发现通过执行此计算可以找到标志中的每个字符:
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 {} Wh3n0tiMizaioNGOesT00F4r
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
请注意索引10:'o'而不是'p' . 这是一个错误的错误,修改字节的确切位置 . 这不是巧合,对吧?
任何人都知道为什么这两个脚本都不起作用?谢谢 .
1 回答
radare2没有问题,但您对程序的分析不正确,因此您编写的代码错误地处理了此RE .
让我们开始吧
这是部分正确的 . 它在开始时设置为零,但在每轮之后都有以下代码:
所以让's understand what'发生在这里 .
al
是当前计算的标志字符,esi
指向作为参数输入的FLAG,而[ebx + 0xa]
我们当前有0(设置在开头),因此索引0xa
处的字符仅在计算出的标记字符为char时保持为零等于参数中的那个,因为你正在运行带有假标志的r2,这从第6个字符开始出现问题,但是你在第一个see在索引10看到的结果 . 为了减轻我们需要更新你的脚本一点点 .我们在这里做的是,在brekpoint被击中并且我们读取计算的标志字符后,我们进一步移动两个指令(达到
0x01002054
)然后我们将al
设置为0x0
以模拟我们在[esi]的字符实际上与计算一个(所以xor
将在这种情况下返回0
) . 通过这样做,我们将0xa
的值保持为零 .现在是第二个角色 . 这个RE很棘手;) - 它会自己读取,如果你忘记了这一点,你最终可能会遇到这样的情况 . 让我们试着分析为什么这个角色不对了 . 它是标志的第18个字符(因此我们从0开始索引是17)如果我们检查从二进制读取的字符索引的公式,我们注意到索引是:
17(dec) = 11(hex)
,17 + 32 = 49(dec) = 31(hex)
,17 + 64 = 81(dec) = 51(hex)
,17 + 96 = 113(dec) = 71(hex)
. 但这个51(hex)
看起来很奇怪吗? Didn 't we see that somewhere before? Yup, it'是设置断点以读取al
值的偏移量 .这是破坏你的第二个字符的代码
是的 - 你的断点 . 你设置为在该地址处断开,并且软断点将
0xcc
放在内存地址中,因此当读取第18个字符的第3个字节的操作码命中该点时,它得不到0x5b
(原始值),它得到0xcc
. 所以要解决这个问题,我们需要纠正这个计算 . 这可能是以更智能/更优雅的方式完成的,但我选择了一个简单的解决方案,所以我只是这样做:只是通过在代码中放置一个断点来无意中添加了减去 .
最终代码:
打印正确的标志:
至于Unicorn你没有设置断点,所以问题2消失了,而第10个索引的1分是由于与r2相同的原因 .