调试器和cpu仿真器不检测自修改代码

MyU*_*358 2 python debugging assembly reverse-engineering self-modifying

问题:

我制作了一个自我修改其字节之一的elf可执行文件.它只是将1更改为1.当我正常运行可执行文件时,我可以看到更改成功,因为它完全按预期运行(更多关于更进一步下来).调试时出现问题:调试器(使用radare2)在查看修改后的字节时返回错误的值.

语境:

我做了一个逆向工程挑战,受到了最小精灵的启发.您可以在那里看到"源代码"(如果您甚至可以称之为):https://pastebin.com/Yr1nFX8W.

汇编和执行:

nasm -f bin -o tinyelf tinyelf.asm
chmod +x tinyelf
./tinyelf [flag]
Run Code Online (Sandbox Code Playgroud)

如果标志是正确的,则返回0.任何其他值表示您的答案是错误的.

./tinyelf FLAG{wrong-flag}; echo $?
Run Code Online (Sandbox Code Playgroud)

...输出"255".

!解决方案SPOILERS!

可以静态反转它.完成后,您会发现通过执行此计算可以找到标志中的每个字符:

flag[i] = b[i] + b[i+32] + b[i+64] + b[i+96];
Run Code Online (Sandbox Code Playgroud)

...其中i是字符的索引,b是可执行文件本身的字节.这是一个ac脚本,可以在没有调试器的情况下解决问题:

#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");
}
Run Code Online (Sandbox Code Playgroud)

您可以看到我的求解器处理特殊情况:当i == 10时,c = 0.这是因为它是执行期间修改的字节的索引.运行求解器并用它调用tinyelf我得到:

FLAG{Wh3n0ptiMizaTioNGOesT00F4r}
./tinyelf FLAG{Wh3n0ptiMizaTioNGOesT00F4r} ; echo $?
Run Code Online (Sandbox Code Playgroud)

输出: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)
Run Code Online (Sandbox Code Playgroud)

它在命令上放置一个断点,用于将输入字符与预期字符进行比较,然后得到与(al)相比较的内容.这应该工作.然而,这是输出:

FLAG {} Wh3n0tiMizaioNGOesT00F4r

2个不正确的值,其中一个位于索引10(修改后的字节).很奇怪,也许是radare2的错误?让我们下一步尝试独角兽(一个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
Run Code Online (Sandbox Code Playgroud)

这次求解器输出:FLAG {Wh3n0otiMizaTioNGOesT00F4r}

请注意索引10:'o'而不是'p'.这是一个错误的错误,字节被修改.这不是巧合,对吧?

任何人都知道为什么这两个脚本都不起作用?谢谢.

Paw*_*sik 6

radare2没有问题,但您对程序的分析不正确,因此您编写的代码错误地处理了此RE.

让我们开始吧

当i == 10时,c = 0.这是因为它是执行期间修改的字节的索引.

这是部分正确的.它在开始时设置为零,但在每轮之后都有以下代码:

xor al, byte [esi]                               
or byte [ebx + 0xa], al
Run Code Online (Sandbox Code Playgroud)

所以,让我们了解这里发生了什么.al是当前计算的标志字符,并esi指向作为参数输入的FLAG,并且[ebx + 0xa]我们当前有0(在开头设置),因此0xa仅当计算的标志字符char等于时,索引处的字符将保持为零一个在参数中,因为你正在运行带有假标志的r2,这从第6个字符开始出现问题,但是你在第一个see在索引10看到的结果.为了减轻我们需要更新你的脚本一点点.

eax = r2.cmd('dr? al')
c = int(eax, 16)
r2.cmd("ds 2")
r2.cmd("dr al = 0x0")
Run Code Online (Sandbox Code Playgroud)

我们在这里做的是,brekpoint被击中,我们读到计算的标志字符后,我们提出两项指令还(到达0x01002054),然后我们设置al0x0模仿我们在[ESI]焦炭竟是一样的计算的一个(所以xor0在这种情况下返回).通过这样做,我们保持价值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')
Run Code Online (Sandbox Code Playgroud)

是的 - 你的断点.你设置为在该地址处断开,并且软断点正在将0xcc内存地址放入存储器地址中,因此当读取第18个字符的第3个字节的操作码击中它所获得的那个点时0x5b(原始值)0xcc.所以要解决这个问题,我们需要纠正这个计算.这可能是以更智能/更优雅的方式完成的,但我选择了一个简单的解决方案,所以我只是这样做:

if i == 17:
  c -= (0xcc-0x5b)
Run Code Online (Sandbox Code Playgroud)

只是通过在代码中放置一个断点来无意中添加了减去.

最终代码:

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)
Run Code Online (Sandbox Code Playgroud)

打印正确的标志:

FLAG {} Wh3n0ptiMizaTioNGOesT00F4r

至于Unicorn你没有设置断点,所以问题2消失了,而第10个索引的1分是由于与r2相同的原因.