如何将汇编代码翻译回 C?
How To Translate Assembly Code Back Into C?
我看到了下面的汇编代码(注意是我自己加的注释):
00000000004005f0 <check_password>:
4005f0: 31 c0 xor %eax,%eax # i=0
4005f2: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) # ignore
4005f8: 0f b6 90 c0 1b 40 00 movzbl 0x401bc0(%rax),%edx# edx=foo[i]
4005ff: 83 f2 5f xor [=10=]x5f,%edx
400602: 38 14 07 cmp %dl,(%rdi,%rax,1) # cmp prev result with s[i]
400605: 75 14 jne 40061b <check_password+0x2b> # failed return 0
400607: 48 83 c0 01 add [=10=]x1,%rax # i++
40060b: 48 83 f8 0a cmp [=10=]xa,%rax # if (i==10)
40060f: 75 e7 jne 4005f8 <check_password+0x8> # run again
400611: 31 c0 xor %eax,%eax
400613: 80 7f 0a 00 cmpb [=10=]x0,0xa(%rdi)
400617: 0f 94 c0 sete %al
40061a: c3 retq
40061b: 31 c0 xor %eax,%eax
40061d: c3 retq
并想编写等效的 C 代码来填充此内容:
编辑 1:
int check_password (char *s)
{
for (int i=0; i!=10 ; i++)
{
if (foo[i] ^ 0x5f != s[i])
return 0;
}
return 0==s[10]; // Isn't this strange? input is of size 10...
}
编辑 2:
int check_password (char *s)
{
for (int i=0; i!=10 ; i++)
{
if (foo[i] ^ 0x5f != s[i])
return 0==s[10]; // Isn't this strange? input is of size 10...
}
return 1;
}
我觉得太接近了,但有点卡住了,版本 1 是正确的还是版本 2 或其中的 none。
另外,这里正确的翻译应该是什么?
一些重要的事情要知道:
我有以下名为 foo 的字符数组,它位于内存中的地址 401bc0
。
char foo[10] = { 0x33, 0x3d, ...... }; // Not all values are shown
从过去做这种事情的经验来看,一个循序渐进的过程是有序的。所以首先像 fortran 一样写它:
check_password(char *rdi) {
int eax = 0;
char *str = (char *)0x401bc0;
loop:
char dl = str[eax];
dl ^= 0x5f;
if (dl == rdi[eax]) {
eax++;
if (eax == 10) {
return rdi[10] == 0;
}
goto loop;
}
return 0;
}
在进一步学习 C 语言之前:
char *str = (char *)0x401bc0;
int check_password(char *rdi) {
int i;
for (i = 0; i < 10 && rdi[i] == str[i]^0x5f; i++) {
}
return i == 10 && rdi[i] == 0;
}
这不是非常重要,但是随着 asm 的分支越来越多,C-goto 是你的朋友;减少它后,检测结构化模式通常很简单。
在一个例子中,我将一个 900 行没人理解的汇编块提炼成 80 行 C。然后它传播到 6 个架构,每个架构都有 10 - 30% 的减速,并且所有这些都在最长的中断关闭时间内内核。
您是否查看了实际的编译器输出以查看 GCC 是否使用您的任一版本重建了相同的 asm? https://godbolt.org/z/ed86jG9rf 显示几乎正确和错误的版本。 (我故意不在这里说哪个是正确的,所以你必须自己去寻找,尽管 cmp/sete 在失败路径或不匹配路径中的区别对我来说似乎很明显。另外,你看到任何无条件 returns 1
的汇编吗?0
怎么样?)
请注意,在您修复运算符优先级错误后,其中一个会使用 GCC5.4 编译回您的 asm:foo[i] ^ 0x5f != s[i]
表示 foo[i] ^ (0x5f != s[i])
,即与布尔异或,因此 if根据 XOR 结果的低位,将是 true/false。你居然(pass[i] ^ 0xf5) != s[i]
比较异或结果
我只是从 asm 中注意到这一点很奇怪(循环中有 cmp / setne ),所以我打开了 -Wall
并且 GCC 告诉我发生了什么上。
<source>:19:28: warning: suggest parentheses around comparison in operand of '^' [-Wparentheses]
if (pass[i] ^ 0x5f != s[i])
^
我们有很好的C编译工具;在出于任何原因编写 C 时利用它们,包括从 asm 进行逆向工程。特别是对于您知道是编译器生成的代码,尤其是当您可以猜出编译器时。具有 System V 调用约定(RDI 中的 arg)的 x86-64 通常是 GCC 或 clang。在这种情况下,如果您将 -fno-unroll-loops
与 clang 一起使用,它们都会产生非常相似的 asm。
return 0==s[10]; // Isn't this strange? input is of size 10...
对我来说似乎很正常(但效率低下):显然静态 const char pass[]
没有以 0 ^ 0x5f
终止符结尾。否则,他们可以让循环 运行 再进行一次迭代,以检查输入的隐式长度 C 字符串函数 arg 在其 10 个匹配字节后是否具有 0 终止符。
此最终检查将拒绝在前 10 个字节后有尾随垃圾但匹配到该点的密码。
所以这意味着它期望 strlen(s) == 10
,即 char [11]
包含 10 个字节加上终止符 0。
我看到了下面的汇编代码(注意是我自己加的注释):
00000000004005f0 <check_password>:
4005f0: 31 c0 xor %eax,%eax # i=0
4005f2: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) # ignore
4005f8: 0f b6 90 c0 1b 40 00 movzbl 0x401bc0(%rax),%edx# edx=foo[i]
4005ff: 83 f2 5f xor [=10=]x5f,%edx
400602: 38 14 07 cmp %dl,(%rdi,%rax,1) # cmp prev result with s[i]
400605: 75 14 jne 40061b <check_password+0x2b> # failed return 0
400607: 48 83 c0 01 add [=10=]x1,%rax # i++
40060b: 48 83 f8 0a cmp [=10=]xa,%rax # if (i==10)
40060f: 75 e7 jne 4005f8 <check_password+0x8> # run again
400611: 31 c0 xor %eax,%eax
400613: 80 7f 0a 00 cmpb [=10=]x0,0xa(%rdi)
400617: 0f 94 c0 sete %al
40061a: c3 retq
40061b: 31 c0 xor %eax,%eax
40061d: c3 retq
并想编写等效的 C 代码来填充此内容:
编辑 1:
int check_password (char *s)
{
for (int i=0; i!=10 ; i++)
{
if (foo[i] ^ 0x5f != s[i])
return 0;
}
return 0==s[10]; // Isn't this strange? input is of size 10...
}
编辑 2:
int check_password (char *s)
{
for (int i=0; i!=10 ; i++)
{
if (foo[i] ^ 0x5f != s[i])
return 0==s[10]; // Isn't this strange? input is of size 10...
}
return 1;
}
我觉得太接近了,但有点卡住了,版本 1 是正确的还是版本 2 或其中的 none。 另外,这里正确的翻译应该是什么?
一些重要的事情要知道:
我有以下名为 foo 的字符数组,它位于内存中的地址 401bc0
。
char foo[10] = { 0x33, 0x3d, ...... }; // Not all values are shown
从过去做这种事情的经验来看,一个循序渐进的过程是有序的。所以首先像 fortran 一样写它:
check_password(char *rdi) {
int eax = 0;
char *str = (char *)0x401bc0;
loop:
char dl = str[eax];
dl ^= 0x5f;
if (dl == rdi[eax]) {
eax++;
if (eax == 10) {
return rdi[10] == 0;
}
goto loop;
}
return 0;
}
在进一步学习 C 语言之前:
char *str = (char *)0x401bc0;
int check_password(char *rdi) {
int i;
for (i = 0; i < 10 && rdi[i] == str[i]^0x5f; i++) {
}
return i == 10 && rdi[i] == 0;
}
这不是非常重要,但是随着 asm 的分支越来越多,C-goto 是你的朋友;减少它后,检测结构化模式通常很简单。 在一个例子中,我将一个 900 行没人理解的汇编块提炼成 80 行 C。然后它传播到 6 个架构,每个架构都有 10 - 30% 的减速,并且所有这些都在最长的中断关闭时间内内核。
您是否查看了实际的编译器输出以查看 GCC 是否使用您的任一版本重建了相同的 asm? https://godbolt.org/z/ed86jG9rf 显示几乎正确和错误的版本。 (我故意不在这里说哪个是正确的,所以你必须自己去寻找,尽管 cmp/sete 在失败路径或不匹配路径中的区别对我来说似乎很明显。另外,你看到任何无条件 returns 1
的汇编吗?0
怎么样?)
请注意,在您修复运算符优先级错误后,其中一个会使用 GCC5.4 编译回您的 asm:foo[i] ^ 0x5f != s[i]
表示 foo[i] ^ (0x5f != s[i])
,即与布尔异或,因此 if根据 XOR 结果的低位,将是 true/false。你居然(pass[i] ^ 0xf5) != s[i]
比较异或结果
我只是从 asm 中注意到这一点很奇怪(循环中有 cmp / setne ),所以我打开了 -Wall
并且 GCC 告诉我发生了什么上。
<source>:19:28: warning: suggest parentheses around comparison in operand of '^' [-Wparentheses]
if (pass[i] ^ 0x5f != s[i])
^
我们有很好的C编译工具;在出于任何原因编写 C 时利用它们,包括从 asm 进行逆向工程。特别是对于您知道是编译器生成的代码,尤其是当您可以猜出编译器时。具有 System V 调用约定(RDI 中的 arg)的 x86-64 通常是 GCC 或 clang。在这种情况下,如果您将 -fno-unroll-loops
与 clang 一起使用,它们都会产生非常相似的 asm。
return 0==s[10]; // Isn't this strange? input is of size 10...
对我来说似乎很正常(但效率低下):显然静态 const char pass[]
没有以 0 ^ 0x5f
终止符结尾。否则,他们可以让循环 运行 再进行一次迭代,以检查输入的隐式长度 C 字符串函数 arg 在其 10 个匹配字节后是否具有 0 终止符。
此最终检查将拒绝在前 10 个字节后有尾随垃圾但匹配到该点的密码。
所以这意味着它期望 strlen(s) == 10
,即 char [11]
包含 10 个字节加上终止符 0。