调试器和 cpu 仿真器未检测到自修改代码
Debugger and cpu emulator don't detect self-modified code
问题:
我制作了一个 elf 可执行文件,它可以自行修改其中一个字节。它只是将 0 更改为 1。当我正常 运行 可执行文件时,我可以看到更改是成功的,因为它 运行 完全符合预期(更多内容在下方)。调试时出现问题:调试器(使用radare2)returns查看修改后的字节时的错误值。
上下文:
受 Smallest elf. You can see the "source code" (if you can even call it that) there: https://pastebin.com/Yr1nFX8W 启发,我进行了逆向工程挑战。
到assemble执行:
nasm -f bin -o tinyelf tinyelf.asm
chmod +x tinyelf
./tinyelf [flag]
如果标志是正确的,它 returns 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
指向作为参数输入的标志,在 [ebx + 0xa]
处我们当前有 0(设置在开头),因此字符只有当计算出的标志 char 等于参数中的一个时,索引 0xa
才会保持零,并且由于你是 运行 r2 带有假标志,这从第 6 个字符开始成为问题但结果您在索引 10 处的第一个 � 看到了这一点。为了减轻这种情况,我们需要稍微更新您的脚本。
eax = r2.cmd('dr? al')
c = int(eax, 16)
r2.cmd("ds 2")
r2.cmd("dr al = 0x0")
我们在这里做的是,在断点被击中后,我们读取计算的标志字符,我们进一步移动两条指令(到达 0x01002054
),然后我们将 al
设置为 0x0
来模拟我们在 [esi] 处的字符实际上与计算的字符相同(因此 xor
将 return 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
。因此,要解决这个问题,我们需要更正该计算。这里可能可以用 smarter/more 优雅的方式完成,但我寻求一个简单的解决方案,所以我这样做了:
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 个索引上的 off-by-1 是由于与 r2 相同的原因。
问题:
我制作了一个 elf 可执行文件,它可以自行修改其中一个字节。它只是将 0 更改为 1。当我正常 运行 可执行文件时,我可以看到更改是成功的,因为它 运行 完全符合预期(更多内容在下方)。调试时出现问题:调试器(使用radare2)returns查看修改后的字节时的错误值。
上下文:
受 Smallest elf. You can see the "source code" (if you can even call it that) there: https://pastebin.com/Yr1nFX8W 启发,我进行了逆向工程挑战。
到assemble执行:
nasm -f bin -o tinyelf tinyelf.asm
chmod +x tinyelf
./tinyelf [flag]
如果标志是正确的,它 returns 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
指向作为参数输入的标志,在 [ebx + 0xa]
处我们当前有 0(设置在开头),因此字符只有当计算出的标志 char 等于参数中的一个时,索引 0xa
才会保持零,并且由于你是 运行 r2 带有假标志,这从第 6 个字符开始成为问题但结果您在索引 10 处的第一个 � 看到了这一点。为了减轻这种情况,我们需要稍微更新您的脚本。
eax = r2.cmd('dr? al')
c = int(eax, 16)
r2.cmd("ds 2")
r2.cmd("dr al = 0x0")
我们在这里做的是,在断点被击中后,我们读取计算的标志字符,我们进一步移动两条指令(到达 0x01002054
),然后我们将 al
设置为 0x0
来模拟我们在 [esi] 处的字符实际上与计算的字符相同(因此 xor
将 return 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
。因此,要解决这个问题,我们需要更正该计算。这里可能可以用 smarter/more 优雅的方式完成,但我寻求一个简单的解决方案,所以我这样做了:
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 个索引上的 off-by-1 是由于与 r2 相同的原因。