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

标签 python debugging assembly reverse-engineering self-modifying

问题:

我制作了一个 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'。正是在修改字节的地方出现了 1 个错误。这不会是巧合,对吧?

有人知道为什么这两个脚本都不起作用吗?谢谢。

最佳答案

radare2 没有问题,但您对程序的分析不正确,因此您编写的代码无法正确处理此 RE。

让我们开始

When i == 10, c = 0. That's because it's the index of the byte that is modified during execution.

部分正确。它在开始时设置为零,但在每一轮之后都有这段代码:

xor al, byte [esi]                               
or byte [ebx + 0xa], al

那么让我们了解这里发生了什么。 al 是当前计算的标志字符,esi 指向作为参数输入的 FLAG,在 [ebx + 0xa] 我们当前有 0(在开头设置),因此索引 0xa 处的 char 只有在计算出的标志 char 等于参数中的 char 时才会保持为零,并且因为您正在使用假标志运行 r2,这从第 6 个字符开始成为问题,但您在索引 10 处的第一个 � 处看到了这个问题。为了缓解这个问题,我们需要稍微更新您的脚本。

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

我们在这里所做的是,在断点被击中后,我们读取计算出的标志字符,我们进一步移动两条指令(到达 0x01002054),然后我们设置 al0x0 以模拟我们在 [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(十进制)= 71(十六进制)。但是这个 51(hex) 看起来很奇怪?我们以前不是在某处看到过吗?是的,它是您设置断点以读取 al 值的偏移量。

这是打破你的第二个字符的代码

r2.cmd('db 0x01002051')

是的 - 你的断点。您正在设置在该地址处中断,软断点将 0xcc 放入内存地址,因此当读取第 18 个字符的第 3 个字节的操作码命中该点时,它不会得到 0x5b (原始值)它得到 0xcc。因此,要解决这个问题,我们需要更正该计算。在这里可能可以用更聪明/更优雅的方式完成,但我选择了一个简单的解决方案,所以我这样做了:

if i == 17:
  c -= (0xcc-0x5b)

Just subtract was 是通过在代码中放置断点而无意中添加的。

最终代码:

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)

打印正确的标志:

FLAG{Wh3n0ptiMizaTioNGOesT00F4r}

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

关于python - 调试器和 cpu 仿真器不检测自修改代码,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44828937/

相关文章:

c - 微 Controller 编程

windows - 我可以在不使用寄存器的情况下在 .text 区域实现计数器吗?

python - 为什么这个 Regexp 使用 pcre 而不是 Python 的步骤减少了 99.89%?

python - keras 加载模型错误尝试将包含 17 层的权重文件加载到具有 0 层的模型中

python - 应用程序启动失败,因为它的并行配置不正确 (Python/Pyinstaller/Tkinter)

javascript - 当我点击网页中的某个元素时,如何监控正在触发的 JavaScript?

c - 如果我要用汇编编写程序,这个 HelloWorld 汇编代码的哪些部分是必不可少的?

python - 在Python中删除字符串中每个元素的第一项

c - Execl 返回错误地址

python - 什么决定了调试器的运行时性能