首页 文章

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

提问于
浏览
2

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 回答

  • 6

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

    让我们开始吧

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

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

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

    所以让's understand what'发生在这里 . 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")
    

    我们在这里做的是,在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 值的偏移量 .

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

    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)
    

    打印正确的标志:

    FLAG

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

相关问题