【reverse】通俗易懂的gcc内联汇编入门+示例:实现花指令
本帖最后由 hans7 于 2024-5-22 13:14 编辑### 引言
基于Visual Studio的内联汇编教程已然不少,且质量较好。但基于gcc/g++的内联汇编教程少得可怜,且即使是英文文档也……真是一把辛酸泪!但是看到本文的你们,就不必感受那些辛酸了,只需在5分钟后感叹一句:原来这么简单!因为我也是萌新,可能本文有诸多谬误,还请指出。
### 依赖
- Windows10:mingw64
- Ubuntu20.04:g++8.4.0
本文juejin:https://juejin.cn/post/7149765832665464869/
本文52pojie:https://www.52pojie.cn/thread-1695068-1-1.html
本文csdn:https://blog.csdn.net/hans774882968/article/details/127141703
**作者:(https://blog.csdn.net/hans774882968)以及(https://juejin.cn/user/1464964842528888)以及(https://www.52pojie.cn/home.php?mod=space&uid=1906177)**
### Hello world
```cpp
int a = 10, b;
asm(
"movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) // output
:"r"(a) // input
:"%eax" // clobbered register
);
```
也许初看此代码的你们和我有着一样的感受:
1. 不习惯AT&T语法。
2. 这离谱的冒号写法是什么?
首先解决第二个问题。
1. 第一个冒号表示输出Operands。这个例子中是b变量。输出Operands的字符串的第一个字符应该是`'='`。
2. 第二个冒号表示输入Operands。这个例子中是a变量。
3. 第三个冒号表示clobbered register,告诉gcc你的内联汇编需要使用一些寄存器,编译器应该在执行内联汇编之前将所有活动数据移出该寄存器。指定clobbered register往往是必要的,不指定的话会造成bug。
4. 第四个冒号(这个例子没用到,Demo2用到了)用于`asm goto`,主要用于实现汇编的跳转。
更具体准确的解释,可以看参考链接1。
接着解决第一个问题。不习惯AT&T,要改用intel风格:
1. 代码加一行`".intel_syntax noprefix\n"`。
2. g++编译参数新加一项:`-masm=intel`。
```cpp
asm(
".intel_syntax noprefix\n"
"...\n"
);
```
那么编译命令如下:
```bash
g++ -masm=intel g++_asm_hw.cpp -o g++_asm_hw.exe # Windows
g++ -masm=intel g++_asm_hw.cpp -o g++_asm_hw # Ubuntu
```
最后,我们只需要通过一些简单的实操,加深印象。
### Demo1:读取函数若干个字节的数据
我们写一段代码,来读取main函数若干个字节的数据。源码如下(Windows和Ubuntu下一致):
```cpp
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)
const int N = 105;
void dbg() {
puts ("");
}
template<typename T, typename... R>void dbg (const T &f, const R &... r) {
cout << f << " ";
dbg (r...);
}
template<typename Type>inline void read (Type &xx) {
Type f = 1;
char ch;
xx = 0;
for (ch = getchar(); ch < '0' || ch > '9'; ch = getchar() ) if (ch == '-') f = -1;
for (; ch >= '0' && ch <= '9'; ch = getchar() ) xx = xx * 10 + ch - '0';
xx *= f;
}
void read() {}
template<typename T, typename ...R>void read (T &x, R &...r) {
read (x);
read (r...);
}
int main (int argc, char **argv) {
int a;
LL main_addr = (LL) main;
printf ("main %x\n", (int) main_addr);
asm (
".intel_syntax noprefix\n"
"mov rax, %2\n"
"mov ebx, dword ptr cs:\n"
"mov %0, ebx\n"
"add rax, 4\n"
"mov ebx, dword ptr cs:\n"
"mov %1, ebx\n"
"add rax, 4\n"
:"=r" (a), "=r" (a)
:"r" (main_addr)
:"%rax"
);
main_addr += 8;
asm (
".intel_syntax noprefix\n"
"mov rax, %2\n"
"mov ebx, dword ptr cs:\n"
"mov %0, ebx\n"
"add rax, 4\n"
"mov ebx, dword ptr cs:\n"
"mov %1, ebx\n"
"add rax, 4\n"
:"=r" (a), "=r" (a)
:"r" (main_addr)
:"%rax"
);
main_addr += 8;
asm (
".intel_syntax noprefix\n"
"mov rax, %2\n"
"mov ebx, dword ptr cs:\n"
"mov %0, ebx\n"
"add rax, 4\n"
"mov ebx, dword ptr cs:\n"
"mov %1, ebx\n"
"add rax, 4\n"
:"=r" (a), "=r" (a)
:"r" (main_addr)
:"%rax"
);
re_ (i, 0, 6) printf ("a[%d] = %x\n", i, a);
return 0;
}
```
注意:
1. 输出Operands和输入Operands不宜超过4个(感觉可能是我姿势不对?),否则有bug。
2. **必须指定clobbered register为`rax`**,否则出来的结果不对。我们可以通过对比两者反汇编的差异去探究具体的原因。
编译命令:
```bash
g++ -masm=intel x.cpp -o x.exe # Windows
g++ -masm=intel -no-pie x.cpp -o x # Ubuntu
```
`-no-pie`用于去除ASLR,在这里不是必需的。
#### 效果
Ubuntu下:
```
main 317f91eb # 每次都会变,但无所谓
a = fa1e0ff3
a = e5894855
a = 40ec8348
a = 48cc7d89
a = 64c07589
a = 25048b48
```
Windows下:
```
main 4015c1
a = e5894855
a = 50ec8348
a = 48104d89
a = e8185589
a = 1fb
a = e5058d48
```
通过IDA、x64dbg等工具查看main函数相关的数据,来确认结果的正确性。
### Demo2:基础的花指令
我们来出一道带有最简单的花指令的逆向题。源码如下(Windows和Ubuntu一致):
```cpp
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)
const int N = 105;
void dbg() {
puts ("");
}
template<typename T, typename... R>void dbg (const T &f, const R &... r) {
cout << f << " ";
dbg (r...);
}
template<typename Type>inline void read (Type &xx) {
Type f = 1;
char ch;
xx = 0;
for (ch = getchar(); ch < '0' || ch > '9'; ch = getchar() ) if (ch == '-') f = -1;
for (; ch >= '0' && ch <= '9'; ch = getchar() ) xx = xx * 10 + ch - '0';
xx *= f;
}
void read() {}
template<typename T, typename ...R>void read (T &x, R &...r) {
read (x);
read (r...);
}
int main (int argc, char **argv) {
string s;
cin >> s;
string t = "gmbh|tdvdug~";
asm goto (
".intel_syntax noprefix\n"
"xor rax, rax\n"
"jz %l\n"
".byte 0xEB\n"
:
:
:"%rax"
:label
);
label:
if (s.size() != t.size() ) {
puts ("Lose");
return 0;
}
bool fl = true;
re_ (i, 0, t.length() ) {
if (char (s + 1) != t) {
int tmp1 = 1;
asm goto (
".intel_syntax noprefix\n"
"mov eax, %0\n"
"test eax, eax\n"
"jnz %l\n"
".byte 0xEB\n"
:
:"r" (tmp1)
:"%rax"
:label1
);
label1:
fl = false;
break;
}
}
int tmp2 = 114514;
asm goto (
".intel_syntax noprefix\n"
"mov eax, %0\n"
"cmp eax, 114514\n"
"jz %l\n"
".byte 0xEB\n"
:
:"r" (tmp2)
:"%rax"
:label2
);
label2:
puts (fl ? "Win" : "Lose");
return 0;
}
```
这段代码有3处花指令,思路都一样,利用了`jmp`的opcode:`0xEB`,加上配套的必定跳转的语句。
gcc在内联汇编里实现跳转的资料真的特别少,我费劲心思才找到参考链接2,但参考链接2给出的写法里,也只有`asm goto`那种写法是可以通过编译的。
注意点:
1. 报错:`invalid 'asm': operand number out of range`。参考链接2给出的`%l0, %l1`的写法会引发这个错误,不清楚原因,但只需要改成`%l`就行。
2. 分清c语言标签和汇编标签,并且不要重名。
编译命令:
```bash
g++ -masm=intel 花指令hw.cpp -o 花指令hw.exe # Windows
g++ -masm=intel 花指令hw.cpp -o 花指令hw # Ubuntu
```
#### 效果
Windows:
Ubuntu:
#### 如何去除花指令
Windows:去除3个`0xEB`后,按F5即可。
```cpp
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rbx
unsigned __int64 v4; // rbx
char v5; // bl
const char *v6; // rax
char v8; // BYREF
char v9; // BYREF
char v10; // BYREF
int v11; //
int v12; //
int i; //
char v14; //
_main(argc, argv, envp);
std::string::basic_string(v9);
std::operator>><char>(refptr__ZSt3cin, v9);
std::allocator<char>::allocator(&v10);
std::string::basic_string(v8, "gmbh|tdvdug~", &v10);
std::allocator<char>::~allocator(&v10);
v3 = std::string::size(v9);
if ( v3 == std::string::size(v8) )
{
v14 = 1;
for ( i = 0; ; ++i )
{
v4 = i;
if ( v4 >= std::string::length(v8) )
break;
v5 = *(_BYTE *)std::string::operator[](v9, i) + 1;
if ( v5 != *(_BYTE *)std::string::operator[](v8, i) )
{
v12 = 1;
v14 = 0;
break;
}
}
v11 = 114514;
if ( v14 )
v6 = "Win";
else
v6 = "Lose";
puts(v6);
}
else
{
puts("Lose");
}
std::string::~string(v8);
std::string::~string(v9);
return 0;
}
```
Ubuntu:去除3个`0xEB`后,回到main函数头,鼠标右键Create Function,按F5即可。
```cpp
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rbx
unsigned __int64 v4; // rbx
char v5; // bl
const char *v6; // rax
char v8; // BYREF
char v9; //
int i; //
int v11; //
int v12; //
char v13; // BYREF
char v14; // BYREF
unsigned __int64 v15; //
v15 = __readfsqword(0x28u);
std::string::basic_string(v13, argv, envp);
std::operator>><char>(&std::cin, v13);
std::allocator<char>::allocator(&v8);
std::string::basic_string(v14, "gmbh|tdvdug~", &v8);
std::allocator<char>::~allocator(&v8);
v3 = std::string::size(v13);
if ( v3 == std::string::size(v14) )
{
v9 = 1;
for ( i = 0; ; ++i )
{
v4 = i;
if ( v4 >= std::string::length(v14) )
break;
v5 = *(_BYTE *)std::string::operator[](v13, i) + 1;
if ( v5 != *(_BYTE *)std::string::operator[](v14, i) )
{
v11 = 1;
v9 = 0;
break;
}
}
v12 = 114514;
if ( v9 )
v6 = "Win";
else
v6 = "Lose";
puts(v6);
}
else
{
puts("Lose");
}
std::string::~string(v14);
std::string::~string(v13);
return 0;
}
```
### 参考资料
1. Inline Assembly——osdev:https://wiki.osdev.org/Inline_assembly
2. gcc inline asm goto的写法:https://www.appsloveworld.com/c/100/6/labels-in-gcc-inline-assembly 膜拜膜拜,太厉害了 太厉害了,大佬!!! 又一位大佬!!! 很少用gcc
太厉害了,大佬!!! 帖子左侧那个目录是怎么实现的?发了几张帖子但是不知道怎么搞 学习一下了 RUO 发表于 2022-10-2 19:38
帖子左侧那个目录是怎么实现的?发了几张帖子但是不知道怎么搞
全文都用markdown来写,目录靠markdown的n级标题(n个”#“表示n级标题)来实现,写完了点击MD那个图标来插入markdown文本。 hans7 发表于 2022-10-2 23:47
全文都用markdown来写,目录靠markdown的n级标题(n个”#“表示n级标题)来实现,写完了点击MD那个图标来 ...
忘记还能markdown了,还用markdown的话估计的重新排版了。。只能下次再试试了
页:
[1]
2